safrano 0.3.4 → 0.4.4

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 (57) 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 +17 -15
  15. data/lib/odata/collection.rb +141 -500
  16. data/lib/odata/collection_filter.rb +44 -37
  17. data/lib/odata/collection_media.rb +193 -43
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +39 -12
  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 +201 -176
  23. data/lib/odata/error.rb +186 -33
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +69 -0
  26. data/lib/odata/filter/error.rb +55 -6
  27. data/lib/odata/filter/parse.rb +38 -36
  28. data/lib/odata/filter/sequel.rb +121 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +15 -11
  31. data/lib/odata/filter/tree.rb +110 -60
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +50 -32
  35. data/lib/odata/relations.rb +7 -7
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +18 -28
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +43 -0
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
  46. data/lib/safrano/rack_app.rb +175 -0
  47. data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
  48. data/lib/{request.rb → safrano/request.rb} +102 -50
  49. data/lib/{response.rb → safrano/response.rb} +5 -4
  50. data/lib/safrano/sequel_join_by_paths.rb +5 -0
  51. data/lib/{service.rb → safrano/service.rb} +257 -188
  52. data/lib/safrano/version.rb +5 -0
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +53 -17
  55. data/lib/rack_app.rb +0 -174
  56. data/lib/sequel_join_by_paths.rb +0 -5
  57. data/lib/version.rb +0 -4
@@ -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
 
@@ -31,77 +39,222 @@ module OData
31
39
  end
32
40
  end
33
41
 
34
- # base module for HTTP errors
35
- module Error
42
+ # base module for HTTP errors, when used as a Error Class
43
+ module ErrorClass
44
+ include ::Safrano::Contract::Invalid
45
+ def http_code
46
+ const_get(:HTTP_CODE)
47
+ end
48
+ EMPTYH = {}.freeze
36
49
  def odata_get(req)
50
+ message = (m = @msg.to_s).empty? ? to_s : m
37
51
  if req.accept?(APPJSON)
38
- [const_get(:HTTP_CODE), CT_JSON,
39
- { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
52
+ # json is default content type so we dont need to specify it here again
53
+ [self.http_code, EMPTY_HASH,
54
+ { 'odata.error' => { 'code' => "#{http_code}",
55
+ 'type' => to_s,
56
+ 'message' => message } }.to_json]
40
57
  else
41
- [const_get(:HTTP_CODE), CT_TEXT, @msg]
58
+ [self.http_code, CT_TEXT, message]
42
59
  end
43
60
  end
44
61
  end
62
+
63
+ # base module for HTTP errors, when used as an Error instance
64
+ module ErrorInstance
65
+ include ::Safrano::Contract::Invalid
66
+ # can(should) be overriden in subclasses
67
+ def msg
68
+ @msg
69
+ end
70
+
71
+ def odata_get(req)
72
+ message = (m = msg.to_s).empty? ? self.class.to_s : m
73
+ if req.accept?(APPJSON)
74
+ # json is default content type so we dont need to specify it here again
75
+ [self.class.http_code, EMPTY_HASH,
76
+ { 'odata.error' => { 'code' => "#{self.class.http_code}",
77
+ 'type' => "#{self.class}",
78
+ 'message' => message } }.to_json]
79
+ else
80
+ [self.class.http_code, CT_TEXT, message]
81
+ end
82
+ end
83
+ end
84
+
85
+ # generic http 500 server err
86
+ class ServerError
87
+ extend ErrorClass
88
+ HTTP_CODE = 500
89
+ @msg = 'Server error'
90
+ end
91
+
92
+ # for outputing Sequel exceptions that we could not prevent
93
+ class SequelExceptionError < ServerError
94
+ include ErrorInstance
95
+ def initialize(seqle7n)
96
+ @msg = seqle7n.message
97
+ end
98
+ end
99
+
100
+ # for outputing Ruby StandardError exceptions that we could not prevent
101
+ class RubyStandardErrorException < ServerError
102
+ include ErrorInstance
103
+ def initialize(rubye7n)
104
+ @msg = rubye7n.message
105
+ end
106
+ end
107
+
45
108
  # http Bad Req.
46
109
  class BadRequestError
47
- extend Error
110
+ extend ErrorClass
111
+ include ErrorInstance
48
112
  HTTP_CODE = 400
49
- @msg = 'Bad Request Error'
113
+ @msg = 'Bad Request'
114
+ def initialize(reason)
115
+ @msg = "Bad Request : #{reason}"
116
+ end
50
117
  end
118
+
51
119
  # Generic failed changeset
52
120
  class BadRequestFailedChangeSet < BadRequestError
53
- HTTP_CODE = 400
54
121
  @msg = 'Bad Request: Failed changeset '
55
122
  end
56
123
 
57
124
  # $value request for a non-media entity
58
125
  class BadRequestNonMediaValue < BadRequestError
59
- HTTP_CODE = 400
60
126
  @msg = 'Bad Request: $value request for a non-media entity'
61
127
  end
128
+ class BadRequestSequelAdapterError < BadRequestError
129
+ include ErrorInstance
130
+ def initialize(err)
131
+ @msg = err.inner.message
132
+ end
133
+ end
62
134
 
63
135
  # for Syntax error in Filtering
64
136
  class BadRequestFilterParseError < BadRequestError
65
- HTTP_CODE = 400
66
- @msg = 'Bad Request: Syntax error in Filter'
137
+ @msg = 'Bad Request: Syntax error in $filter'
138
+ end
139
+
140
+ # for invalid attribute in path
141
+ class BadRequestInvalidAttribPath < BadRequestError
142
+ include ErrorInstance
143
+ def initialize(model, attr)
144
+ @msg = "Bad Request: the attribute #{attr} is invalid for entityset #{model.entity_set_name}"
145
+ end
146
+ end
147
+
148
+ # for invalid attribute in $expand param
149
+ class BadRequestExpandInvalidPath < BadRequestError
150
+ include ErrorInstance
151
+ def initialize(model, path)
152
+ @msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
153
+ end
67
154
  end
155
+ # for invalid properti(es) in $select param
156
+ class BadRequestSelectInvalidProps < BadRequestError
157
+ include ErrorInstance
158
+ def initialize(model, iprops)
159
+ @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}")
160
+ end
161
+ end
162
+
163
+ # for Syntax error in $orderby param
164
+ class BadRequestOrderParseError < BadRequestError
165
+ @msg = 'Bad Request: Syntax error in $orderby'
166
+ end
167
+
68
168
  # for $inlinecount error
69
169
  class BadRequestInlineCountParamError < BadRequestError
70
- HTTP_CODE = 400
71
170
  @msg = 'Bad Request: wrong $inlinecount parameter'
72
171
  end
172
+
73
173
  # http not found
74
174
  class ErrorNotFound
75
- extend Error
175
+ extend ErrorClass
76
176
  HTTP_CODE = 404
77
177
  @msg = 'The requested ressource was not found'
78
178
  end
179
+
180
+ # http not found segment
181
+ class ErrorNotFoundSegment < ErrorNotFound
182
+ include ErrorInstance
183
+
184
+ def initialize(segment)
185
+ @msg = "The requested ressource segment #{segment} was not found"
186
+ end
187
+ end
188
+
79
189
  # Transition error (Safrano specific)
80
- class ServerTransitionError
81
- extend Error
82
- HTTP_CODE = 500
190
+ class ServerTransitionError < ServerError
83
191
  @msg = 'Server error: Segment could not be parsed'
84
192
  end
85
- # generic http 500 server err
86
- class ServerError
87
- extend Error
88
- HTTP_CODE = 500
89
- @msg = 'Server error'
90
- end
193
+
91
194
  # not implemented (Safrano specific)
92
195
  class NotImplementedError
93
- extend Error
196
+ extend ErrorClass
94
197
  HTTP_CODE = 501
95
198
  end
96
- # batch not implemented (Safrano specific)
97
- class BatchNotImplementedError
98
- extend Error
199
+
200
+ # version not implemented (Safrano specific)
201
+ class VersionNotImplementedError
202
+ extend ErrorClass
99
203
  HTTP_CODE = 501
204
+ @msg = 'The requested OData version is not yet supported'
205
+ end
206
+ # batch not implemented (Safrano specific)
207
+ class BatchNotImplementedError < NotImplementedError
100
208
  @msg = 'Not implemented: OData batch'
101
209
  end
210
+
102
211
  # error in filter parsing (Safrano specific)
103
212
  class FilterParseError < BadRequestError
104
- extend Error
105
- HTTP_CODE = 400
213
+ extend ErrorClass
214
+ end
215
+
216
+ class FilterFunctionNotImplementedError < BadRequestError
217
+ extend ErrorClass
218
+ include ErrorInstance
219
+ @msg = 'the requested $filter function is Not implemented'
220
+ def initialize(xmsg)
221
+ @msg = xmsg
222
+ end
223
+ end
224
+ class FilterUnknownFunctionError < BadRequestError
225
+ include ErrorInstance
226
+ def initialize(badfuncname)
227
+ @msg = "Bad request: unknown function #{badfuncname} in $filter"
228
+ end
229
+ end
230
+ class FilterParseErrorWrongColumnName < BadRequestError
231
+ extend ErrorClass
232
+ @msg = 'Bad request: invalid property name in $filter'
233
+ end
234
+ class FilterParseWrappedError < BadRequestError
235
+ include ErrorInstance
236
+ def initialize(exception)
237
+ @msg = exception.to_s
238
+ end
239
+ end
240
+ class ServiceOperationParameterMissing < BadRequestError
241
+ include ErrorInstance
242
+ def initialize(missing:, sopname:)
243
+ @msg = "Bad request: Parameter(s) missing for for service operation #{sopname} : #{missing.join(', ')}"
244
+ end
245
+ end
246
+
247
+ class ServiceOperationParameterError < BadRequestError
248
+ include ErrorInstance
249
+ def initialize(type:, value:, param:, sopname:)
250
+ @type = type
251
+ @value = value
252
+ @param = param
253
+ @sopname = sopname
254
+ end
255
+
256
+ def msg
257
+ "Bad request: Parameter #{@param} with value '#{@value}' cannot be converted to type #{@type} for service operation #{@sopname}"
258
+ end
106
259
  end
107
260
  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,69 @@
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
+ # Qualit (qualified lits) are words separated by /
61
+ # path/path/path/attrib
62
+ class Qualit < Literal
63
+ end
64
+
65
+ # Quoted Strings
66
+ class QString < Leave
67
+ end
68
+ end
69
+ end