safrano 0.4.2 → 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.
@@ -129,9 +129,27 @@ module OData
129
129
  HTTP_CODE = 501
130
130
  @msg = 'Not implemented: OData batch'
131
131
  end
132
+
132
133
  # error in filter parsing (Safrano specific)
133
134
  class FilterParseError < BadRequestError
134
135
  extend ErrorClass
135
136
  HTTP_CODE = 400
136
137
  end
138
+
139
+ class FilterFunctionNotImplementedError < BadRequestError
140
+ extend ErrorClass
141
+ include ErrorInstance
142
+ @msg = 'the requested $filter function is Not implemented'
143
+ HTTP_CODE = 400
144
+ def initialize(exception)
145
+ @msg = exception.to_s
146
+ end
147
+ end
148
+ class FilterInvalidFunctionError < BadRequestError
149
+ include ErrorInstance
150
+ HTTP_CODE = 400
151
+ def initialize(exception)
152
+ @msg = exception.to_s
153
+ end
154
+ end
137
155
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OData
4
+ module Filter
5
+ # Base class for Leaves, Trees, RootTrees etc
6
+ class Node
7
+ end
8
+
9
+ # Leaves are Nodes with a parent but no children
10
+ class Leave < Node
11
+ end
12
+
13
+ # RootTrees have childrens but no parent
14
+ class RootTree < Node
15
+ end
16
+
17
+ # Tree's have Parent and children
18
+ class Tree < RootTree
19
+ end
20
+
21
+ # For functions... should have a single child---> the argument list
22
+ class FuncTree < Tree
23
+ end
24
+
25
+ # Indentity Func to use as "parent" func of parenthesis expressions
26
+ # --> allow to handle generically parenthesis always as argument of
27
+ # some function
28
+ class IdentityFuncTree < FuncTree
29
+ end
30
+
31
+ # unary op eg. NOT
32
+ class UnopTree < Tree
33
+ end
34
+
35
+ # Bin ops
36
+ class BinopTree < Tree
37
+ end
38
+
39
+ class BinopBool < BinopTree
40
+ end
41
+
42
+ class BinopArithm < BinopTree
43
+ end
44
+
45
+ # Arguments or lists
46
+ class ArgTree < Tree
47
+ end
48
+
49
+ # Numbers (floating point, ints, dec)
50
+ class FPNumber < Leave
51
+ end
52
+
53
+ # Literals are unquoted words without /
54
+ class Literal < Leave
55
+ end
56
+
57
+ # Qualit (qualified lits) are words separated by /
58
+ # path/path/path/attrib
59
+ class Qualit < Literal
60
+ end
61
+
62
+ # Quoted Strings
63
+ class QString < Leave
64
+ end
65
+ end
66
+ end
@@ -1,3 +1,5 @@
1
+ require_relative '../error'
2
+
1
3
  module OData
2
4
  class SequelAdapterError < StandardError
3
5
  attr_reader :inner
@@ -5,7 +7,22 @@ module OData
5
7
  @inner = err
6
8
  end
7
9
  end
10
+
11
+ # exception to OData error bridge
12
+ module ErrorBridge
13
+ # return an odata error object wrapping the exception
14
+ # the odata error object should respond to odata_get for output
15
+ def odata_error
16
+ self.class.const_get('ODATA_ERROR_KLASS').new(self)
17
+ end
18
+ end
19
+
8
20
  module Filter
21
+ class FunctionNotImplemented < StandardError
22
+ ODATA_ERROR_KLASS = OData::FilterFunctionNotImplementedError
23
+ include ::OData::ErrorBridge
24
+ end
25
+
9
26
  class Parser
10
27
  # Parser errors
11
28
  class Error < StandardError
@@ -35,6 +52,16 @@ module OData
35
52
  class ErrorWrongColumnName < StandardError
36
53
  end
37
54
 
55
+ # attempt to add a child to a Leave
56
+ class ErrorLeaveChild < StandardError
57
+ end
58
+
59
+ # invalid function error (literal attach to IdentityFuncTree)
60
+ class ErrorInvalidFunction < StandardError
61
+ ODATA_ERROR_KLASS = OData::FilterInvalidFunctionError
62
+ include ::OData::ErrorBridge
63
+ end
64
+
38
65
  # Invalid function arity
39
66
  class ErrorInvalidArity < Error
40
67
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './token.rb'
2
4
  require_relative './tree.rb'
3
5
  require_relative './error.rb'
@@ -1,13 +1,17 @@
1
- require_relative './tree.rb'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base.rb'
4
+ require_relative './sequel_function_adapter.rb'
5
+
2
6
  module OData
3
7
  module Filter
4
8
  # Base class for Leaves, Trees, RootTrees etc
5
- class Node
6
- end
9
+ # class Node
10
+ # end
7
11
 
8
12
  # Leaves are Nodes with a parent but no children
9
- class Leave < Node
10
- end
13
+ # class Leave < Node
14
+ # end
11
15
 
12
16
  # RootTrees have childrens but no parent
13
17
  class RootTree
@@ -26,6 +30,8 @@ module OData
26
30
  end
27
31
 
28
32
  # For functions... should have a single child---> the argument list
33
+ # note: Adapter specific function helpers like year() or substringof_sig2()
34
+ # need to be mixed in on startup (eg. on publish finalize)
29
35
  class FuncTree < Tree
30
36
  def leuqes(jh)
31
37
  case @value
@@ -48,18 +54,7 @@ module OData
48
54
  Sequel.like(args[1].leuqes(jh),
49
55
  args[0].leuqes_substringof_sig1(jh))
50
56
  elsif args[1].is_a?(QString)
51
- # substringof(name, '__Route du Rhum__') -->
52
- # '__Route du Rhum__' contains name as a substring
53
- # TODO... check if the database supports instr (how?)
54
- # othewise use substr(postgresql) or whatevr?
55
- instr_substr_func = if Sequel::Model.db.adapter_scheme == :postgres
56
- Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
57
- else
58
- Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
59
- end
60
-
61
- Sequel::SQL::BooleanExpression.new(:>, instr_substr_func, 0)
62
-
57
+ substringof_sig2(jh) # adapter specific
63
58
  else
64
59
  # TODO... actually not supported?
65
60
  raise OData::Filter::Parser::ErrorFunctionArgumentType
@@ -75,6 +70,26 @@ module OData
75
70
  Sequel.function(:upper, args.first.leuqes(jh))
76
71
  when :tolower
77
72
  Sequel.function(:lower, args.first.leuqes(jh))
73
+ # all datetime funcs are adapter specific (because sqlite does not have extract)
74
+ when :year
75
+ year(jh)
76
+ when :month
77
+ month(jh)
78
+ when :second
79
+ second(jh)
80
+ when :minute
81
+ minute(jh)
82
+ when :hour
83
+ hour(jh)
84
+ when :day
85
+ day(jh)
86
+ # math functions
87
+ when :round
88
+ Sequel.function(:round, args.first.leuqes(jh))
89
+ when :floor
90
+ floor(jh)
91
+ when :ceiling
92
+ ceiling(jh)
78
93
  else
79
94
  raise OData::FilterParseError
80
95
  end
@@ -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
@@ -245,7 +253,7 @@ module OData
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
@@ -307,7 +315,7 @@ module OData
307
315
  end
308
316
 
309
317
  # Numbers (floating point, ints, dec)
310
- class FPNumber < Leave
318
+ class FPNumber
311
319
  def accept?(tok, typ)
312
320
  case typ
313
321
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
@@ -324,7 +332,7 @@ module OData
324
332
  end
325
333
 
326
334
  # Literals are unquoted words without /
327
- class Literal < Leave
335
+ class Literal
328
336
  def accept?(tok, typ)
329
337
  case typ
330
338
  when :Delimiter, :Separator, :BinopBool, :BinopArithm
@@ -337,11 +345,23 @@ module OData
337
345
  def edm_type
338
346
  :any
339
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
340
360
  end
341
361
 
342
362
  # Qualit (qualified lits) are words separated by /
343
363
  # path/path/path/attrib
344
- class Qualit < Literal
364
+ class Qualit
345
365
  REGEXP = /((?:\w+\/)+)(\w+)/.freeze
346
366
  attr_reader :path
347
367
  attr_reader :attrib
@@ -356,7 +376,7 @@ module OData
356
376
  end
357
377
 
358
378
  # Quoted Strings
359
- class QString < Leave
379
+ class QString
360
380
  DBL_QO = "''".freeze
361
381
  SI_QO = "'".freeze
362
382
  def initialize(val)