safrano 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -1,21 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'rexml/document'
3
- require 'safrano.rb'
5
+ require_relative '../safrano/contract'
4
6
 
5
7
  # Error handling
6
- module OData
8
+ module Safrano
7
9
  # for errors occurring in API (publishing) usage --> Exceptions
8
10
  module API
9
11
  # when published class is not a Sequel Model
10
12
  class ModelNameError < NameError
11
13
  def initialize(name)
12
- super("class #{name} is not a Sequel Model", name)
14
+ super("class #{name} is not a Sequel Model")
15
+ end
16
+ end
17
+
18
+ # when published complex type is not a Complex Type
19
+ class ComplexTypeNameError < NameError
20
+ def initialize(name)
21
+ super("class #{name} is not a ComplexType")
13
22
  end
14
23
  end
24
+
15
25
  # when published class as media does not have the mandatory media fields
16
26
  class MediaModelError < NameError
17
27
  def initialize(name)
18
- super("Model #{name} does not have the mandatory media attributes content_type/media_src", name)
28
+ super("Model #{name} does not have the mandatory media attributes content_type/media_src")
19
29
  end
20
30
  end
21
31
 
@@ -31,43 +41,88 @@ module OData
31
41
 
32
42
  # base module for HTTP errors, when used as a Error Class
33
43
  module ErrorClass
44
+ include ::Safrano::Contract::Invalid
45
+ def http_code
46
+ const_get(:HTTP_CODE)
47
+ end
48
+ EMPTYH = {}.freeze
34
49
  def odata_get(req)
50
+ message = (m = @msg.to_s).empty? ? to_s : m
35
51
  if req.accept?(APPJSON)
36
- [const_get(:HTTP_CODE), CT_JSON,
37
- { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
52
+ # json is default content type so we dont need to specify it here again
53
+ [self.http_code, EMPTY_HASH,
54
+ { 'odata.error' => { 'code' => "#{http_code}",
55
+ 'type' => to_s,
56
+ 'message' => message } }.to_json]
38
57
  else
39
- [const_get(:HTTP_CODE), CT_TEXT, @msg]
58
+ [self.http_code, CT_TEXT, message]
40
59
  end
41
60
  end
42
61
  end
43
62
 
44
63
  # base module for HTTP errors, when used as an Error instance
45
64
  module ErrorInstance
65
+ include ::Safrano::Contract::Invalid
66
+ # can(should) be overriden in subclasses
67
+ def msg
68
+ @msg
69
+ end
70
+
46
71
  def odata_get(req)
72
+ message = (m = msg.to_s).empty? ? self.class.to_s : m
47
73
  if req.accept?(APPJSON)
48
- [self.class.const_get(:HTTP_CODE), CT_JSON,
49
- { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
74
+ # json is default content type so we dont need to specify it here again
75
+ [self.class.http_code, EMPTY_HASH,
76
+ { 'odata.error' => { 'code' => "#{self.class.http_code}",
77
+ 'type' => "#{self.class}",
78
+ 'message' => message } }.to_json]
50
79
  else
51
- [self.class.const_get(:HTTP_CODE), CT_TEXT, @msg]
80
+ [self.class.http_code, CT_TEXT, message]
52
81
  end
53
82
  end
54
83
  end
55
84
 
85
+ # generic http 500 server err
86
+ class ServerError
87
+ extend ErrorClass
88
+ HTTP_CODE = 500
89
+ @msg = 'Server error'
90
+ end
91
+
92
+ # for outputing Sequel exceptions that we could not prevent
93
+ class SequelExceptionError < ServerError
94
+ include ErrorInstance
95
+ def initialize(seqle7n)
96
+ @msg = seqle7n.message
97
+ end
98
+ end
99
+
100
+ # for outputing Ruby StandardError exceptions that we could not prevent
101
+ class RubyStandardErrorException < ServerError
102
+ include ErrorInstance
103
+ def initialize(rubye7n)
104
+ @msg = rubye7n.message
105
+ end
106
+ end
107
+
56
108
  # http Bad Req.
57
109
  class BadRequestError
58
110
  extend ErrorClass
111
+ include ErrorInstance
59
112
  HTTP_CODE = 400
60
- @msg = 'Bad Request Error'
113
+ @msg = 'Bad Request'
114
+ def initialize(reason)
115
+ @msg = "Bad Request : #{reason}"
116
+ end
61
117
  end
118
+
62
119
  # Generic failed changeset
63
120
  class BadRequestFailedChangeSet < BadRequestError
64
- HTTP_CODE = 400
65
121
  @msg = 'Bad Request: Failed changeset '
66
122
  end
67
123
 
68
124
  # $value request for a non-media entity
69
125
  class BadRequestNonMediaValue < BadRequestError
70
- HTTP_CODE = 400
71
126
  @msg = 'Bad Request: $value request for a non-media entity'
72
127
  end
73
128
  class BadRequestSequelAdapterError < BadRequestError
@@ -79,77 +134,127 @@ module OData
79
134
 
80
135
  # for Syntax error in Filtering
81
136
  class BadRequestFilterParseError < BadRequestError
82
- HTTP_CODE = 400
83
137
  @msg = 'Bad Request: Syntax error in $filter'
84
138
  end
85
139
 
86
- # for Syntax error in $expand param
87
- class BadRequestExpandParseError < BadRequestError
88
- HTTP_CODE = 400
89
- @msg = 'Bad Request: Syntax error in $expand'
140
+ # for invalid attribute in path
141
+ class BadRequestInvalidAttribPath < BadRequestError
142
+ include ErrorInstance
143
+ def initialize(model, attr)
144
+ @msg = "Bad Request: the attribute #{attr} is invalid for entityset #{model.entity_set_name}"
145
+ end
146
+ end
147
+
148
+ # for invalid attribute in $expand param
149
+ class BadRequestExpandInvalidPath < BadRequestError
150
+ include ErrorInstance
151
+ def initialize(model, path)
152
+ @msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
153
+ end
154
+ end
155
+ # for invalid properti(es) in $select param
156
+ class BadRequestSelectInvalidProps < BadRequestError
157
+ include ErrorInstance
158
+ def initialize(model, iprops)
159
+ @msg = ((iprops.size > 1) ? "Bad Request: the $select properties #{iprops.to_a.join(', ')} are invalid for entityset #{model.entity_set_name}" : "Bad Request: the $select property #{iprops.first} is invalid for entityset #{model.entity_set_name}")
160
+ end
90
161
  end
91
162
 
92
163
  # for Syntax error in $orderby param
93
164
  class BadRequestOrderParseError < BadRequestError
94
- HTTP_CODE = 400
95
165
  @msg = 'Bad Request: Syntax error in $orderby'
96
166
  end
97
167
 
98
168
  # for $inlinecount error
99
169
  class BadRequestInlineCountParamError < BadRequestError
100
- HTTP_CODE = 400
101
170
  @msg = 'Bad Request: wrong $inlinecount parameter'
102
171
  end
172
+
103
173
  # http not found
104
174
  class ErrorNotFound
105
175
  extend ErrorClass
106
176
  HTTP_CODE = 404
107
177
  @msg = 'The requested ressource was not found'
108
178
  end
179
+
180
+ # http not found segment
181
+ class ErrorNotFoundSegment < ErrorNotFound
182
+ include ErrorInstance
183
+
184
+ def initialize(segment)
185
+ @msg = "The requested ressource segment #{segment} was not found"
186
+ end
187
+ end
188
+
109
189
  # Transition error (Safrano specific)
110
- class ServerTransitionError
111
- extend ErrorClass
112
- HTTP_CODE = 500
190
+ class ServerTransitionError < ServerError
113
191
  @msg = 'Server error: Segment could not be parsed'
114
192
  end
115
- # generic http 500 server err
116
- class ServerError
117
- extend ErrorClass
118
- HTTP_CODE = 500
119
- @msg = 'Server error'
120
- end
193
+
121
194
  # not implemented (Safrano specific)
122
195
  class NotImplementedError
123
196
  extend ErrorClass
124
197
  HTTP_CODE = 501
125
198
  end
126
- # batch not implemented (Safrano specific)
127
- class BatchNotImplementedError
199
+
200
+ # version not implemented (Safrano specific)
201
+ class VersionNotImplementedError
128
202
  extend ErrorClass
129
203
  HTTP_CODE = 501
204
+ @msg = 'The requested OData version is not yet supported'
205
+ end
206
+ # batch not implemented (Safrano specific)
207
+ class BatchNotImplementedError < NotImplementedError
130
208
  @msg = 'Not implemented: OData batch'
131
209
  end
132
210
 
133
211
  # error in filter parsing (Safrano specific)
134
212
  class FilterParseError < BadRequestError
135
213
  extend ErrorClass
136
- HTTP_CODE = 400
137
214
  end
138
215
 
139
216
  class FilterFunctionNotImplementedError < BadRequestError
140
217
  extend ErrorClass
141
218
  include ErrorInstance
142
219
  @msg = 'the requested $filter function is Not implemented'
143
- HTTP_CODE = 400
144
- def initialize(exception)
145
- @msg = exception.to_s
220
+ def initialize(xmsg)
221
+ @msg = xmsg
146
222
  end
147
223
  end
148
- class FilterInvalidFunctionError < BadRequestError
224
+ class FilterUnknownFunctionError < BadRequestError
225
+ include ErrorInstance
226
+ def initialize(badfuncname)
227
+ @msg = "Bad request: unknown function #{badfuncname} in $filter"
228
+ end
229
+ end
230
+ class FilterParseErrorWrongColumnName < BadRequestError
231
+ extend ErrorClass
232
+ @msg = 'Bad request: invalid property name in $filter'
233
+ end
234
+ class FilterParseWrappedError < BadRequestError
149
235
  include ErrorInstance
150
- HTTP_CODE = 400
151
236
  def initialize(exception)
152
237
  @msg = exception.to_s
153
238
  end
154
239
  end
240
+ class ServiceOperationParameterMissing < BadRequestError
241
+ include ErrorInstance
242
+ def initialize(missing:, sopname:)
243
+ @msg = "Bad request: Parameter(s) missing for for service operation #{sopname} : #{missing.join(', ')}"
244
+ end
245
+ end
246
+
247
+ class ServiceOperationParameterError < BadRequestError
248
+ include ErrorInstance
249
+ def initialize(type:, value:, param:, sopname:)
250
+ @type = type
251
+ @value = value
252
+ @param = param
253
+ @sopname = sopname
254
+ end
255
+
256
+ def msg
257
+ "Bad request: Parameter #{@param} with value '#{@value}' cannot be converted to type #{@type} for service operation #{@sopname}"
258
+ end
259
+ end
155
260
  end
@@ -1,22 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'odata/error.rb'
2
4
 
3
5
  # all dataset expanding related classes in our OData module
4
6
  # ie do eager loading
5
- module OData
7
+ module Safrano
6
8
  # base class for expanding
7
9
  class ExpandBase
8
10
  EmptyExpand = new # re-useable empty expanding (idempotent)
9
11
  EMPTYH = {}.freeze
10
12
 
11
- def self.factory(expandstr)
12
- expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr)
13
+ def self.factory(expandstr, model)
14
+ expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr, model)
13
15
  end
14
16
 
15
17
  # output template
16
18
  attr_reader :template
17
19
 
18
20
  def apply_to_dataset(dtcx)
19
- dtcx
21
+ Contract.valid(dtcx)
20
22
  end
21
23
 
22
24
  def empty?
@@ -24,7 +26,7 @@ module OData
24
26
  end
25
27
 
26
28
  def parse_error?
27
- false
29
+ Contract::OK
28
30
  end
29
31
 
30
32
  def template
@@ -60,7 +62,7 @@ module OData
60
62
  end
61
63
 
62
64
  def apply_to_dataset(dtcx)
63
- dtcx
65
+ Contract.valid(dtcx)
64
66
  end
65
67
 
66
68
  def build_arg
@@ -70,11 +72,6 @@ module OData
70
72
  @template = DEEPH_1.call(@nodes)
71
73
  end
72
74
 
73
- def parse_error?
74
- # todo
75
- false
76
- end
77
-
78
75
  def empty?
79
76
  false
80
77
  end
@@ -85,19 +82,22 @@ module OData
85
82
  COMASPLIT = /\s*,\s*/.freeze
86
83
  attr_reader :template
87
84
 
88
- def initialize(expandstr)
85
+ # Note: if you change this method, please also update arity_full_monkey?
86
+ # see below
87
+ def initialize(expandstr, model)
89
88
  expandstr.strip!
89
+ @model = model
90
90
  @expandp = expandstr
91
- @exlist = []
92
91
 
93
- @exlist = expandstr.split(COMASPLIT).map { |exstr| Expand.new(exstr) }
92
+ @exstrlist = expandstr.split(COMASPLIT)
93
+ @exlist = @exstrlist.map { |exstr| Expand.new(exstr) }
94
94
  build_template
95
95
  end
96
96
 
97
97
  def apply_to_dataset(dtcx)
98
98
  # use eager loading for each used association
99
99
  @exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
100
- dtcx
100
+ Contract.valid(dtcx)
101
101
  end
102
102
 
103
103
  def build_template
@@ -112,8 +112,11 @@ module OData
112
112
  end
113
113
 
114
114
  def parse_error?
115
- # todo
116
- false
115
+ @exstrlist.each do |expstr|
116
+ return BadRequestExpandInvalidPath.new(@model, expstr) unless @model.expand_path_valid? expstr
117
+ end
118
+
119
+ Contract::OK
117
120
  end
118
121
 
119
122
  def empty?
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module OData
3
+ module Safrano
4
4
  module Filter
5
5
  # Base class for Leaves, Trees, RootTrees etc
6
6
  class Node
7
+ def success(res)
8
+ Contract.valid(res)
9
+ end
7
10
  end
8
11
 
9
12
  # Leaves are Nodes with a parent but no children
@@ -1,35 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../error'
2
4
 
3
- module OData
5
+ module Safrano
4
6
  class SequelAdapterError < StandardError
5
7
  attr_reader :inner
8
+
6
9
  def initialize(err)
7
10
  @inner = err
8
11
  end
9
12
  end
10
13
 
11
- # exception to OData error bridge
12
- module ErrorBridge
13
- # return an odata error object wrapping the exception
14
- # the odata error object should respond to odata_get for output
15
- def odata_error
16
- self.class.const_get('ODATA_ERROR_KLASS').new(self)
17
- end
18
- end
19
-
20
14
  module Filter
21
- class FunctionNotImplemented < StandardError
22
- ODATA_ERROR_KLASS = OData::FilterFunctionNotImplementedError
23
- include ::OData::ErrorBridge
24
- end
25
-
26
15
  class Parser
27
16
  # Parser errors
28
- class Error < StandardError
17
+
18
+ class Error
19
+ def Error.http_code
20
+ const_get(:HTTP_CODE)
21
+ end
22
+ HTTP_CODE = 400
23
+
29
24
  attr_reader :tok
30
25
  attr_reader :typ
31
26
  attr_reader :cur_val
32
27
  attr_reader :cur_typ
28
+
33
29
  def initialize(tok, typ, cur)
34
30
  @tok = tok
35
31
  @typ = typ
@@ -41,41 +37,61 @@ module OData
41
37
  end
42
38
  # Invalid Tokens
43
39
  class ErrorInvalidToken < Error
40
+ include ::Safrano::ErrorInstance
41
+ def initialize(tok, typ, cur)
42
+ super
43
+ @msg = "Bad Request: invalid token #{tok} in $filter"
44
+ end
44
45
  end
45
46
  # Unmached closed
46
47
  class ErrorUnmatchedClose < Error
48
+ include ::Safrano::ErrorInstance
49
+ def initialize(tok, typ, cur)
50
+ super
51
+ @msg = "Bad Request: unmatched #{tok} in $filter"
52
+ end
47
53
  end
48
54
 
49
- class ErrorFunctionArgumentType < StandardError
55
+ class ErrorFunctionArgumentType
56
+ include ::Safrano::ErrorInstance
50
57
  end
51
58
 
52
- class ErrorWrongColumnName < StandardError
59
+ class ErrorWrongColumnName
60
+ include ::Safrano::ErrorInstance
53
61
  end
54
62
 
55
63
  # attempt to add a child to a Leave
56
- class ErrorLeaveChild < StandardError
57
- end
58
-
59
- # invalid function error (literal attach to IdentityFuncTree)
60
- class ErrorInvalidFunction < StandardError
61
- ODATA_ERROR_KLASS = OData::FilterInvalidFunctionError
62
- include ::OData::ErrorBridge
64
+ class ErrorLeaveChild
65
+ include ::Safrano::ErrorInstance
63
66
  end
64
67
 
65
68
  # Invalid function arity
66
69
  class ErrorInvalidArity < Error
70
+ include ::Safrano::ErrorInstance
71
+ def initialize(tok, typ, cur)
72
+ super
73
+ @msg = "Bad Request: wrong number of parameters for function #{cur.parent.value.to_s} in $filter"
74
+ end
67
75
  end
68
76
  # Invalid separator in this context (missing parenthesis?)
69
77
  class ErrorInvalidSeparator < Error
78
+ include ::Safrano::ErrorInstance
70
79
  end
71
80
 
72
81
  # unmatched quot3
73
82
  class UnmatchedQuoteError < Error
83
+ include ::Safrano::ErrorInstance
84
+ def initialize(tok, typ, cur)
85
+ super
86
+ @msg = "Bad Request: unbalanced quotes #{tok} in $filter"
87
+ end
74
88
  end
75
89
 
76
90
  # wrong type of function argument
77
- class ErrorInvalidArgumentType < StandardError
78
- def initialize(tree, expected:, actual:)
91
+ class ErrorInvalidArgumentType < Error
92
+ include ::Safrano::ErrorInstance
93
+
94
+ def initialize(tree, expected:, actual:)
79
95
  @tree = tree
80
96
  @expected = expected
81
97
  @actual = actual