safrano 0.4.2 → 0.5.0

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 +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  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 +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  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 +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  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 +106 -52
  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 +13 -26
  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 +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  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 +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -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
@@ -79,59 +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
238
+
132
239
  # error in filter parsing (Safrano specific)
133
240
  class FilterParseError < BadRequestError
134
241
  extend ErrorClass
135
- 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
136
287
  end
137
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?
@@ -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