safrano 0.4.1 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
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 +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +155 -99
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  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 +183 -216
  23. data/lib/odata/error.rb +195 -31
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -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")
13
15
  end
14
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")
22
+ end
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
@@ -76,49 +159,130 @@ module OData
76
159
  @msg = err.inner.message
77
160
  end
78
161
  end
79
-
162
+
80
163
  # for Syntax error in Filtering
81
164
  class BadRequestFilterParseError < BadRequestError
82
- HTTP_CODE = 400
83
- @msg = 'Bad Request: Syntax error in Filter'
165
+ @msg = 'Bad Request: Syntax error in $filter'
84
166
  end
167
+
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
189
+ end
190
+
191
+ # for Syntax error in $orderby param
192
+ class BadRequestOrderParseError < BadRequestError
193
+ @msg = 'Bad Request: Syntax error in $orderby'
194
+ end
195
+
85
196
  # for $inlinecount error
86
197
  class BadRequestInlineCountParamError < BadRequestError
87
- HTTP_CODE = 400
88
198
  @msg = 'Bad Request: wrong $inlinecount parameter'
89
199
  end
200
+
90
201
  # http not found
91
202
  class ErrorNotFound
92
203
  extend ErrorClass
93
204
  HTTP_CODE = 404
94
205
  @msg = 'The requested ressource was not found'
95
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
+
96
217
  # Transition error (Safrano specific)
97
- class ServerTransitionError
98
- extend ErrorClass
99
- HTTP_CODE = 500
218
+ class ServerTransitionError < ServerError
100
219
  @msg = 'Server error: Segment could not be parsed'
101
220
  end
102
- # generic http 500 server err
103
- class ServerError
104
- extend ErrorClass
105
- HTTP_CODE = 500
106
- @msg = 'Server error'
107
- end
221
+
108
222
  # not implemented (Safrano specific)
109
223
  class NotImplementedError
110
224
  extend ErrorClass
111
225
  HTTP_CODE = 501
112
226
  end
113
- # batch not implemented (Safrano specific)
114
- class BatchNotImplementedError
227
+
228
+ # version not implemented (Safrano specific)
229
+ class VersionNotImplementedError
115
230
  extend ErrorClass
116
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
117
236
  @msg = 'Not implemented: OData batch'
118
237
  end
238
+
119
239
  # error in filter parsing (Safrano specific)
120
240
  class FilterParseError < BadRequestError
121
241
  extend ErrorClass
122
- HTTP_CODE = 400
242
+ end
243
+
244
+ class FilterFunctionNotImplementedError < BadRequestError
245
+ extend ErrorClass
246
+ include ErrorInstance
247
+ @msg = 'the requested $filter function is Not implemented'
248
+ def initialize(xmsg)
249
+ @msg = xmsg
250
+ end
251
+ end
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
263
+ include ErrorInstance
264
+ def initialize(exception)
265
+ @msg = exception.to_s
266
+ end
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
123
287
  end
124
288
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'odata/error.rb'
4
+
5
+ # all dataset expanding related classes in our OData module
6
+ # ie do eager loading
7
+ module Safrano
8
+ # base class for expanding
9
+ class ExpandBase
10
+ EmptyExpand = new # re-useable empty expanding (idempotent)
11
+ EMPTYH = {}.freeze
12
+
13
+ def self.factory(expandstr, model)
14
+ expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr, model)
15
+ end
16
+
17
+ # output template
18
+ attr_reader :template
19
+
20
+ def apply_to_dataset(dtcx)
21
+ Contract.valid(dtcx)
22
+ end
23
+
24
+ def empty?
25
+ true
26
+ end
27
+
28
+ def parse_error?
29
+ Contract::OK
30
+ end
31
+
32
+ def template
33
+ EMPTYH
34
+ end
35
+ end
36
+
37
+ # single expand
38
+ class Expand < ExpandBase
39
+ # sequel eager arg.
40
+ attr_reader :arg
41
+ attr_reader :template
42
+
43
+ # used for Sequel eager argument
44
+ # Recursive array to deep hash
45
+ # [1,2,3,4] --> {1=>{2=>{3=>4}}}
46
+ # [1] --> 1
47
+ DEEPH_0 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_0.call(inp[1..-1]) } : inp[0] }
48
+
49
+ # used for building output template
50
+ # Recursive array to deep hash
51
+ # [1,2,3,4] --> {1=>{2=>{3=>4}}}
52
+ # [1] --> { 1 => {} }
53
+ DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
54
+
55
+ NODESEP = '/'.freeze
56
+
57
+ def initialize(exstr)
58
+ exstr.strip!
59
+ @expandp = exstr
60
+ @nodes = @expandp.split(NODESEP)
61
+ build_arg
62
+ end
63
+
64
+ def apply_to_dataset(dtcx)
65
+ Contract.valid(dtcx)
66
+ end
67
+
68
+ def build_arg
69
+ # 'a/b/c/d' ==> {a: {b:{c: :d}}}
70
+ # 'xy' ==> :xy
71
+ @arg = DEEPH_0.call(@nodes.map(&:to_sym))
72
+ @template = DEEPH_1.call(@nodes)
73
+ end
74
+
75
+ def empty?
76
+ false
77
+ end
78
+ end
79
+
80
+ # Multi expanding logic
81
+ class MultiExpand < ExpandBase
82
+ COMASPLIT = /\s*,\s*/.freeze
83
+ attr_reader :template
84
+
85
+ # Note: if you change this method, please also update arity_full_monkey?
86
+ # see below
87
+ def initialize(expandstr, model)
88
+ expandstr.strip!
89
+ @model = model
90
+ @expandp = expandstr
91
+
92
+ @exstrlist = expandstr.split(COMASPLIT)
93
+ @exlist = @exstrlist.map { |exstr| Expand.new(exstr) }
94
+ build_template
95
+ end
96
+
97
+ def apply_to_dataset(dtcx)
98
+ # use eager loading for each used association
99
+ @exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
100
+ Contract.valid(dtcx)
101
+ end
102
+
103
+ def build_template
104
+ # 'a/b/c/d,xy' ==> [ {'a' =>{ 'b' => {'c' => {'d' => {} } }}},
105
+ # { 'xy' => {} }]
106
+ #
107
+ @template = @exlist.map(&:template)
108
+
109
+ # { 'a' => { 'b' => {'c' => 'd' }},
110
+ # 'xy' => {} }
111
+ @template = @template.inject({}) { |mrg, elmt| mrg.merge elmt }
112
+ end
113
+
114
+ def parse_error?
115
+ @exstrlist.each do |expstr|
116
+ return BadRequestExpandInvalidPath.new(@model, expstr) unless @model.expand_path_valid? expstr
117
+ end
118
+
119
+ Contract::OK
120
+ end
121
+
122
+ def empty?
123
+ false
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module Filter
5
+ # Base class for Leaves, Trees, RootTrees etc
6
+ class Node
7
+ def success(res)
8
+ Contract.valid(res)
9
+ end
10
+ end
11
+
12
+ # Leaves are Nodes with a parent but no children
13
+ class Leave < Node
14
+ end
15
+
16
+ # RootTrees have childrens but no parent
17
+ class RootTree < Node
18
+ end
19
+
20
+ # Tree's have Parent and children
21
+ class Tree < RootTree
22
+ end
23
+
24
+ # For functions... should have a single child---> the argument list
25
+ class FuncTree < Tree
26
+ end
27
+
28
+ # Indentity Func to use as "parent" func of parenthesis expressions
29
+ # --> allow to handle generically parenthesis always as argument of
30
+ # some function
31
+ class IdentityFuncTree < FuncTree
32
+ end
33
+
34
+ # unary op eg. NOT
35
+ class UnopTree < Tree
36
+ end
37
+
38
+ # Bin ops
39
+ class BinopTree < Tree
40
+ end
41
+
42
+ class BinopBool < BinopTree
43
+ end
44
+
45
+ class BinopArithm < BinopTree
46
+ end
47
+
48
+ # Arguments or lists
49
+ class ArgTree < Tree
50
+ end
51
+
52
+ # Numbers (floating point, ints, dec)
53
+ class FPNumber < Leave
54
+ end
55
+
56
+ # Literals are unquoted words without /
57
+ class Literal < Leave
58
+ end
59
+
60
+ # Null Literal is unquoted null word
61
+ class NullLiteral < Literal
62
+ LEUQES = nil
63
+ end
64
+
65
+ # Qualit (qualified lits) are words separated by /
66
+ # path/path/path/attrib
67
+ class Qualit < Literal
68
+ end
69
+
70
+ # Quoted Strings
71
+ class QString < Leave
72
+ end
73
+ end
74
+ end