safrano 0.4.3 → 0.5.1

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 +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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # top level OData namespace
4
- module OData
3
+ module Safrano
5
4
  module Filter
6
5
  class Parser
7
6
  # Input tokenizer
@@ -10,14 +9,16 @@ module OData
10
9
  replace substring trim toupper tolower
11
10
  day hour minute month second year
12
11
  round floor ceiling].freeze
13
- FUNCRGX = FUNCNAMES.join('|').freeze
14
- QSTRINGRGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
15
- BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'.freeze
16
- BINOARITHM = '[aA][dD][dD]|[sS][uU][bB]|[mM][uU][lL]|[dD][iI][vV]|[mM][oO][dD]'.freeze
17
- NOTRGX = 'not|NOT'.freeze
18
- FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'.freeze
19
- QUALITRGX = '\w+(?:\/\w+)+'.freeze
20
- RGX = /(#{FUNCRGX})|([\(\),])|(#{BINOBOOL})|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
12
+ FUNCRGX = FUNCNAMES.join('|')
13
+ NULLRGX = 'null|NULL|Null'
14
+ QSTRINGRGX = /'((?:[^']|(?:'{2}))*)'/.freeze
15
+ BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'
16
+ BINOARITHM = '[aA][dD][dD]|[sS][uU][bB]|[mM][uU][lL]|[dD][iI][vV]|[mM][oO][dD]'
17
+ NOTRGX = 'not|NOT|Not'
18
+ FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'
19
+ QUALITRGX = '\w+(?:\/\w+)+'
20
+ RGX = /(#{FUNCRGX})|(#{NULLRGX})|([\(\),])|(#{BINOBOOL})\s+|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
21
+
21
22
  def each_typed_token(inp)
22
23
  typ = nil
23
24
 
@@ -34,27 +35,29 @@ module OData
34
35
  when 0
35
36
  :FuncTree
36
37
  when 1
38
+ :NullLiteral
39
+ when 2
37
40
  case found
38
41
  when '(', ')'
39
42
  :Delimiter
40
43
  when ','
41
44
  :Separator
42
45
  end
43
- when 2
44
- :BinopBool
45
46
  when 3
46
- :BinopArithm
47
+ :BinopBool
47
48
  when 4
48
- :UnopTree
49
+ :BinopArithm
49
50
  when 5
50
- :QString
51
+ :UnopTree
51
52
  when 6
52
- :FPNumber
53
+ :QString
53
54
  when 7
54
- :Qualit
55
+ :FPNumber
55
56
  when 8
56
- :Literal
57
+ :Qualit
57
58
  when 9
59
+ :Literal
60
+ when 10
58
61
  :unmatchedQuote
59
62
  end
60
63
  yield found, typ
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './base.rb'
4
- require_relative './error.rb'
3
+ require_relative './base'
4
+ require_relative './error'
5
5
 
6
- module OData
6
+ module Safrano
7
7
  module Filter
8
8
  # Base class for Leaves, Trees, RootTrees etc
9
9
  class Node
10
10
  attr_reader :value
11
+
11
12
  def initialize(val, &block)
12
13
  @value = val
13
14
  instance_eval(&block) if block_given?
@@ -21,15 +22,16 @@ module OData
21
22
  # Leaves are Nodes with a parent but no children
22
23
  class Leave
23
24
  attr_accessor :parent
25
+
26
+ # nil is considered as accepted, otherwise non-nil=the error
24
27
  def accept?(tok, typ)
25
- [false, Parser::ErrorInvalidToken(tok, typ)]
28
+ Parser::ErrorInvalidToken(tok, typ)
26
29
  end
27
30
 
28
31
  def check_types; end
29
32
 
30
- def attach(child)
31
- # TODO better reporting of error infos
32
- raise ErrorLeaveChild
33
+ def attach(_child)
34
+ Safrano::Filter::Parser::ErrorLeaveChild
33
35
  end
34
36
  end
35
37
 
@@ -37,6 +39,7 @@ module OData
37
39
  class RootTree
38
40
  attr_reader :children
39
41
  attr_accessor :state
42
+
40
43
  def initialize(val: :root, &block)
41
44
  @children = []
42
45
  super(val, &block)
@@ -45,6 +48,7 @@ module OData
45
48
  def attach(child)
46
49
  child.parent = self
47
50
  @children << child
51
+ Contract::OK
48
52
  end
49
53
 
50
54
  def detach(child)
@@ -58,23 +62,27 @@ module OData
58
62
 
59
63
  def update_state(tok, typ) end
60
64
 
65
+ # nil is considered as accepted, otherwise non-nil=the error
61
66
  def accept?(tok, typ)
62
67
  case typ
63
- when :Literal, :Qualit, :QString, :FuncTree, :ArgTree, :UnopTree, :FPNumber
64
- true
68
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :ArgTree,
69
+ :UnopTree, :FPNumber
70
+ nil
65
71
  when :Delimiter
66
72
  if tok == '('
67
- true
73
+ nil
68
74
  else
69
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
75
+ Parser::ErrorInvalidToken.new(tok, typ, self)
70
76
  end
71
77
  else
72
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
78
+ Parser::ErrorInvalidToken.new(tok, typ, self)
73
79
  end
74
80
  end
75
81
 
76
82
  def check_types
77
- @children.each(&:check_types)
83
+ err = nil
84
+ @children.find { |c| (err = c.check_types) }
85
+ err
78
86
  end
79
87
  end
80
88
 
@@ -125,10 +133,11 @@ module OData
125
133
  end
126
134
  end
127
135
 
136
+ # nil is considered as accepted, otherwise non-nil=the error
128
137
  def accept?(tok, typ)
129
138
  case typ
130
139
  when :BinopBool, :BinopArithm
131
- true
140
+ nil
132
141
  else
133
142
  super(tok, typ)
134
143
  end
@@ -139,9 +148,9 @@ module OData
139
148
  when :length
140
149
  argtyp = args.first.edm_type
141
150
  if (argtyp != :any) && (argtyp != :string)
142
- raise Parser::ErrorInvalidArgumentType.new(self,
143
- expected: :string,
144
- actual: argtyp)
151
+ return Parser::ErrorInvalidArgumentType.new(self,
152
+ expected: :string,
153
+ actual: argtyp)
145
154
  end
146
155
  end
147
156
  super
@@ -158,13 +167,33 @@ module OData
158
167
 
159
168
  # we can have parenthesis with one expression inside everywhere
160
169
  # only in FuncTree this is redefined for the function's arity
170
+ # Note: if you change this method, please also update arity_full_monkey?
171
+ # see below
161
172
  def arity_full?(cursize)
162
173
  cursize >= 1
163
174
  end
164
175
 
176
+ # this is for testing only.
177
+ # see 99_threadsafe_tc.rb
178
+ # there we will monkey patch arity_full? by adding some sleeping
179
+ # to easily slow down a given test-thread (while the other one runs normaly)
180
+ #
181
+ # The rule is to keep this method here exactly same as the original
182
+ # "productive" one
183
+ #
184
+ # With this trick we can test threadsafeness without touching
185
+ # "productive" code
186
+ def arity_full_monkey?(cursize)
187
+ cursize >= 1
188
+ end
189
+
165
190
  def edm_type
166
191
  @children.first.edm_type
167
192
  end
193
+
194
+ def ==(other)
195
+ @children == other.children
196
+ end
168
197
  end
169
198
 
170
199
  # unary op eg. NOT
@@ -203,7 +232,7 @@ module OData
203
232
 
204
233
  def update_state(_tok, typ)
205
234
  case typ
206
- when :Literal, :Qualit, :QString, :FuncTree, :BinopBool, :BinopArithm, :UnopTree, :FPNumber
235
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :BinopBool, :BinopArithm, :UnopTree, :FPNumber
207
236
  @state = :closed
208
237
  end
209
238
  end
@@ -255,6 +284,7 @@ module OData
255
284
  # Arguments or lists
256
285
  class ArgTree
257
286
  attr_reader :type
287
+
258
288
  def initialize(val)
259
289
  @type = :expression
260
290
  @state = :open
@@ -267,45 +297,48 @@ module OData
267
297
  @state = :closed
268
298
  when :Separator
269
299
  @state = :sep
270
- when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
300
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber
271
301
  @state = :val
272
302
  end
273
303
  end
274
304
 
305
+ # nil is considered as accepted, otherwise non-nil=the error
275
306
  def accept?(tok, typ)
276
307
  case typ
277
308
  when :Delimiter
278
309
  if @value == '(' && tok == ')' && @state != :closed
279
- if @parent.arity_full?(@children.size)
280
- true
310
+ if (@parent.class == IdentityFuncTree) or
311
+ (@parent.arity_full?(@children.size))
312
+
313
+ nil
281
314
  else
282
- [false, Parser::ErrorInvalidArity.new(tok, typ, self)]
315
+ Parser::ErrorInvalidArity.new(tok, typ, self)
283
316
  end
284
317
  else
285
- [false, Parser::ErrorUnmatchedClose.new(tok, typ, self)]
318
+ if @value == '(' && tok == '(' && @state == :open
319
+ nil
320
+ else
321
+ Parser::ErrorUnmatchedClose.new(tok, typ, self)
322
+ end
286
323
  end
287
324
  when :Separator
288
325
  if @value == '(' && tok == ',' && @state == :val
289
- true
326
+ nil
290
327
  elsif @state == :sep
291
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
292
- else
293
- true
328
+ Parser::ErrorInvalidToken.new(tok, typ, self)
294
329
  end
295
- when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
330
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber
296
331
  if (@state == :open) || (@state == :sep)
297
332
  if @parent.arity_full?(@children.size)
298
- [false, Parser::ErrorInvalidArity.new(tok, typ, self)]
299
- else
300
- true
333
+ Parser::ErrorInvalidArity.new(tok, typ, self)
301
334
  end
302
335
  else
303
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
336
+ Parser::ErrorInvalidToken.new(tok, typ, self)
304
337
  end
305
338
  when :BinopBool, :BinopArithm
306
- true
339
+ nil
307
340
  else
308
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
341
+ Parser::ErrorInvalidToken.new(tok, typ, self)
309
342
  end
310
343
  end
311
344
 
@@ -319,9 +352,9 @@ module OData
319
352
  def accept?(tok, typ)
320
353
  case typ
321
354
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
322
- true
355
+ nil
323
356
  else
324
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
357
+ Parser::ErrorInvalidToken.new(tok, typ, self)
325
358
  end
326
359
  end
327
360
 
@@ -336,9 +369,9 @@ module OData
336
369
  def accept?(tok, typ)
337
370
  case typ
338
371
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
339
- true
372
+ nil
340
373
  else
341
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
374
+ Parser::ErrorInvalidToken.new(tok, typ, self)
342
375
  end
343
376
  end
344
377
 
@@ -351,8 +384,8 @@ module OData
351
384
  # an attempt to use a unknown function, eg. ceil(Total)
352
385
  # instead of ceiling(Total)
353
386
  def attach(child)
354
- if child.kind_of? OData::Filter::IdentityFuncTree
355
- raise Parser::ErrorInvalidFunction.new("Error in $filter expr.: invalid function #{self.value}")
387
+ if child.is_a? Safrano::Filter::IdentityFuncTree
388
+ Safrano::FilterUnknownFunctionError.new(value)
356
389
  else
357
390
  super
358
391
  end
@@ -365,6 +398,7 @@ module OData
365
398
  REGEXP = /((?:\w+\/)+)(\w+)/.freeze
366
399
  attr_reader :path
367
400
  attr_reader :attrib
401
+
368
402
  def initialize(val)
369
403
  super(val)
370
404
  # split into path + attrib
@@ -377,8 +411,8 @@ module OData
377
411
 
378
412
  # Quoted Strings
379
413
  class QString
380
- DBL_QO = "''".freeze
381
- SI_QO = "'".freeze
414
+ DBL_QO = "''"
415
+ SI_QO = "'"
382
416
  def initialize(val)
383
417
  # unescape double quotes
384
418
  super(val.gsub(DBL_QO, SI_QO))
@@ -387,9 +421,9 @@ module OData
387
421
  def accept?(tok, typ)
388
422
  case typ
389
423
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
390
- true
424
+ nil
391
425
  else
392
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
426
+ Parser::ErrorInvalidToken.new(tok, typ, self)
393
427
  end
394
428
  end
395
429
 
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'complex_type'
4
+ require_relative 'edm/primitive_types'
5
+ require_relative 'transition'
6
+
7
+ module Safrano
8
+ def self.FunctionImport(name)
9
+ FunctionImport::Function.new(name)
10
+ end
11
+
12
+ module FunctionImport
13
+ class Function
14
+ @allowed_transitions = [Safrano::TransitionEnd]
15
+ attr_reader :name
16
+ attr_reader :proc
17
+
18
+ def initialize(name)
19
+ @name = name
20
+ @http_method = 'GET'
21
+ end
22
+
23
+ def allowed_transitions
24
+ [Safrano::TransitionEnd]
25
+ end
26
+
27
+ def input(**parmtypes)
28
+ @input = {}
29
+ parmtypes.each do |k, t|
30
+ @input[k] = case t.name
31
+ when 'Integer'
32
+ Safrano::Edm::Edm::Int32
33
+ when 'String'
34
+ Safrano::Edm::Edm::String
35
+ when 'Float'
36
+ Safrano::Edm::Edm::Double
37
+ when 'DateTime'
38
+ Safrano::Edm::Edm::DateTime
39
+ else
40
+ t
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ def return(klassmod, &proc)
47
+ raise('Please provide a code block') unless block_given?
48
+
49
+ @returning = if klassmod.respond_to? :return_as_instance_descriptor
50
+ klassmod.return_as_instance_descriptor
51
+ else
52
+ # if it's neither a ComplexType nor a Model-Entity
53
+ # --> assume it is a Primitive
54
+ ResultAsPrimitiveType.new(klassmod)
55
+ end
56
+ @proc = proc
57
+ self
58
+ end
59
+
60
+ def return_collection(klassmod, &proc)
61
+ raise('Please provide a code block') unless block_given?
62
+
63
+ @returning = if klassmod.respond_to? :return_as_collection_descriptor
64
+ klassmod.return_as_collection_descriptor
65
+ else
66
+ # if it's neither a ComplexType nor a Modle-Entity
67
+ # --> assume it is a Primitive
68
+ ResultAsPrimitiveTypeColl.new(klassmod)
69
+ end
70
+ @proc = proc
71
+ self
72
+ end
73
+ # def initialize_params
74
+ # @uparms = UrlParameters4Func.new(@model, @params)
75
+ # end
76
+
77
+ def check_missing_params
78
+ # do we have all parameters provided ? use Set difference to check
79
+ pkeys = @params.keys.map(&:to_sym).to_set
80
+ unless (idiff = @input.keys.to_set - pkeys).empty?
81
+
82
+ Safrano::ServiceOperationParameterMissing.new(
83
+ missing: idiff.to_a,
84
+ sopname: @name
85
+ )
86
+ else
87
+ Contract::OK
88
+ end
89
+ end
90
+
91
+ def check_url_func_params
92
+ @funcparams = {}
93
+ return nil unless @input # anything to check ?
94
+
95
+ # do we have all parameters provided ?
96
+ check_missing_params.tap_error { |error| return error }
97
+ # ==> all params were provided
98
+
99
+ # now we shall check the content and type of the parameters
100
+ @input.each do |ksym, typ|
101
+ typ.convert_from_urlparam(v = @params[ksym.to_s])
102
+ .tap_valid do |retval|
103
+ @funcparams[ksym] = retval
104
+ end
105
+ .tap_error do
106
+ # return is really needed here, or we end up returning nil below
107
+ return parameter_convertion_error(ksym, typ, v)
108
+ end
109
+ end
110
+ nil
111
+ end
112
+
113
+ def parameter_convertion_error(param, type, val)
114
+ Safrano::ServiceOperationParameterError.new(type: type,
115
+ value: val,
116
+ param: param,
117
+ sopname: @name)
118
+ end
119
+
120
+ def add_metadata_rexml(ec)
121
+ ## https://services.odata.org/V2/OData/Safrano.svc/$metadata
122
+ # <FunctionImport Name="GetProductsByRating" EntitySet="Products" ReturnType="Collection(ODataDemo.Product)" m:HttpMethod="GET">
123
+ # <Parameter Name="rating" Type="Edm.Int32" Mode="In"/>
124
+ # </FunctionImport>
125
+ funky = ec.add_element('FunctionImport',
126
+ 'Name' => @name.to_s,
127
+ # EntitySet= @entity_set ,
128
+ 'ReturnType' => @returning.type_metadata,
129
+ 'm:HttpMethod' => @http_method)
130
+ @input.each do |iname, type|
131
+ funky.add_element('Parameter',
132
+ 'Name' => iname.to_s,
133
+ 'Type' => type.type_name,
134
+ 'Mode' => 'In')
135
+ end if @input
136
+ funky
137
+ end
138
+
139
+ def with_validated_get(req)
140
+ # initialize_params
141
+ return yield unless (@error = check_url_func_params)
142
+
143
+ @error.odata_get(req) if @error
144
+ end
145
+
146
+ def to_odata_json(req)
147
+ result = @proc.call(**@funcparams)
148
+ @returning.to_odata_json(result, req)
149
+ end
150
+
151
+ def odata_get_output(req)
152
+ [200, EMPTY_HASH, [to_odata_json(req)]]
153
+ end
154
+
155
+ def odata_get(req)
156
+ @params = req.params
157
+
158
+ with_validated_get(req) do
159
+ odata_get_output(req)
160
+ end
161
+ end
162
+
163
+ def transition_end(_match_result)
164
+ Transition::RESULT_END
165
+ end
166
+ end
167
+ end
168
+ end