safrano 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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 +17 -15
  15. data/lib/odata/collection.rb +141 -500
  16. data/lib/odata/collection_filter.rb +44 -37
  17. data/lib/odata/collection_media.rb +193 -43
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +39 -12
  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 +201 -176
  23. data/lib/odata/error.rb +186 -33
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +69 -0
  26. data/lib/odata/filter/error.rb +55 -6
  27. data/lib/odata/filter/parse.rb +38 -36
  28. data/lib/odata/filter/sequel.rb +121 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +15 -11
  31. data/lib/odata/filter/tree.rb +110 -60
  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 +50 -32
  35. data/lib/odata/relations.rb +7 -7
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +18 -28
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +43 -0
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
  46. data/lib/safrano/rack_app.rb +175 -0
  47. data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
  48. data/lib/{request.rb → safrano/request.rb} +102 -50
  49. data/lib/{response.rb → safrano/response.rb} +5 -4
  50. data/lib/safrano/sequel_join_by_paths.rb +5 -0
  51. data/lib/{service.rb → safrano/service.rb} +257 -188
  52. data/lib/safrano/version.rb +5 -0
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +53 -17
  55. data/lib/rack_app.rb +0 -174
  56. data/lib/sequel_join_by_paths.rb +0 -5
  57. data/lib/version.rb +0 -4
@@ -0,0 +1,148 @@
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 function handler
9
+ module FuncTreeSqlite
10
+ def substringof_sig2(jh)
11
+ # substringof(name, '__Route du Rhum__') -->
12
+ # '__Route du Rhum__' contains name as a substring
13
+ # sqlite uses instr()
14
+ Contract.collect_result!(args[1].leuqes(jh),
15
+ args[0].leuqes(jh)) do |l1, l0|
16
+ substr_func = Sequel.function(:instr, l1, l0)
17
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
18
+ end
19
+ end
20
+ # %d day of month: 00
21
+ # %f fractional seconds: SS.SSS
22
+ # %H hour: 00-24
23
+ # %j day of year: 001-366
24
+ # %J Julian day number
25
+ # %m month: 01-12
26
+ # %M minute: 00-59
27
+ # %s seconds since 1970-01-01
28
+ # %S seconds: 00-59
29
+ # %w day of week 0-6 with Sunday==0
30
+ # %W week of year: 00-53
31
+ # %Y year: 0000-9999
32
+ # %% %
33
+
34
+ # sqlite does not have extract but recommends to use strftime
35
+ def year(lq)
36
+ Sequel.function(:strftime, '%Y', lq).cast(:integer)
37
+ end
38
+
39
+ def month(lq)
40
+ Sequel.function(:strftime, '%m', lq).cast(:integer)
41
+ end
42
+
43
+ def second(lq)
44
+ Sequel.function(:strftime, '%S', lq).cast(:integer)
45
+ end
46
+
47
+ def minute(lq)
48
+ Sequel.function(:strftime, '%M', lq).cast(:integer)
49
+ end
50
+
51
+ def hour(lq)
52
+ Sequel.function(:strftime, '%H', lq).cast(:integer)
53
+ end
54
+
55
+ def day(lq)
56
+ Sequel.function(:strftime, '%d', lq).cast(:integer)
57
+ end
58
+
59
+ def floor(_lq)
60
+ Safrano::FilterFunctionNotImplementedError.new("$filter function 'floor' is not implemented in sqlite adapter")
61
+ end
62
+
63
+ def ceiling(_lq)
64
+ Safrano::FilterFunctionNotImplementedError.new("$filter function 'ceiling' is not implemented in sqlite adapter")
65
+ end
66
+ end
67
+ # re-useable module with math floor/ceil functions for those adapters having these SQL funcs
68
+ module MathFloorCeilFuncTree
69
+ def floor(lq)
70
+ success Sequel.function(:floor, lq)
71
+ end
72
+
73
+ def ceiling(lq)
74
+ success Sequel.function(:ceil, lq)
75
+ end
76
+ end
77
+
78
+ # re-useable module with Datetime functions with extract()
79
+ module DateTimeFuncTreeExtract
80
+ def year(lq)
81
+ lq.extract(:year)
82
+ end
83
+
84
+ def month(lq)
85
+ lq.extract(:month)
86
+ end
87
+
88
+ def second(lq)
89
+ lq.extract(:second)
90
+ end
91
+
92
+ def minute(lq)
93
+ lq.extract(:minute)
94
+ end
95
+
96
+ def hour(lq)
97
+ lq.extract(:hour)
98
+ end
99
+
100
+ def day(lq)
101
+ lq.extract(:day)
102
+ end
103
+ end
104
+
105
+ # postgresql adapter specific function handler
106
+ module FuncTreePostgres
107
+ def substringof_sig2(jh)
108
+ # substringof(name, '__Route du Rhum__') -->
109
+ # '__Route du Rhum__' contains name as a substring
110
+ # postgres does not know instr() but has strpos
111
+ Contract.collect_result!(args[1].leuqes(jh),
112
+ args[0].leuqes(jh)) do |l1, l0|
113
+ substr_func = Sequel.function(:strpos, l1, l0)
114
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
115
+ end
116
+ end
117
+
118
+ # postgres uses extract()
119
+ include DateTimeFuncTreeExtract
120
+
121
+ # postgres has floor/ceil funcs
122
+ include MathFloorCeilFuncTree
123
+ end
124
+
125
+ # default adapter function handler for all others... try to use the most common version
126
+ # :substring --> instr because here is seems Postgres is special
127
+ # datetime funcs --> exctract, here sqlite is special(uses format)
128
+ # note: we dont test this, provided as an example/template, might work eg for mysql
129
+ module FuncTreeDefault
130
+ def substringof_sig2(jh)
131
+ # substringof(name, '__Route du Rhum__') -->
132
+ # '__Route du Rhum__' contains name as a substring
133
+ # instr() seems to be the most common substring func
134
+ Contract.collect_result!(args[1].leuqes(jh),
135
+ args[0].leuqes(jh)) do |l1, l0|
136
+ substr_func = Sequel.function(:instr, l1, l0)
137
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
138
+ end
139
+ end
140
+
141
+ # XYZ uses extract() ?
142
+ include DateTimeFuncTreeExtract
143
+
144
+ # ... assume SQL
145
+ include MathFloorCeilFuncTree
146
+ end
147
+ end
148
+ end
@@ -1,19 +1,23 @@
1
- # top level OData namespace
2
- module OData
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
3
4
  module Filter
4
5
  class Parser
5
6
  # Input tokenizer
6
7
  module Token
7
8
  FUNCNAMES = %w[concat substringof endswith startswith length indexof
8
- replace substring trim toupper tolower].freeze
9
- FUNCRGX = FUNCNAMES.join('|').freeze
10
- QSTRINGRGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
11
- BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'.freeze
12
- BINOARITHM = '[aA][dD][dD]|[sS][uU][bB]|[mM][uU][lL]|[dD][iI][vV]|[mM][oO][dD]'.freeze
13
- NOTRGX = 'not|NOT'.freeze
14
- FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'.freeze
15
- QUALITRGX = '\w+(?:\/\w+)+'.freeze
16
- RGX = /(#{FUNCRGX})|([\(\),])|(#{BINOBOOL})|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
9
+ replace substring trim toupper tolower
10
+ day hour minute month second year
11
+ round floor ceiling].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
+
17
21
  def each_typed_token(inp)
18
22
  typ = nil
19
23
 
@@ -1,10 +1,14 @@
1
- require_relative './error.rb'
1
+ # frozen_string_literal: true
2
2
 
3
- module OData
3
+ require_relative './base'
4
+ require_relative './error'
5
+
6
+ module Safrano
4
7
  module Filter
5
8
  # Base class for Leaves, Trees, RootTrees etc
6
9
  class Node
7
10
  attr_reader :value
11
+
8
12
  def initialize(val, &block)
9
13
  @value = val
10
14
  instance_eval(&block) if block_given?
@@ -16,19 +20,26 @@ module OData
16
20
  end
17
21
 
18
22
  # Leaves are Nodes with a parent but no children
19
- class Leave < Node
23
+ class Leave
20
24
  attr_accessor :parent
25
+
26
+ # nil is considered as accepted, otherwise non-nil=the error
21
27
  def accept?(tok, typ)
22
- [false, Parser::ErrorInvalidToken(tok, typ)]
28
+ Parser::ErrorInvalidToken(tok, typ)
23
29
  end
24
30
 
25
31
  def check_types; end
32
+
33
+ def attach(_child)
34
+ Safrano::Filter::Parser::ErrorLeaveChild
35
+ end
26
36
  end
27
37
 
28
38
  # RootTrees have childrens but no parent
29
- class RootTree < Node
39
+ class RootTree
30
40
  attr_reader :children
31
41
  attr_accessor :state
42
+
32
43
  def initialize(val: :root, &block)
33
44
  @children = []
34
45
  super(val, &block)
@@ -37,6 +48,7 @@ module OData
37
48
  def attach(child)
38
49
  child.parent = self
39
50
  @children << child
51
+ Contract::OK
40
52
  end
41
53
 
42
54
  def detach(child)
@@ -50,28 +62,32 @@ module OData
50
62
 
51
63
  def update_state(tok, typ) end
52
64
 
65
+ # nil is considered as accepted, otherwise non-nil=the error
53
66
  def accept?(tok, typ)
54
67
  case typ
55
- when :Literal, :Qualit, :QString, :FuncTree, :ArgTree, :UnopTree, :FPNumber
56
- true
68
+ when :Literal, :Qualit, :QString, :FuncTree, :ArgTree,
69
+ :UnopTree, :FPNumber
70
+ nil
57
71
  when :Delimiter
58
72
  if tok == '('
59
- true
73
+ nil
60
74
  else
61
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
75
+ Parser::ErrorInvalidToken.new(tok, typ, self)
62
76
  end
63
77
  else
64
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
78
+ Parser::ErrorInvalidToken.new(tok, typ, self)
65
79
  end
66
80
  end
67
81
 
68
82
  def check_types
69
- @children.each(&:check_types)
83
+ err = nil
84
+ @children.find { |c| (err = c.check_types) }
85
+ err
70
86
  end
71
87
  end
72
88
 
73
89
  # Tree's have Parent and children
74
- class Tree < RootTree
90
+ class Tree
75
91
  attr_accessor :parent
76
92
 
77
93
  def initialize(val)
@@ -80,7 +96,7 @@ module OData
80
96
  end
81
97
 
82
98
  # For functions... should have a single child---> the argument list
83
- class FuncTree < Tree
99
+ class FuncTree
84
100
  def initialize(val)
85
101
  super(val.downcase.to_sym)
86
102
  end
@@ -117,10 +133,11 @@ module OData
117
133
  end
118
134
  end
119
135
 
136
+ # nil is considered as accepted, otherwise non-nil=the error
120
137
  def accept?(tok, typ)
121
138
  case typ
122
139
  when :BinopBool, :BinopArithm
123
- true
140
+ nil
124
141
  else
125
142
  super(tok, typ)
126
143
  end
@@ -131,9 +148,9 @@ module OData
131
148
  when :length
132
149
  argtyp = args.first.edm_type
133
150
  if (argtyp != :any) && (argtyp != :string)
134
- raise Parser::ErrorInvalidArgumentType.new(self,
135
- expected: :string,
136
- actual: argtyp)
151
+ return Parser::ErrorInvalidArgumentType.new(self,
152
+ expected: :string,
153
+ actual: argtyp)
137
154
  end
138
155
  end
139
156
  super
@@ -143,24 +160,44 @@ module OData
143
160
  # Indentity Func to use as "parent" func of parenthesis expressions
144
161
  # --> allow to handle generically parenthesis always as argument of
145
162
  # some function
146
- class IdentityFuncTree < FuncTree
163
+ class IdentityFuncTree
147
164
  def initialize
148
165
  super(:__indentity)
149
166
  end
150
167
 
151
168
  # we can have parenthesis with one expression inside everywhere
152
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
153
172
  def arity_full?(cursize)
154
173
  cursize >= 1
155
174
  end
156
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
+
157
190
  def edm_type
158
191
  @children.first.edm_type
159
192
  end
193
+
194
+ def ==(other)
195
+ @children == other.children
196
+ end
160
197
  end
161
198
 
162
199
  # unary op eg. NOT
163
- class UnopTree < Tree
200
+ class UnopTree
164
201
  def initialize(val)
165
202
  super(val.downcase.to_sym)
166
203
  end
@@ -187,7 +224,7 @@ module OData
187
224
  end
188
225
 
189
226
  # Bin ops
190
- class BinopTree < Tree
227
+ class BinopTree
191
228
  def initialize(val)
192
229
  @state = :open
193
230
  super(val.downcase.to_sym)
@@ -201,7 +238,7 @@ module OData
201
238
  end
202
239
  end
203
240
 
204
- class BinopBool < BinopTree
241
+ class BinopBool
205
242
  # reference:
206
243
  # OData v4 par 5.1.1.9 Operator Precedence
207
244
  def precedence
@@ -224,7 +261,7 @@ module OData
224
261
  end
225
262
  end
226
263
 
227
- class BinopArithm < BinopTree
264
+ class BinopArithm
228
265
  # reference:
229
266
  # OData v4 par 5.1.1.9 Operator Precedence
230
267
  def precedence
@@ -238,15 +275,16 @@ module OData
238
275
  end
239
276
  end
240
277
 
241
- # TODO different num types?
278
+ # TODO: different num types?
242
279
  def edm_type
243
280
  :any
244
281
  end
245
282
  end
246
283
 
247
284
  # Arguments or lists
248
- class ArgTree < Tree
285
+ class ArgTree
249
286
  attr_reader :type
287
+
250
288
  def initialize(val)
251
289
  @type = :expression
252
290
  @state = :open
@@ -264,42 +302,43 @@ module OData
264
302
  end
265
303
  end
266
304
 
305
+ # nil is considered as accepted, otherwise non-nil=the error
267
306
  def accept?(tok, typ)
268
307
  case typ
269
308
  when :Delimiter
270
309
  if @value == '(' && tok == ')' && @state != :closed
271
- if @parent.arity_full?(@children.size)
272
- true
310
+ if (@parent.class == IdentityFuncTree) or
311
+ (@parent.arity_full?(@children.size))
312
+
313
+ nil
273
314
  else
274
- [false, Parser::ErrorInvalidArity.new(tok, typ, self)]
315
+ Parser::ErrorInvalidArity.new(tok, typ, self)
275
316
  end
276
317
  else
277
- [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
278
323
  end
279
324
  when :Separator
280
325
  if @value == '(' && tok == ',' && @state == :val
281
- true
282
- else
283
- if (@state == :sep)
284
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
285
- else
286
- true
287
- end
326
+ nil
327
+ elsif @state == :sep
328
+ Parser::ErrorInvalidToken.new(tok, typ, self)
288
329
  end
289
330
  when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
290
331
  if (@state == :open) || (@state == :sep)
291
332
  if @parent.arity_full?(@children.size)
292
- [false, Parser::ErrorInvalidArity.new(tok, typ, self)]
293
- else
294
- true
333
+ Parser::ErrorInvalidArity.new(tok, typ, self)
295
334
  end
296
335
  else
297
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
336
+ Parser::ErrorInvalidToken.new(tok, typ, self)
298
337
  end
299
338
  when :BinopBool, :BinopArithm
300
- true
339
+ nil
301
340
  else
302
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
341
+ Parser::ErrorInvalidToken.new(tok, typ, self)
303
342
  end
304
343
  end
305
344
 
@@ -309,13 +348,13 @@ module OData
309
348
  end
310
349
 
311
350
  # Numbers (floating point, ints, dec)
312
- class FPNumber < Leave
351
+ class FPNumber
313
352
  def accept?(tok, typ)
314
353
  case typ
315
354
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
316
- true
355
+ nil
317
356
  else
318
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
357
+ Parser::ErrorInvalidToken.new(tok, typ, self)
319
358
  end
320
359
  end
321
360
 
@@ -326,54 +365,65 @@ module OData
326
365
  end
327
366
 
328
367
  # Literals are unquoted words without /
329
- class Literal < Leave
368
+ class Literal
330
369
  def accept?(tok, typ)
331
370
  case typ
332
371
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
333
- true
372
+ nil
334
373
  else
335
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
374
+ Parser::ErrorInvalidToken.new(tok, typ, self)
336
375
  end
337
376
  end
338
377
 
339
378
  def edm_type
340
379
  :any
341
380
  end
381
+
382
+ # error, Literal are leaves
383
+ # when the child is a IdentityFuncTree then this looks like
384
+ # an attempt to use a unknown function, eg. ceil(Total)
385
+ # instead of ceiling(Total)
386
+ def attach(child)
387
+ if child.is_a? Safrano::Filter::IdentityFuncTree
388
+ Safrano::FilterUnknownFunctionError.new(value)
389
+ else
390
+ super
391
+ end
392
+ end
342
393
  end
343
394
 
344
395
  # Qualit (qualified lits) are words separated by /
345
396
  # path/path/path/attrib
346
- class Qualit < Literal
397
+ class Qualit
347
398
  REGEXP = /((?:\w+\/)+)(\w+)/.freeze
348
399
  attr_reader :path
349
400
  attr_reader :attrib
401
+
350
402
  def initialize(val)
351
403
  super(val)
352
404
  # split into path + attrib
353
- if (md = REGEXP.match(val))
354
- @path = md[1].chomp('/')
355
- @attrib = md[2]
356
- else
357
- raise Parser::Error.new(self, Qualit)
358
- end
405
+ raise Parser::Error.new(self, Qualit) unless (md = REGEXP.match(val))
406
+
407
+ @path = md[1].chomp('/')
408
+ @attrib = md[2]
359
409
  end
360
410
  end
361
411
 
362
412
  # Quoted Strings
363
- class QString < Leave
364
- DoubleQuote = "''".freeze
365
- SingleQuote = "'".freeze
413
+ class QString
414
+ DBL_QO = "''"
415
+ SI_QO = "'"
366
416
  def initialize(val)
367
417
  # unescape double quotes
368
- super(val.gsub(DoubleQuote, SingleQuote))
418
+ super(val.gsub(DBL_QO, SI_QO))
369
419
  end
370
420
 
371
421
  def accept?(tok, typ)
372
422
  case typ
373
423
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
374
- true
424
+ nil
375
425
  else
376
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
426
+ Parser::ErrorInvalidToken.new(tok, typ, self)
377
427
  end
378
428
  end
379
429