safrano 0.4.3 → 0.5.1

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