safrano 0.4.0 → 0.4.5

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 +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 +145 -74
  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 +151 -197
  23. data/lib/odata/error.rb +175 -32
  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 +637 -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 +19 -11
  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 +264 -220
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -12
@@ -1,23 +1,31 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
4
  require 'rexml/document'
5
- require 'safrano.rb'
5
+ require_relative '../safrano/contract'
6
6
 
7
7
  # Error handling
8
- module OData
8
+ module Safrano
9
9
  # for errors occurring in API (publishing) usage --> Exceptions
10
10
  module API
11
11
  # when published class is not a Sequel Model
12
12
  class ModelNameError < NameError
13
13
  def initialize(name)
14
- super("class #{name} is not a Sequel Model", name)
14
+ super("class #{name} is not a Sequel Model")
15
15
  end
16
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
+
17
25
  # when published class as media does not have the mandatory media fields
18
26
  class MediaModelError < NameError
19
27
  def initialize(name)
20
- 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")
21
29
  end
22
30
  end
23
31
 
@@ -29,47 +37,101 @@ module OData
29
37
  super(msg, symbname)
30
38
  end
31
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
32
49
  end
33
50
 
34
51
  # base module for HTTP errors, when used as a Error Class
35
52
  module ErrorClass
53
+ include ::Safrano::Contract::Invalid
54
+ def http_code
55
+ const_get(:HTTP_CODE)
56
+ end
57
+ EMPTYH = {}.freeze
36
58
  def odata_get(req)
59
+ message = (m = @msg.to_s).empty? ? to_s : m
37
60
  if req.accept?(APPJSON)
38
- [const_get(:HTTP_CODE), CT_JSON,
39
- { '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]
40
66
  else
41
- [const_get(:HTTP_CODE), CT_TEXT, @msg]
67
+ [self.http_code, CT_TEXT, message]
42
68
  end
43
69
  end
44
70
  end
45
-
71
+
46
72
  # base module for HTTP errors, when used as an Error instance
47
73
  module ErrorInstance
74
+ include ::Safrano::Contract::Invalid
75
+ # can(should) be overriden in subclasses
76
+ def msg
77
+ @msg
78
+ end
79
+
48
80
  def odata_get(req)
81
+ message = (m = msg.to_s).empty? ? self.class.to_s : m
49
82
  if req.accept?(APPJSON)
50
- [self.class.const_get(:HTTP_CODE), CT_JSON,
51
- { '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]
52
88
  else
53
- [self.class.const_get(:HTTP_CODE), CT_TEXT, @msg]
89
+ [self.class.http_code, CT_TEXT, message]
54
90
  end
55
91
  end
56
92
  end
57
-
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
+
58
117
  # http Bad Req.
59
118
  class BadRequestError
60
119
  extend ErrorClass
120
+ include ErrorInstance
61
121
  HTTP_CODE = 400
62
- @msg = 'Bad Request Error'
122
+ @msg = 'Bad Request'
123
+ def initialize(reason)
124
+ @msg = "Bad Request : #{reason}"
125
+ end
63
126
  end
127
+
64
128
  # Generic failed changeset
65
129
  class BadRequestFailedChangeSet < BadRequestError
66
- HTTP_CODE = 400
67
130
  @msg = 'Bad Request: Failed changeset '
68
131
  end
69
132
 
70
133
  # $value request for a non-media entity
71
134
  class BadRequestNonMediaValue < BadRequestError
72
- HTTP_CODE = 400
73
135
  @msg = 'Bad Request: $value request for a non-media entity'
74
136
  end
75
137
  class BadRequestSequelAdapterError < BadRequestError
@@ -78,49 +140,130 @@ module OData
78
140
  @msg = err.inner.message
79
141
  end
80
142
  end
81
-
143
+
82
144
  # for Syntax error in Filtering
83
145
  class BadRequestFilterParseError < BadRequestError
84
- HTTP_CODE = 400
85
- @msg = 'Bad Request: Syntax error in Filter'
146
+ @msg = 'Bad Request: Syntax error in $filter'
147
+ end
148
+
149
+ # for invalid attribute in path
150
+ class BadRequestInvalidAttribPath < BadRequestError
151
+ include ErrorInstance
152
+ def initialize(model, attr)
153
+ @msg = "Bad Request: the attribute #{attr} is invalid for entityset #{model.entity_set_name}"
154
+ end
155
+ end
156
+
157
+ # for invalid attribute in $expand param
158
+ class BadRequestExpandInvalidPath < BadRequestError
159
+ include ErrorInstance
160
+ def initialize(model, path)
161
+ @msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
162
+ end
163
+ end
164
+ # for invalid properti(es) in $select param
165
+ class BadRequestSelectInvalidProps < BadRequestError
166
+ include ErrorInstance
167
+ def initialize(model, iprops)
168
+ @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}")
169
+ end
170
+ end
171
+
172
+ # for Syntax error in $orderby param
173
+ class BadRequestOrderParseError < BadRequestError
174
+ @msg = 'Bad Request: Syntax error in $orderby'
86
175
  end
176
+
87
177
  # for $inlinecount error
88
178
  class BadRequestInlineCountParamError < BadRequestError
89
- HTTP_CODE = 400
90
179
  @msg = 'Bad Request: wrong $inlinecount parameter'
91
180
  end
181
+
92
182
  # http not found
93
183
  class ErrorNotFound
94
184
  extend ErrorClass
95
185
  HTTP_CODE = 404
96
186
  @msg = 'The requested ressource was not found'
97
187
  end
188
+
189
+ # http not found segment
190
+ class ErrorNotFoundSegment < ErrorNotFound
191
+ include ErrorInstance
192
+
193
+ def initialize(segment)
194
+ @msg = "The requested ressource segment #{segment} was not found"
195
+ end
196
+ end
197
+
98
198
  # Transition error (Safrano specific)
99
- class ServerTransitionError
100
- extend ErrorClass
101
- HTTP_CODE = 500
199
+ class ServerTransitionError < ServerError
102
200
  @msg = 'Server error: Segment could not be parsed'
103
201
  end
104
- # generic http 500 server err
105
- class ServerError
106
- extend ErrorClass
107
- HTTP_CODE = 500
108
- @msg = 'Server error'
109
- end
202
+
110
203
  # not implemented (Safrano specific)
111
204
  class NotImplementedError
112
205
  extend ErrorClass
113
206
  HTTP_CODE = 501
114
207
  end
115
- # batch not implemented (Safrano specific)
116
- class BatchNotImplementedError
208
+
209
+ # version not implemented (Safrano specific)
210
+ class VersionNotImplementedError
117
211
  extend ErrorClass
118
212
  HTTP_CODE = 501
213
+ @msg = 'The requested OData version is not yet supported'
214
+ end
215
+ # batch not implemented (Safrano specific)
216
+ class BatchNotImplementedError < NotImplementedError
119
217
  @msg = 'Not implemented: OData batch'
120
218
  end
219
+
121
220
  # error in filter parsing (Safrano specific)
122
221
  class FilterParseError < BadRequestError
123
222
  extend ErrorClass
124
- HTTP_CODE = 400
223
+ end
224
+
225
+ class FilterFunctionNotImplementedError < BadRequestError
226
+ extend ErrorClass
227
+ include ErrorInstance
228
+ @msg = 'the requested $filter function is Not implemented'
229
+ def initialize(xmsg)
230
+ @msg = xmsg
231
+ end
232
+ end
233
+ class FilterUnknownFunctionError < BadRequestError
234
+ include ErrorInstance
235
+ def initialize(badfuncname)
236
+ @msg = "Bad request: unknown function #{badfuncname} in $filter"
237
+ end
238
+ end
239
+ class FilterParseErrorWrongColumnName < BadRequestError
240
+ extend ErrorClass
241
+ @msg = 'Bad request: invalid property name in $filter'
242
+ end
243
+ class FilterParseWrappedError < BadRequestError
244
+ include ErrorInstance
245
+ def initialize(exception)
246
+ @msg = exception.to_s
247
+ end
248
+ end
249
+ class ServiceOperationParameterMissing < BadRequestError
250
+ include ErrorInstance
251
+ def initialize(missing:, sopname:)
252
+ @msg = "Bad request: Parameter(s) missing for for service operation #{sopname} : #{missing.join(', ')}"
253
+ end
254
+ end
255
+
256
+ class ServiceOperationParameterError < BadRequestError
257
+ include ErrorInstance
258
+ def initialize(type:, value:, param:, sopname:)
259
+ @type = type
260
+ @value = value
261
+ @param = param
262
+ @sopname = sopname
263
+ end
264
+
265
+ def msg
266
+ "Bad request: Parameter #{@param} with value '#{@value}' cannot be converted to type #{@type} for service operation #{@sopname}"
267
+ end
125
268
  end
126
269
  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