safrano 0.4.3 → 0.5.1

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