safrano 0.5.3 → 0.6.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Date/format.rb +47 -0
  3. data/lib/core_ext/DateTime/format.rb +54 -0
  4. data/lib/core_ext/Dir/iter.rb +7 -5
  5. data/lib/core_ext/Hash/transform.rb +14 -6
  6. data/lib/core_ext/Numeric/convert.rb +25 -0
  7. data/lib/core_ext/Time/format.rb +71 -0
  8. data/lib/core_ext/date.rb +5 -0
  9. data/lib/core_ext/datetime.rb +5 -0
  10. data/lib/core_ext/numeric.rb +3 -0
  11. data/lib/core_ext/time.rb +5 -0
  12. data/lib/odata/attribute.rb +8 -6
  13. data/lib/odata/batch.rb +3 -3
  14. data/lib/odata/collection.rb +9 -9
  15. data/lib/odata/collection_filter.rb +3 -1
  16. data/lib/odata/collection_media.rb +4 -27
  17. data/lib/odata/collection_order.rb +1 -1
  18. data/lib/odata/common_logger.rb +5 -27
  19. data/lib/odata/complex_type.rb +61 -67
  20. data/lib/odata/edm/primitive_types.rb +110 -42
  21. data/lib/odata/entity.rb +14 -47
  22. data/lib/odata/error.rb +7 -7
  23. data/lib/odata/expand.rb +2 -2
  24. data/lib/odata/filter/base.rb +10 -1
  25. data/lib/odata/filter/error.rb +2 -2
  26. data/lib/odata/filter/parse.rb +16 -2
  27. data/lib/odata/filter/sequel.rb +31 -4
  28. data/lib/odata/filter/sequel_datetime_adapter.rb +21 -0
  29. data/lib/odata/filter/token.rb +18 -5
  30. data/lib/odata/filter/tree.rb +83 -9
  31. data/lib/odata/function_import.rb +19 -18
  32. data/lib/odata/model_ext.rb +96 -38
  33. data/lib/odata/request/json.rb +171 -0
  34. data/lib/odata/transition.rb +13 -9
  35. data/lib/odata/url_parameters.rb +3 -3
  36. data/lib/odata/walker.rb +9 -9
  37. data/lib/safrano/multipart.rb +1 -3
  38. data/lib/safrano/rack_app.rb +2 -14
  39. data/lib/safrano/rack_builder.rb +0 -15
  40. data/lib/safrano/request.rb +3 -3
  41. data/lib/safrano/response.rb +3 -3
  42. data/lib/safrano/service.rb +43 -12
  43. data/lib/safrano/type_mapping.rb +149 -0
  44. data/lib/safrano/version.rb +1 -2
  45. data/lib/safrano.rb +3 -0
  46. metadata +54 -15
data/lib/odata/error.rb CHANGED
@@ -59,12 +59,12 @@ module Safrano
59
59
  message = (m = @msg.to_s).empty? ? to_s : m
60
60
  if req.accept?(APPJSON)
61
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}",
62
+ [http_code, EMPTY_HASH,
63
+ { 'odata.error' => { 'code' => http_code.to_s,
64
64
  'type' => to_s,
65
65
  'message' => message } }.to_json]
66
66
  else
67
- [self.http_code, CT_TEXT, message]
67
+ [http_code, CT_TEXT, message]
68
68
  end
69
69
  end
70
70
  end
@@ -82,8 +82,8 @@ module Safrano
82
82
  if req.accept?(APPJSON)
83
83
  # json is default content type so we dont need to specify it here again
84
84
  [self.class.http_code, EMPTY_HASH,
85
- { 'odata.error' => { 'code' => "#{self.class.http_code}",
86
- 'type' => "#{self.class}",
85
+ { 'odata.error' => { 'code' => self.class.http_code.to_s,
86
+ 'type' => self.class.to_s,
87
87
  'message' => message } }.to_json]
88
88
  else
89
89
  [self.class.http_code, CT_TEXT, message]
@@ -184,7 +184,7 @@ module Safrano
184
184
  class BadRequestSelectInvalidProps < BadRequestError
185
185
  include ErrorInstance
186
186
  def initialize(model, iprops)
187
- @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}")
187
+ @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}")
188
188
  end
189
189
  end
190
190
 
@@ -232,7 +232,7 @@ module Safrano
232
232
  @msg = 'The requested OData version is not yet supported'
233
233
  end
234
234
  # batch not implemented (Safrano specific)
235
- class BatchNotImplementedError < NotImplementedError
235
+ class BatchNotImplementedError < RuntimeError
236
236
  @msg = 'Not implemented: OData batch'
237
237
  end
238
238
 
data/lib/odata/expand.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'odata/error.rb'
3
+ require 'odata/error'
4
4
 
5
5
  # all dataset expanding related classes in our OData module
6
6
  # ie do eager loading
@@ -52,7 +52,7 @@ module Safrano
52
52
  # [1] --> { 1 => {} }
53
53
  DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
54
54
 
55
- NODESEP = '/'.freeze
55
+ NODESEP = '/'
56
56
 
57
57
  def initialize(exstr)
58
58
  exstr.strip!
@@ -49,10 +49,13 @@ module Safrano
49
49
  class ArgTree < Tree
50
50
  end
51
51
 
52
- # Numbers (floating point, ints, dec)
52
+ # Numbers (floating point, ints)
53
53
  class FPNumber < Leave
54
54
  end
55
55
 
56
+ class DecimalLit < Leave
57
+ end
58
+
56
59
  # Literals are unquoted words without /
57
60
  class Literal < Leave
58
61
  end
@@ -70,5 +73,11 @@ module Safrano
70
73
  # Quoted Strings
71
74
  class QString < Leave
72
75
  end
76
+
77
+ # DateTime Literals
78
+ class DateTimeLit < Leave
79
+ end
80
+ class DateTimeOffsetLit < Leave
81
+ end
73
82
  end
74
83
  end
@@ -16,7 +16,7 @@ module Safrano
16
16
  # Parser errors
17
17
 
18
18
  class Error
19
- def Error.http_code
19
+ def self.http_code
20
20
  const_get(:HTTP_CODE)
21
21
  end
22
22
  HTTP_CODE = 400
@@ -70,7 +70,7 @@ module Safrano
70
70
  include ::Safrano::ErrorInstance
71
71
  def initialize(tok, typ, cur)
72
72
  super
73
- @msg = "Bad Request: wrong number of parameters for function #{cur.parent.value.to_s} in $filter"
73
+ @msg = "Bad Request: wrong number of parameters for function #{cur.parent.value} in $filter"
74
74
  end
75
75
  end
76
76
  # Invalid separator in this context (missing parenthesis?)
@@ -174,7 +174,21 @@ module Safrano
174
174
  @cursor.update_state(tok, typ)
175
175
  grow_at_cursor(FPNumber.new(tok))
176
176
  end
177
-
177
+ when :DecimalLit
178
+ with_accepted(tok, typ) do
179
+ @cursor.update_state(tok, typ)
180
+ grow_at_cursor(DecimalLit.new(tok))
181
+ end
182
+ when :DateTimeLit
183
+ with_accepted(tok, typ) do
184
+ @cursor.update_state(tok, typ)
185
+ grow_at_cursor(DateTimeLit.new(tok))
186
+ end
187
+ when :DateTimeOffsetLit
188
+ with_accepted(tok, typ) do
189
+ @cursor.update_state(tok, typ)
190
+ grow_at_cursor(DateTimeOffsetLit.new(tok))
191
+ end
178
192
  when :unmatchedQuote
179
193
  break unmatched_quote_error(tok, typ)
180
194
 
@@ -188,7 +202,7 @@ module Safrano
188
202
  break(@error) if @error
189
203
  end
190
204
  (@error = @tree.check_types) unless @error
191
- @error ? @error : Contract.valid(@tree)
205
+ @error || Contract.valid(@tree)
192
206
  end
193
207
  end
194
208
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative './base'
4
4
  require_relative './sequel_function_adapter'
5
-
5
+ require_relative './sequel_datetime_adapter'
6
6
  module Safrano
7
7
  module Filter
8
8
  # Base class for Leaves, Trees, RootTrees etc
@@ -184,9 +184,10 @@ module Safrano
184
184
  Contract.collect_result!(@children[0].leuqes(jh),
185
185
  @children[1].leuqes(jh)) do |c0, c1|
186
186
  if c1 == NullLiteral::LEUQES
187
- if @value == :eq
187
+ case @value
188
+ when :eq
188
189
  leuqes_op = :IS
189
- elsif @value == :ne
190
+ when :ne
190
191
  leuqes_op = :'IS NOT'
191
192
  end
192
193
  end
@@ -242,6 +243,12 @@ module Safrano
242
243
  end
243
244
  end
244
245
 
246
+ class DecimalLit
247
+ def leuqes(_jh)
248
+ success Sequel.lit(@value)
249
+ end
250
+ end
251
+
245
252
  # Literals are unquoted words
246
253
  class Literal
247
254
  def leuqes(jh)
@@ -271,7 +278,7 @@ module Safrano
271
278
 
272
279
  # Null
273
280
  class NullLiteral
274
- def leuqes(jh)
281
+ def leuqes(_jh)
275
282
  # Sequel's representation of NULL
276
283
  success LEUQES
277
284
  end
@@ -304,5 +311,25 @@ module Safrano
304
311
  success "%#{@value}%"
305
312
  end
306
313
  end
314
+
315
+ # DateTime literals datetime'2017-04-15T00:00:00'
316
+ class DateTimeLit
317
+ # datetime method is defined dynamically by adapter-specific include on startup
318
+ # --> sequel_datetime_adapter.rb
319
+ def leuqes(_jh)
320
+ # success Sequel.function(:datetime, @value)
321
+ success datetime(@value)
322
+ end
323
+ end
324
+
325
+ # DateTimeOffset literals datetimeoffset'2017-04-15T00:00:00+02:00'
326
+ class DateTimeOffsetLit
327
+ # datetime method is defined dynamically by adapter-specific include on startup
328
+ # --> sequel_datetime_adapter.rb
329
+ def leuqes(_jh)
330
+ # success Sequel.function(:datetime, @value)
331
+ success datetime(@value)
332
+ end
333
+ end
307
334
  end
308
335
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './tree'
4
+ require_relative './sequel'
5
+
6
+ module Safrano
7
+ module Filter
8
+ # sqlite adapter specific DateTime handler
9
+ module DateTimeSqlite
10
+ def datetime(_val)
11
+ Sequel.function(:datetime, @value)
12
+ end
13
+ end
14
+ # non-sqlite adapter specific DateTime handler
15
+ module DateTimeDefault
16
+ def datetime(_val)
17
+ Sequel.lit("'#{@value}'")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -15,9 +15,16 @@ module Safrano
15
15
  BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'
16
16
  BINOARITHM = '[aA][dD][dD]|[sS][uU][bB]|[mM][uU][lL]|[dD][iI][vV]|[mM][oO][dD]'
17
17
  NOTRGX = 'not|NOT|Not'
18
- FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'
18
+ FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?[df]?'
19
+ DECIMALRGX = '\d+(?:\.\d+)[mM]'
19
20
  QUALITRGX = '\w+(?:\/\w+)+'
20
- RGX = /(#{FUNCRGX})|(#{NULLRGX})|([\(\),])|(#{BINOBOOL})\s+|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
21
+ # datetime'yyyy-mm-ddThh:mm[:ss[.fffffff]]' NOTE: Spaces are not allowed between datetime and quoted portion.
22
+ # datetime is case-insensitive
23
+ DATIRGX = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?:\:\d{2})?(?:\.\d{1,7})?'
24
+ DATETIMERGX = /datetime'#{DATIRGX}[zZ]?'/i.freeze
25
+ DATIOFFRGX = /datetimeoffset'#{DATIRGX}(?:[zZ]|[+-]\d{2}:\d{2})'/.freeze
26
+
27
+ RGX = /(#{FUNCRGX})|(#{NULLRGX})|([(),])|(#{BINOBOOL})\s+|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{DECIMALRGX})|(#{FPRGX})|(#{QUALITRGX})|(#{DATETIMERGX})|(#{DATIOFFRGX})|(\w+)|(')/.freeze
21
28
 
22
29
  def each_typed_token(inp)
23
30
  typ = nil
@@ -52,12 +59,18 @@ module Safrano
52
59
  when 6
53
60
  :QString
54
61
  when 7
55
- :FPNumber
62
+ :DecimalLit
56
63
  when 8
57
- :Qualit
64
+ :FPNumber
58
65
  when 9
59
- :Literal
66
+ :Qualit
60
67
  when 10
68
+ :DateTimeLit
69
+ when 11
70
+ :DateTimeOffsetLit
71
+ when 12
72
+ :Literal
73
+ when 13
61
74
  :unmatchedQuote
62
75
  end
63
76
  yield found, typ
@@ -45,6 +45,11 @@ module Safrano
45
45
  super(val, &block)
46
46
  end
47
47
 
48
+ # shortcut used for testing
49
+ def first_child_value
50
+ @children.first.value
51
+ end
52
+
48
53
  def attach(child)
49
54
  child.parent = self
50
55
  @children << child
@@ -66,7 +71,7 @@ module Safrano
66
71
  def accept?(tok, typ)
67
72
  case typ
68
73
  when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :ArgTree,
69
- :UnopTree, :FPNumber
74
+ :UnopTree, :FPNumber, :DecimalLit, :DateTimeLit, :DateTimeOffsetLit
70
75
  nil
71
76
  when :Delimiter
72
77
  if tok == '('
@@ -232,7 +237,8 @@ module Safrano
232
237
 
233
238
  def update_state(_tok, typ)
234
239
  case typ
235
- when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :BinopBool, :BinopArithm, :UnopTree, :FPNumber
240
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :BinopBool, :BinopArithm,
241
+ :UnopTree, :FPNumber, :DecimalLit, :DateTimeLit, :DateTimeOffsetLit
236
242
  @state = :closed
237
243
  end
238
244
  end
@@ -297,7 +303,8 @@ module Safrano
297
303
  @state = :closed
298
304
  when :Separator
299
305
  @state = :sep
300
- when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber
306
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber, :DecimalLit,
307
+ :DateTimeLit, :DateTimeOffsetLit
301
308
  @state = :val
302
309
  end
303
310
  end
@@ -307,8 +314,8 @@ module Safrano
307
314
  case typ
308
315
  when :Delimiter
309
316
  if @value == '(' && tok == ')' && @state != :closed
310
- if (@parent.class == IdentityFuncTree) or
311
- (@parent.arity_full?(@children.size))
317
+ if (@parent.class == IdentityFuncTree) ||
318
+ @parent.arity_full?(@children.size)
312
319
 
313
320
  nil
314
321
  else
@@ -327,11 +334,10 @@ module Safrano
327
334
  elsif @state == :sep
328
335
  Parser::ErrorInvalidToken.new(tok, typ, self)
329
336
  end
330
- when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber
337
+ when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber, :DecimalLit,
338
+ :DateTimeLit, :DateTimeOffsetLit
331
339
  if (@state == :open) || (@state == :sep)
332
- if @parent.arity_full?(@children.size)
333
- Parser::ErrorInvalidArity.new(tok, typ, self)
334
- end
340
+ Parser::ErrorInvalidArity.new(tok, typ, self) if @parent.arity_full?(@children.size)
335
341
  else
336
342
  Parser::ErrorInvalidToken.new(tok, typ, self)
337
343
  end
@@ -349,6 +355,13 @@ module Safrano
349
355
 
350
356
  # Numbers (floating point, ints, dec)
351
357
  class FPNumber
358
+ def initialize(val)
359
+ # 1.53f --> value 1.53
360
+ # 1.53d --> value 1.53
361
+ # 1.53 --> value 1.53
362
+ val[-1] =~ /[fd]/i ? super(val[0..-2]) : super(val)
363
+ end
364
+
352
365
  def accept?(tok, typ)
353
366
  case typ
354
367
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
@@ -363,7 +376,28 @@ module Safrano
363
376
  :number
364
377
  end
365
378
  end
379
+ class DecimalLit
380
+ def initialize(val)
381
+ # 1.53m --> value 1.53
382
+ # Warning, this assumes that the m|M part in the input is really not optional
383
+ # cf. DECIMALRGX in token.rb
384
+
385
+ super(val[0..-2])
386
+ end
387
+
388
+ def accept?(tok, typ)
389
+ case typ
390
+ when :Delimiter, :Separator, :BinopBool, :BinopArithm
391
+ nil
392
+ else
393
+ Parser::ErrorInvalidToken.new(tok, typ, self)
394
+ end
395
+ end
366
396
 
397
+ def edm_type
398
+ :decimal
399
+ end
400
+ end
367
401
  # Literals are unquoted words without /
368
402
  class Literal
369
403
  def accept?(tok, typ)
@@ -409,6 +443,46 @@ module Safrano
409
443
  end
410
444
  end
411
445
 
446
+ # DateTimeLit
447
+ class DateTimeLit
448
+ def initialize(val)
449
+ # datetime'2000-12-12T12:00:53' --> value 2000-12-12T12:00:53
450
+ super(val[9..-2])
451
+ end
452
+
453
+ def accept?(tok, typ)
454
+ case typ
455
+ when :Delimiter, :Separator, :BinopBool, :BinopArithm
456
+ nil
457
+ else
458
+ Parser::ErrorInvalidToken.new(tok, typ, self)
459
+ end
460
+ end
461
+
462
+ def edm_type
463
+ :datetime
464
+ end
465
+ end
466
+ # DateTimeOffsetLit
467
+ class DateTimeOffsetLit
468
+ def initialize(val)
469
+ # datetimeoffset'2000-12-12T12:00:53+02:00' --> value 2000-12-12T12:00:53+02:00
470
+ super(val[15..-2])
471
+ end
472
+
473
+ def accept?(tok, typ)
474
+ case typ
475
+ when :Delimiter, :Separator, :BinopBool, :BinopArithm
476
+ nil
477
+ else
478
+ Parser::ErrorInvalidToken.new(tok, typ, self)
479
+ end
480
+ end
481
+
482
+ def edm_type
483
+ :datetimeoffset
484
+ end
485
+ end
412
486
  # Quoted Strings
413
487
  class QString
414
488
  DBL_QO = "''"
@@ -42,13 +42,13 @@ module Safrano
42
42
  end
43
43
  self
44
44
  end
45
-
45
+
46
46
  def auto_query_parameters
47
47
  @auto_query_params = true
48
- self # chaining
48
+ self # chaining
49
49
  end
50
- alias auto_query_params auto_query_parameters
51
-
50
+ alias auto_query_params auto_query_parameters
51
+
52
52
  def return(klassmod, &proc)
53
53
  raise('Please provide a code block') unless block_given?
54
54
 
@@ -71,7 +71,7 @@ module Safrano
71
71
  else
72
72
  # if it's neither a ComplexType nor a Modle-Entity
73
73
  # --> assume it is a Primitive
74
- # ResultAsPrimitiveTypeColl.new(klassmod)
74
+ # ResultAsPrimitiveTypeColl.new(klassmod)
75
75
  ResultDefinition.asPrimitiveTypeColl(klassmod)
76
76
  end
77
77
  @proc = proc
@@ -84,14 +84,14 @@ module Safrano
84
84
  def check_missing_params
85
85
  # do we have all parameters provided ? use Set difference to check
86
86
  pkeys = @params.keys.map(&:to_sym).to_set
87
- unless (idiff = @input.keys.to_set - pkeys).empty?
87
+ if (idiff = @input.keys.to_set - pkeys).empty?
88
+ Contract::OK
89
+ else
88
90
 
89
91
  Safrano::ServiceOperationParameterMissing.new(
90
92
  missing: idiff.to_a,
91
93
  sopname: @name
92
94
  )
93
- else
94
- Contract::OK
95
95
  end
96
96
  end
97
97
 
@@ -134,15 +134,17 @@ module Safrano
134
134
  # EntitySet= @entity_set ,
135
135
  'ReturnType' => @returning.type_metadata,
136
136
  'm:HttpMethod' => @http_method)
137
- @input.each do |iname, type|
138
- funky.add_element('Parameter',
139
- 'Name' => iname.to_s,
140
- 'Type' => type.type_name,
141
- 'Mode' => 'In')
142
- end if @input
137
+ if @input
138
+ @input.each do |iname, type|
139
+ funky.add_element('Parameter',
140
+ 'Name' => iname.to_s,
141
+ 'Type' => type.type_name,
142
+ 'Mode' => 'In')
143
+ end
144
+ end
143
145
  funky
144
146
  end
145
-
147
+
146
148
  def with_transition_validated(req)
147
149
  # initialize_params
148
150
  @params = req.params
@@ -150,15 +152,14 @@ module Safrano
150
152
 
151
153
  [nil, :error, @error] if @error
152
154
  end
153
-
154
-
155
+
155
156
  def do_execute_func(req)
156
157
  with_transition_validated(req) do
157
158
  result = @proc.call(**@funcparams)
158
159
  [@returning.do_execute_func_result(result, req, @auto_query_params), :run]
159
160
  end
160
161
  end
161
-
162
+
162
163
  def transition_execute_func(_match_result)
163
164
  [self, :run_with_execute_func]
164
165
  end