safrano 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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