safrano 0.4.3 → 0.5.1

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 +8 -4
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +139 -642
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  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 +196 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +78 -123
  23. data/lib/odata/error.rb +170 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +9 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +39 -25
  28. data/lib/odata/filter/sequel.rb +112 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +21 -18
  31. data/lib/odata/filter/tree.rb +78 -44
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +641 -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 +18 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +141 -0
  42. data/lib/safrano/core.rb +24 -106
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -24
  46. data/lib/safrano/rack_app.rb +62 -63
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +96 -38
  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 +156 -110
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +30 -11
data/lib/odata/error.rb CHANGED
@@ -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
 
@@ -27,47 +37,120 @@ module OData
27
37
  super(msg, symbname)
28
38
  end
29
39
  end
40
+
41
+ # duplicate attribute name
42
+ class ModelDuplicateAttributeError < NameError
43
+ def initialize(klass, symb)
44
+ symbname = symb.to_s
45
+ msg = "There is already an attribute :#{symbname} defined in class #{klass}"
46
+ super(msg, symbname)
47
+ end
48
+ end
30
49
  end
31
50
 
32
51
  # base module for HTTP errors, when used as a Error Class
33
52
  module ErrorClass
53
+ include ::Safrano::Contract::Invalid
54
+ def http_code
55
+ const_get(:HTTP_CODE)
56
+ end
57
+ EMPTYH = {}.freeze
34
58
  def odata_get(req)
59
+ message = (m = @msg.to_s).empty? ? to_s : m
35
60
  if req.accept?(APPJSON)
36
- [const_get(:HTTP_CODE), CT_JSON,
37
- { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
61
+ # json is default content type so we dont need to specify it here again
62
+ [self.http_code, EMPTY_HASH,
63
+ { 'odata.error' => { 'code' => "#{http_code}",
64
+ 'type' => to_s,
65
+ 'message' => message } }.to_json]
38
66
  else
39
- [const_get(:HTTP_CODE), CT_TEXT, @msg]
67
+ [self.http_code, CT_TEXT, message]
40
68
  end
41
69
  end
42
70
  end
43
71
 
44
72
  # base module for HTTP errors, when used as an Error instance
45
73
  module ErrorInstance
74
+ include ::Safrano::Contract::Invalid
75
+ # can(should) be overriden in subclasses
76
+ def msg
77
+ @msg
78
+ end
79
+
46
80
  def odata_get(req)
81
+ message = (m = msg.to_s).empty? ? self.class.to_s : m
47
82
  if req.accept?(APPJSON)
48
- [self.class.const_get(:HTTP_CODE), CT_JSON,
49
- { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
83
+ # json is default content type so we dont need to specify it here again
84
+ [self.class.http_code, EMPTY_HASH,
85
+ { 'odata.error' => { 'code' => "#{self.class.http_code}",
86
+ 'type' => "#{self.class}",
87
+ 'message' => message } }.to_json]
50
88
  else
51
- [self.class.const_get(:HTTP_CODE), CT_TEXT, @msg]
89
+ [self.class.http_code, CT_TEXT, message]
52
90
  end
53
91
  end
54
92
  end
55
93
 
94
+ # generic http 500 server err
95
+ class ServerError
96
+ extend ErrorClass
97
+ HTTP_CODE = 500
98
+ @msg = 'Server error'
99
+ end
100
+
101
+ # for outputing Sequel exceptions that we could not prevent
102
+ class SequelExceptionError < ServerError
103
+ include ErrorInstance
104
+ def initialize(seqle7n)
105
+ @msg = seqle7n.message
106
+ end
107
+ end
108
+
109
+ # for outputing Ruby StandardError exceptions that we could not prevent
110
+ class RubyStandardErrorException < ServerError
111
+ include ErrorInstance
112
+ def initialize(rubye7n)
113
+ @msg = rubye7n.message
114
+ end
115
+ end
116
+
117
+ # http Unprocessable Entity for example when trying to
118
+ # upload duplicated media ressource
119
+ class UnprocessableEntityError
120
+ extend ErrorClass
121
+ include ErrorInstance
122
+ HTTP_CODE = 422
123
+ @msg = 'Unprocessable Entity'
124
+ def initialize(reason)
125
+ @msg = reason
126
+ end
127
+ end
128
+
56
129
  # http Bad Req.
57
130
  class BadRequestError
58
131
  extend ErrorClass
132
+ include ErrorInstance
59
133
  HTTP_CODE = 400
60
- @msg = 'Bad Request Error'
134
+ @msg = 'Bad Request'
135
+ def initialize(reason)
136
+ @msg = "Bad Request : #{reason}"
137
+ end
138
+ end
139
+
140
+ # for upload empty media
141
+ class BadRequestEmptyMediaUpload < BadRequestError
142
+ include ErrorInstance
143
+ def initialize(path)
144
+ @msg = "Bad Request: empty media file #{path}"
145
+ end
61
146
  end
62
147
  # Generic failed changeset
63
148
  class BadRequestFailedChangeSet < BadRequestError
64
- HTTP_CODE = 400
65
149
  @msg = 'Bad Request: Failed changeset '
66
150
  end
67
151
 
68
152
  # $value request for a non-media entity
69
153
  class BadRequestNonMediaValue < BadRequestError
70
- HTTP_CODE = 400
71
154
  @msg = 'Bad Request: $value request for a non-media entity'
72
155
  end
73
156
  class BadRequestSequelAdapterError < BadRequestError
@@ -79,77 +162,127 @@ module OData
79
162
 
80
163
  # for Syntax error in Filtering
81
164
  class BadRequestFilterParseError < BadRequestError
82
- HTTP_CODE = 400
83
165
  @msg = 'Bad Request: Syntax error in $filter'
84
166
  end
85
167
 
86
- # for Syntax error in $expand param
87
- class BadRequestExpandParseError < BadRequestError
88
- HTTP_CODE = 400
89
- @msg = 'Bad Request: Syntax error in $expand'
168
+ # for invalid attribute in path
169
+ class BadRequestInvalidAttribPath < BadRequestError
170
+ include ErrorInstance
171
+ def initialize(model, attr)
172
+ @msg = "Bad Request: the attribute #{attr} is invalid for entityset #{model.entity_set_name}"
173
+ end
174
+ end
175
+
176
+ # for invalid attribute in $expand param
177
+ class BadRequestExpandInvalidPath < BadRequestError
178
+ include ErrorInstance
179
+ def initialize(model, path)
180
+ @msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
181
+ end
182
+ end
183
+ # for invalid properti(es) in $select param
184
+ class BadRequestSelectInvalidProps < BadRequestError
185
+ include ErrorInstance
186
+ def initialize(model, iprops)
187
+ @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}")
188
+ end
90
189
  end
91
190
 
92
191
  # for Syntax error in $orderby param
93
192
  class BadRequestOrderParseError < BadRequestError
94
- HTTP_CODE = 400
95
193
  @msg = 'Bad Request: Syntax error in $orderby'
96
194
  end
97
195
 
98
196
  # for $inlinecount error
99
197
  class BadRequestInlineCountParamError < BadRequestError
100
- HTTP_CODE = 400
101
198
  @msg = 'Bad Request: wrong $inlinecount parameter'
102
199
  end
200
+
103
201
  # http not found
104
202
  class ErrorNotFound
105
203
  extend ErrorClass
106
204
  HTTP_CODE = 404
107
205
  @msg = 'The requested ressource was not found'
108
206
  end
207
+
208
+ # http not found segment
209
+ class ErrorNotFoundSegment < ErrorNotFound
210
+ include ErrorInstance
211
+
212
+ def initialize(segment)
213
+ @msg = "The requested ressource segment #{segment} was not found"
214
+ end
215
+ end
216
+
109
217
  # Transition error (Safrano specific)
110
- class ServerTransitionError
111
- extend ErrorClass
112
- HTTP_CODE = 500
218
+ class ServerTransitionError < ServerError
113
219
  @msg = 'Server error: Segment could not be parsed'
114
220
  end
115
- # generic http 500 server err
116
- class ServerError
117
- extend ErrorClass
118
- HTTP_CODE = 500
119
- @msg = 'Server error'
120
- end
221
+
121
222
  # not implemented (Safrano specific)
122
223
  class NotImplementedError
123
224
  extend ErrorClass
124
225
  HTTP_CODE = 501
125
226
  end
126
- # batch not implemented (Safrano specific)
127
- class BatchNotImplementedError
227
+
228
+ # version not implemented (Safrano specific)
229
+ class VersionNotImplementedError
128
230
  extend ErrorClass
129
231
  HTTP_CODE = 501
232
+ @msg = 'The requested OData version is not yet supported'
233
+ end
234
+ # batch not implemented (Safrano specific)
235
+ class BatchNotImplementedError < NotImplementedError
130
236
  @msg = 'Not implemented: OData batch'
131
237
  end
132
238
 
133
239
  # error in filter parsing (Safrano specific)
134
240
  class FilterParseError < BadRequestError
135
241
  extend ErrorClass
136
- HTTP_CODE = 400
137
242
  end
138
243
 
139
244
  class FilterFunctionNotImplementedError < BadRequestError
140
245
  extend ErrorClass
141
246
  include ErrorInstance
142
247
  @msg = 'the requested $filter function is Not implemented'
143
- HTTP_CODE = 400
144
- def initialize(exception)
145
- @msg = exception.to_s
248
+ def initialize(xmsg)
249
+ @msg = xmsg
146
250
  end
147
251
  end
148
- class FilterInvalidFunctionError < BadRequestError
252
+ class FilterUnknownFunctionError < BadRequestError
253
+ include ErrorInstance
254
+ def initialize(badfuncname)
255
+ @msg = "Bad request: unknown function #{badfuncname} in $filter"
256
+ end
257
+ end
258
+ class FilterParseErrorWrongColumnName < BadRequestError
259
+ extend ErrorClass
260
+ @msg = 'Bad request: invalid property name in $filter'
261
+ end
262
+ class FilterParseWrappedError < BadRequestError
149
263
  include ErrorInstance
150
- HTTP_CODE = 400
151
264
  def initialize(exception)
152
265
  @msg = exception.to_s
153
266
  end
154
267
  end
268
+ class ServiceOperationParameterMissing < BadRequestError
269
+ include ErrorInstance
270
+ def initialize(missing:, sopname:)
271
+ @msg = "Bad request: Parameter(s) missing for for service operation #{sopname} : #{missing.join(', ')}"
272
+ end
273
+ end
274
+
275
+ class ServiceOperationParameterError < BadRequestError
276
+ include ErrorInstance
277
+ def initialize(type:, value:, param:, sopname:)
278
+ @type = type
279
+ @value = value
280
+ @param = param
281
+ @sopname = sopname
282
+ end
283
+
284
+ def msg
285
+ "Bad request: Parameter #{@param} with value '#{@value}' cannot be converted to type #{@type} for service operation #{@sopname}"
286
+ end
287
+ end
155
288
  end
data/lib/odata/expand.rb CHANGED
@@ -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
@@ -54,6 +57,11 @@ module OData
54
57
  class Literal < Leave
55
58
  end
56
59
 
60
+ # Null Literal is unquoted null word
61
+ class NullLiteral < Literal
62
+ LEUQES = nil
63
+ end
64
+
57
65
  # Qualit (qualified lits) are words separated by /
58
66
  # path/path/path/attrib
59
67
  class Qualit < Literal
@@ -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