safrano 0.4.3 → 0.4.4

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 +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  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 +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  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 +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 +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  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 +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -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,15 @@ 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
+ QSTRINGRGX = /'((?:[^']|(?:'{2}))*)'/.freeze
14
+ BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'
15
+ BINOARITHM = '[aA][dD][dD]|[sS][uU][bB]|[mM][uU][lL]|[dD][iI][vV]|[mM][oO][dD]'
16
+ NOTRGX = 'not|NOT'
17
+ FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'
18
+ QUALITRGX = '\w+(?:\/\w+)+'
19
+ RGX = /(#{FUNCRGX})|([\(\),])|(#{BINOBOOL})\s+|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
20
+
21
21
  def each_typed_token(inp)
22
22
  typ = nil
23
23
 
@@ -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, :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
@@ -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
@@ -272,40 +302,43 @@ module OData
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
330
  when :Literal, :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,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'complex_type'
4
+ require_relative 'edm/primitive_types'
5
+ module Safrano
6
+ def Safrano.FunctionImport(name)
7
+ FunctionImport::Function.new(name)
8
+ end
9
+
10
+ module FunctionImport
11
+ class Function
12
+ @allowed_transitions = [Safrano::TransitionEnd]
13
+ attr_reader :name
14
+ attr_reader :proc
15
+
16
+ def initialize(name)
17
+ @name = name
18
+ @http_method = 'GET'
19
+ end
20
+
21
+ def allowed_transitions
22
+ [Safrano::TransitionEnd]
23
+ end
24
+
25
+ def input(**parmtypes)
26
+ @input = {}
27
+ parmtypes.each { |k, t|
28
+ @input[k] = case t.name
29
+ when 'Integer'
30
+ Safrano::Edm::Edm::Int32
31
+ when 'String'
32
+ Safrano::Edm::Edm::String
33
+ when 'Float'
34
+ Safrano::Edm::Edm::Double
35
+ when 'DateTime'
36
+ Safrano::Edm::Edm::DateTime
37
+ else
38
+ t
39
+ end
40
+ }
41
+ self
42
+ end
43
+
44
+ def return(klassmod, &proc)
45
+ raise('Please provide a code block') unless block_given?
46
+
47
+ @returning = if klassmod.respond_to? :return_as_instance_descriptor
48
+ klassmod.return_as_instance_descriptor
49
+ else
50
+ # if it's neither a ComplexType nor a Model-Entity
51
+ # --> assume it is a Primitive
52
+ ResultAsPrimitiveType.new(klassmod)
53
+ end
54
+ @proc = proc
55
+ self
56
+ end
57
+
58
+ def return_collection(klassmod, &proc)
59
+ raise('Please provide a code block') unless block_given?
60
+
61
+ @returning = if klassmod.respond_to? :return_as_collection_descriptor
62
+ klassmod.return_as_collection_descriptor
63
+ else
64
+ # if it's neither a ComplexType nor a Modle-Entity
65
+ # --> assume it is a Primitive
66
+ ResultAsPrimitiveTypeColl.new(klassmod)
67
+ end
68
+ @proc = proc
69
+ self
70
+ end
71
+ # def initialize_params
72
+ # @uparms = UrlParameters4Func.new(@model, @params)
73
+ # end
74
+
75
+ def check_missing_params
76
+ # do we have all parameters provided ? use Set difference to check
77
+ pkeys = @params.keys.map(&:to_sym).to_set
78
+ unless (idiff = @input.keys.to_set - pkeys).empty?
79
+
80
+ Safrano::ServiceOperationParameterMissing.new(
81
+ missing: idiff.to_a,
82
+ sopname: @name
83
+ )
84
+ else
85
+ Contract::OK
86
+ end
87
+ end
88
+
89
+ def check_url_func_params
90
+ @funcparams = {}
91
+ return nil unless @input # anything to check ?
92
+
93
+ # do we have all parameters provided ?
94
+ check_missing_params.tap_error do |error| return error end
95
+ # ==> all params were provided
96
+
97
+ # now we shall check the content and type of the parameters
98
+ @input.each { |ksym, typ|
99
+ typ.convert_from_urlparam(v = @params[ksym.to_s])
100
+ .tap_valid do |retval|
101
+ @funcparams[ksym] = retval
102
+ end
103
+ .tap_error do |error|
104
+ # return is really needed here, or we end up returning nil below
105
+ return parameter_convertion_error(ksym, typ, v)
106
+ end
107
+ }
108
+ nil
109
+ end
110
+
111
+ def parameter_convertion_error(param, type, val)
112
+ Safrano::ServiceOperationParameterError.new(type: type,
113
+ value: val,
114
+ param: param,
115
+ sopname: @name)
116
+ end
117
+
118
+ def add_metadata_rexml(ec)
119
+ ## https://services.odata.org/V2/OData/Safrano.svc/$metadata
120
+ # <FunctionImport Name="GetProductsByRating" EntitySet="Products" ReturnType="Collection(ODataDemo.Product)" m:HttpMethod="GET">
121
+ # <Parameter Name="rating" Type="Edm.Int32" Mode="In"/>
122
+ # </FunctionImport>
123
+ funky = ec.add_element('FunctionImport',
124
+ 'Name' => @name.to_s,
125
+ # EntitySet= @entity_set ,
126
+ 'ReturnType' => @returning.type_metadata,
127
+ 'm:HttpMethod' => @http_method)
128
+ @input.each { |iname, type|
129
+ funky.add_element('Parameter',
130
+ 'Name' => iname.to_s,
131
+ 'Type' => type.type_name,
132
+ 'Mode' => 'In')
133
+ } if @input
134
+ funky
135
+ end
136
+
137
+ def with_validated_get(req)
138
+ # initialize_params
139
+ return yield unless (@error = check_url_func_params)
140
+
141
+ @error.odata_get(req) if @error
142
+ end
143
+
144
+ def to_odata_json(req)
145
+ result = @proc.call(**@funcparams)
146
+ @returning.to_odata_json(result, req)
147
+ end
148
+
149
+ def odata_get_output(req)
150
+ [200, EMPTY_HASH, [to_odata_json(req)]]
151
+ end
152
+
153
+ def odata_get(req)
154
+ @params = req.params
155
+
156
+ with_validated_get(req) do
157
+ odata_get_output(req)
158
+ end
159
+ end
160
+
161
+ def transition_end(_match_result)
162
+ [nil, :end]
163
+ end
164
+ end
165
+ end
166
+ end