safrano 0.4.2 → 0.5.0

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 +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