safrano 0.3.3 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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