safrano 0.4.0 → 0.4.5

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