safrano 0.4.2 → 0.4.3

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