safrano 0.3.3 → 0.4.3

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odata/attribute.rb +9 -8
  3. data/lib/odata/batch.rb +8 -8
  4. data/lib/odata/collection.rb +239 -92
  5. data/lib/odata/collection_filter.rb +40 -9
  6. data/lib/odata/collection_media.rb +159 -28
  7. data/lib/odata/collection_order.rb +46 -36
  8. data/lib/odata/common_logger.rb +37 -12
  9. data/lib/odata/entity.rb +188 -99
  10. data/lib/odata/error.rb +60 -12
  11. data/lib/odata/expand.rb +123 -0
  12. data/lib/odata/filter/base.rb +66 -0
  13. data/lib/odata/filter/error.rb +33 -0
  14. data/lib/odata/filter/parse.rb +6 -12
  15. data/lib/odata/filter/sequel.rb +42 -29
  16. data/lib/odata/filter/sequel_function_adapter.rb +147 -0
  17. data/lib/odata/filter/token.rb +5 -1
  18. data/lib/odata/filter/tree.rb +45 -29
  19. data/lib/odata/navigation_attribute.rb +60 -27
  20. data/lib/odata/relations.rb +2 -2
  21. data/lib/odata/select.rb +42 -0
  22. data/lib/odata/url_parameters.rb +51 -36
  23. data/lib/odata/walker.rb +6 -6
  24. data/lib/safrano.rb +23 -13
  25. data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
  26. data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
  27. data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
  28. data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
  29. data/lib/{request.rb → safrano/request.rb} +8 -14
  30. data/lib/{response.rb → safrano/response.rb} +1 -2
  31. data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
  32. data/lib/{service.rb → safrano/service.rb} +162 -131
  33. data/lib/safrano/version.rb +3 -0
  34. data/lib/sequel/plugins/join_by_paths.rb +11 -10
  35. metadata +33 -16
  36. data/lib/version.rb +0 -4
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './tree.rb'
4
+ require_relative './sequel.rb'
5
+
6
+ module OData
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
+
15
+ substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
16
+
17
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
18
+ end
19
+ # %d day of month: 00
20
+ # %f fractional seconds: SS.SSS
21
+ # %H hour: 00-24
22
+ # %j day of year: 001-366
23
+ # %J Julian day number
24
+ # %m month: 01-12
25
+ # %M minute: 00-59
26
+ # %s seconds since 1970-01-01
27
+ # %S seconds: 00-59
28
+ # %w day of week 0-6 with Sunday==0
29
+ # %W week of year: 00-53
30
+ # %Y year: 0000-9999
31
+ # %% %
32
+
33
+ # sqlite does not have extract but recommends to use strftime
34
+ def year(jh)
35
+ Sequel.function(:strftime, '%Y', args.first.leuqes(jh)).cast(:integer)
36
+ end
37
+
38
+ def month(jh)
39
+ Sequel.function(:strftime, '%m', args.first.leuqes(jh)).cast(:integer)
40
+ end
41
+
42
+ def second(jh)
43
+ Sequel.function(:strftime, '%S', args.first.leuqes(jh)).cast(:integer)
44
+ end
45
+
46
+ def minute(jh)
47
+ Sequel.function(:strftime, '%M', args.first.leuqes(jh)).cast(:integer)
48
+ end
49
+
50
+ def hour(jh)
51
+ Sequel.function(:strftime, '%H', args.first.leuqes(jh)).cast(:integer)
52
+ end
53
+
54
+ def day(jh)
55
+ Sequel.function(:strftime, '%d', args.first.leuqes(jh)).cast(:integer)
56
+ end
57
+
58
+ def floor(jh)
59
+ raise OData::Filter::FunctionNotImplemented, "$filter function 'floor' is not implemented in sqlite adapter"
60
+ end
61
+
62
+ def ceiling(jh)
63
+ raise OData::Filter::FunctionNotImplemented, "$filter function 'ceiling' is not implemented in sqlite adapter"
64
+ end
65
+ end
66
+ # re-useable module with math floor/ceil functions for those adapters having these SQL funcs
67
+ module MathFloorCeilFuncTree
68
+ def floor(jh)
69
+ Sequel.function(:floor, args.first.leuqes(jh))
70
+ end
71
+
72
+ def ceiling(jh)
73
+ Sequel.function(:ceil, args.first.leuqes(jh))
74
+ end
75
+ end
76
+
77
+ # re-useable module with Datetime functions with extract()
78
+ module DateTimeFuncTreeExtract
79
+ def year(jh)
80
+ args.first.leuqes(jh).extract(:year)
81
+ end
82
+
83
+ def year(jh)
84
+ args.first.leuqes(jh).extract(:year)
85
+ end
86
+
87
+ def month(jh)
88
+ args.first.leuqes(jh).extract(:month)
89
+ end
90
+
91
+ def second(jh)
92
+ args.first.leuqes(jh).extract(:second)
93
+ end
94
+
95
+ def minute(jh)
96
+ args.first.leuqes(jh).extract(:minute)
97
+ end
98
+
99
+ def hour(jh)
100
+ args.first.leuqes(jh).extract(:hour)
101
+ end
102
+
103
+ def day(jh)
104
+ args.first.leuqes(jh).extract(:day)
105
+ end
106
+ end
107
+
108
+ # postgresql adapter specific function handler
109
+ module FuncTreePostgres
110
+ def substringof_sig2(jh)
111
+ # substringof(name, '__Route du Rhum__') -->
112
+ # '__Route du Rhum__' contains name as a substring
113
+ # postgres does not know instr() but has strpos
114
+ substr_func = Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
115
+
116
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
117
+ end
118
+
119
+ # postgres uses extract()
120
+ include DateTimeFuncTreeExtract
121
+
122
+ # postgres has floor/ceil funcs
123
+ include MathFloorCeilFuncTree
124
+ end
125
+
126
+ # default adapter function handler for all others... try to use the most common version
127
+ # :substring --> instr because here is seems Postgres is special
128
+ # datetime funcs --> exctract, here sqlite is special(uses format)
129
+ # note: we dont test this, provided as an example/template, might work eg for mysql
130
+ module FuncTreeDefault
131
+ def substringof_sig2(jh)
132
+ # substringof(name, '__Route du Rhum__') -->
133
+ # '__Route du Rhum__' contains name as a substring
134
+ # instr() seems to be the most common substring func
135
+ substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
136
+
137
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
138
+ end
139
+
140
+ # XYZ uses extract() ?
141
+ include DateTimeFuncTreeExtract
142
+
143
+ # ... assume SQL
144
+ include MathFloorCeilFuncTree
145
+ end
146
+ end
147
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # top level OData namespace
2
4
  module OData
3
5
  module Filter
@@ -5,7 +7,9 @@ module OData
5
7
  # Input tokenizer
6
8
  module Token
7
9
  FUNCNAMES = %w[concat substringof endswith startswith length indexof
8
- replace substring trim toupper tolower].freeze
10
+ replace substring trim toupper tolower
11
+ day hour minute month second year
12
+ round floor ceiling].freeze
9
13
  FUNCRGX = FUNCNAMES.join('|').freeze
10
14
  QSTRINGRGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
11
15
  BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'.freeze
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base.rb'
1
4
  require_relative './error.rb'
2
5
 
3
6
  module OData
@@ -16,17 +19,22 @@ module OData
16
19
  end
17
20
 
18
21
  # Leaves are Nodes with a parent but no children
19
- class Leave < Node
22
+ class Leave
20
23
  attr_accessor :parent
21
24
  def accept?(tok, typ)
22
25
  [false, Parser::ErrorInvalidToken(tok, typ)]
23
26
  end
24
27
 
25
28
  def check_types; end
29
+
30
+ def attach(child)
31
+ # TODO better reporting of error infos
32
+ raise ErrorLeaveChild
33
+ end
26
34
  end
27
35
 
28
36
  # RootTrees have childrens but no parent
29
- class RootTree < Node
37
+ class RootTree
30
38
  attr_reader :children
31
39
  attr_accessor :state
32
40
  def initialize(val: :root, &block)
@@ -71,7 +79,7 @@ module OData
71
79
  end
72
80
 
73
81
  # Tree's have Parent and children
74
- class Tree < RootTree
82
+ class Tree
75
83
  attr_accessor :parent
76
84
 
77
85
  def initialize(val)
@@ -80,7 +88,7 @@ module OData
80
88
  end
81
89
 
82
90
  # For functions... should have a single child---> the argument list
83
- class FuncTree < Tree
91
+ class FuncTree
84
92
  def initialize(val)
85
93
  super(val.downcase.to_sym)
86
94
  end
@@ -143,7 +151,7 @@ module OData
143
151
  # Indentity Func to use as "parent" func of parenthesis expressions
144
152
  # --> allow to handle generically parenthesis always as argument of
145
153
  # some function
146
- class IdentityFuncTree < FuncTree
154
+ class IdentityFuncTree
147
155
  def initialize
148
156
  super(:__indentity)
149
157
  end
@@ -160,7 +168,7 @@ module OData
160
168
  end
161
169
 
162
170
  # unary op eg. NOT
163
- class UnopTree < Tree
171
+ class UnopTree
164
172
  def initialize(val)
165
173
  super(val.downcase.to_sym)
166
174
  end
@@ -187,7 +195,7 @@ module OData
187
195
  end
188
196
 
189
197
  # Bin ops
190
- class BinopTree < Tree
198
+ class BinopTree
191
199
  def initialize(val)
192
200
  @state = :open
193
201
  super(val.downcase.to_sym)
@@ -201,7 +209,7 @@ module OData
201
209
  end
202
210
  end
203
211
 
204
- class BinopBool < BinopTree
212
+ class BinopBool
205
213
  # reference:
206
214
  # OData v4 par 5.1.1.9 Operator Precedence
207
215
  def precedence
@@ -224,7 +232,7 @@ module OData
224
232
  end
225
233
  end
226
234
 
227
- class BinopArithm < BinopTree
235
+ class BinopArithm
228
236
  # reference:
229
237
  # OData v4 par 5.1.1.9 Operator Precedence
230
238
  def precedence
@@ -238,14 +246,14 @@ module OData
238
246
  end
239
247
  end
240
248
 
241
- # TODO different num types?
249
+ # TODO: different num types?
242
250
  def edm_type
243
251
  :any
244
252
  end
245
253
  end
246
254
 
247
255
  # Arguments or lists
248
- class ArgTree < Tree
256
+ class ArgTree
249
257
  attr_reader :type
250
258
  def initialize(val)
251
259
  @type = :expression
@@ -279,12 +287,10 @@ module OData
279
287
  when :Separator
280
288
  if @value == '(' && tok == ',' && @state == :val
281
289
  true
290
+ elsif @state == :sep
291
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
282
292
  else
283
- if (@state == :sep)
284
- [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
285
- else
286
- true
287
- end
293
+ true
288
294
  end
289
295
  when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
290
296
  if (@state == :open) || (@state == :sep)
@@ -309,7 +315,7 @@ module OData
309
315
  end
310
316
 
311
317
  # Numbers (floating point, ints, dec)
312
- class FPNumber < Leave
318
+ class FPNumber
313
319
  def accept?(tok, typ)
314
320
  case typ
315
321
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
@@ -326,7 +332,7 @@ module OData
326
332
  end
327
333
 
328
334
  # Literals are unquoted words without /
329
- class Literal < Leave
335
+ class Literal
330
336
  def accept?(tok, typ)
331
337
  case typ
332
338
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
@@ -339,33 +345,43 @@ module OData
339
345
  def edm_type
340
346
  :any
341
347
  end
348
+
349
+ # error, Literal are leaves
350
+ # when the child is a IdentityFuncTree then this looks like
351
+ # an attempt to use a unknown function, eg. ceil(Total)
352
+ # instead of ceiling(Total)
353
+ def attach(child)
354
+ if child.kind_of? OData::Filter::IdentityFuncTree
355
+ raise Parser::ErrorInvalidFunction.new("Error in $filter expr.: invalid function #{self.value}")
356
+ else
357
+ super
358
+ end
359
+ end
342
360
  end
343
361
 
344
362
  # Qualit (qualified lits) are words separated by /
345
363
  # path/path/path/attrib
346
- class Qualit < Literal
364
+ class Qualit
347
365
  REGEXP = /((?:\w+\/)+)(\w+)/.freeze
348
366
  attr_reader :path
349
367
  attr_reader :attrib
350
368
  def initialize(val)
351
369
  super(val)
352
370
  # 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
371
+ raise Parser::Error.new(self, Qualit) unless (md = REGEXP.match(val))
372
+
373
+ @path = md[1].chomp('/')
374
+ @attrib = md[2]
359
375
  end
360
376
  end
361
377
 
362
378
  # Quoted Strings
363
- class QString < Leave
364
- DoubleQuote = "''".freeze
365
- SingleQuote = "'".freeze
379
+ class QString
380
+ DBL_QO = "''".freeze
381
+ SI_QO = "'".freeze
366
382
  def initialize(val)
367
383
  # unescape double quotes
368
- super(val.gsub(DoubleQuote, SingleQuote))
384
+ super(val.gsub(DBL_QO, SI_QO))
369
385
  end
370
386
 
371
387
  def accept?(tok, typ)
@@ -1,11 +1,29 @@
1
1
  require 'json'
2
- require_relative '../safrano_core.rb'
2
+ require_relative '../safrano/core.rb'
3
3
  require_relative './entity.rb'
4
4
 
5
5
  module OData
6
+ # remove the relation between entity and parent by clearing
7
+ # the FK field(s) (if allowed)
8
+ def self.remove_nav_relation(assoc, parent)
9
+ return unless assoc
10
+
11
+ return unless assoc[:type] == :many_to_one
12
+
13
+ # removes/clear the FK values in parent
14
+ # thus deleting the "link" between the entity and the parent
15
+ # Note: This is called if we have to delete the child--> can only be
16
+ # done after removing the FK in parent (if allowed!)
17
+ lks = [assoc[:key]].flatten
18
+ lks.each do |lk|
19
+ parent.set(lk => nil)
20
+ parent.save(transaction: false)
21
+ end
22
+ end
23
+
6
24
  # link newly created entities(child) to an existing parent
7
25
  # by following the association_reflection rules
8
- def OData.create_nav_relation(child, assoc, parent)
26
+ def self.create_nav_relation(child, assoc, parent)
9
27
  return unless assoc
10
28
 
11
29
  # Note: this coding shares some bits from our sequel/plugins/join_by_paths,
@@ -24,16 +42,16 @@ module OData
24
42
  lks = [leftm.primary_key].flatten
25
43
  rks = [assoc[:key]].flatten
26
44
  join_cond = rks.zip(lks).to_h
27
- join_cond.each { |rk, lk|
45
+ join_cond.each do |rk, lk|
28
46
  if child.values[rk] # FK in new entity from payload not nil, only check consistency
29
47
  # with the parent - id(s)
30
- if (child.values[rk] != parent.pk_hash[lk]) # error...
31
- # TODO
32
- end
48
+ # if (child.values[rk] != parent.pk_hash[lk]) # error...
49
+ # TODO
50
+ # end
33
51
  else # we can set the FK value, thus creating the "link"
34
52
  child.set(rk => parent.pk_hash[lk])
35
53
  end
36
- }
54
+ end
37
55
  when :many_to_one
38
56
  # sets the FK values in parent to corresponding related child key-values
39
57
  # thus creating the "link" between the new entity and the parent
@@ -42,31 +60,37 @@ module OData
42
60
  lks = [assoc[:key]].flatten
43
61
  rks = [child.class.primary_key].flatten
44
62
  join_cond = rks.zip(lks).to_h
45
- join_cond.each { |rk, lk|
63
+ join_cond.each do |rk, lk|
46
64
  if parent.values[lk] # FK in parent not nil, only check consistency
47
65
  # with the child - id(s)
48
- if (parent.values[lk] != child.pk_hash[rk]) # error...
49
- # TODO
50
- end
66
+ # if parent.values[lk] != child.pk_hash[rk] # error...
67
+ # TODO
68
+ # end
51
69
  else # we can set the FK value, thus creating the "link"
52
70
  parent.set(lk => child.pk_hash[rk])
53
71
  end
54
- }
72
+ end
73
+ end
74
+ end
75
+
76
+ module EntityBase
77
+ module NavigationInfo
78
+ attr_reader :nav_parent
79
+ attr_reader :navattr_reflection
80
+ attr_reader :nav_name
81
+ def set_relation_info(parent, name)
82
+ @nav_parent = parent
83
+ @nav_name = name
84
+ @navattr_reflection = parent.class.association_reflections[name.to_sym]
85
+ @nav_klass = @navattr_reflection[:class_name].constantize
86
+ end
55
87
  end
56
88
  end
57
89
 
58
90
  # Represents a named but nil-valued navigation-attribute of an Entity
59
91
  # (usually resulting from a NULL FK db value)
60
92
  class NilNavigationAttribute
61
- attr_reader :name
62
- attr_reader :parent
63
- def initialize(parent, name)
64
- @parent = parent
65
- @name = name
66
- @navattr_reflection = parent.class.association_reflections[name.to_sym]
67
- @klass = @navattr_reflection[:class_name].constantize
68
- end
69
-
93
+ include EntityBase::NavigationInfo
70
94
  def odata_get(req)
71
95
  if req.walker.media_value
72
96
  OData::ErrorNotFound.odata_get
@@ -80,23 +104,30 @@ module OData
80
104
  # create the nav. entity
81
105
  def odata_post(req)
82
106
  # delegate to the class method
83
- @klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
107
+ @nav_klass.odata_create_entity_and_relation(req,
108
+ @navattr_reflection,
109
+ @nav_parent)
84
110
  end
85
111
 
86
112
  # create the nav. entity
87
113
  def odata_put(req)
114
+ # if req.walker.raw_value
88
115
  # delegate to the class method
89
- @klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
116
+ @nav_klass.odata_create_entity_and_relation(req,
117
+ @navattr_reflection,
118
+ @nav_parent)
119
+ # else
120
+ # end
90
121
  end
91
122
 
92
123
  # empty output as OData json (v2)
93
124
  def to_odata_json(*)
94
- { 'd' => {} }.to_json
125
+ { 'd' => EMPTY_HASH }.to_json
95
126
  end
96
127
 
97
128
  # for testing purpose (assert_equal ...)
98
129
  def ==(other)
99
- (@parent == other.parent) && (@name == other.name)
130
+ (@nav_parent == other.nav_parent) && (@nav_name == other.nav_name)
100
131
  end
101
132
 
102
133
  # methods related to transitions to next state (cf. walker)
@@ -109,9 +140,11 @@ module OData
109
140
  [self, :end_with_value]
110
141
  end
111
142
 
143
+ ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
144
+ Safrano::TransitionValue].freeze
145
+
112
146
  def allowed_transitions
113
- [Safrano::TransitionEnd,
114
- Safrano::TransitionValue]
147
+ ALLOWED_TRANSITIONS
115
148
  end
116
149
  end
117
150
  include Transitions