safrano 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,208 @@
1
+ require_relative './tree.rb'
2
+ module OData
3
+ module Filter
4
+ # Base class for Leaves, Trees, RootTrees etc
5
+ class Node
6
+ end
7
+
8
+ # Leaves are Nodes with a parent but no children
9
+ class Leave < Node
10
+ end
11
+
12
+ # RootTrees have childrens but no parent
13
+ class RootTree
14
+ def apply_to_dataset(dtcx, jh)
15
+ filtexpr = @children.first.leuqes(jh)
16
+ dtcx = jh.dataset.where(filtexpr).select_all(jh.start_model.table_name)
17
+ end
18
+
19
+ def sequel_expr(jh)
20
+ @children.first.leuqes(jh)
21
+ end
22
+ end
23
+
24
+ # Tree's have Parent and children
25
+ class Tree < RootTree
26
+ end
27
+
28
+ # For functions... should have a single child---> the argument list
29
+ class FuncTree < Tree
30
+ def leuqes(jh)
31
+ case @value
32
+ when :startswith
33
+ Sequel.like(args[0].leuqes(jh),
34
+ args[1].leuqes_starts_like(jh))
35
+ when :endswith
36
+ Sequel.like(args[0].leuqes(jh),
37
+ args[1].leuqes_ends_like(jh))
38
+ when :substringof
39
+ # there are two possible signatures
40
+ if (args[0].is_a?(QString) && args[1].is_a?(Literal))
41
+ # substringof('Rhum', name) -->
42
+ # name contains substr 'Rhum'
43
+ Sequel.like(args[1].leuqes(jh),
44
+ args[0].leuqes_substringof_like(jh))
45
+ # according to the spec it should be like that?
46
+ # elsif args[1].is_a? QString
47
+ ## substringof(name, '__Route du Rhum__') -->
48
+ ## '__Route du Rhum__' contains name as a substring
49
+ # Sequel.like(args[1].leuqes_substringof_like(dtcx),
50
+ # args[0].leuqes(dtcx) )
51
+ # or like that?
52
+ # elsif args[1].is_a? QString
53
+ # # substringof(name, 'Route du Rhum') -->
54
+ # # name contains substr 'Rhum' ??
55
+ # Sequel.like(args[0].leuqes(dtcx) ,
56
+ # args[1].leuqes_substringof_like(dtcx) )
57
+ elsif (args[0].is_a?(Literal) && args[1].is_a?(Literal))
58
+ Sequel.like(args[1].leuqes(jh),
59
+ args[0].leuqes_substringof_like(jh))
60
+ else
61
+ # TODO... actually not supported?
62
+ raise OData::Filter::Parser::ErrorFunctionArgumentType
63
+ end
64
+ when :concat
65
+ Sequel.join([args[0].leuqes(jh),
66
+ args[1].leuqes(jh)])
67
+ when :length
68
+ Sequel.char_length(args.first.leuqes(jh))
69
+ when :trim
70
+ Sequel.trim(args.first.leuqes(jh))
71
+ when :toupper
72
+ Sequel.function(:upper, args.first.leuqes(jh))
73
+ when :tolower
74
+ Sequel.function(:lower, args.first.leuqes(jh))
75
+ else
76
+ raise OData::FilterParseError
77
+ end
78
+ end
79
+ end
80
+
81
+ # Indentity Func to use as "parent" func of parenthesis expressions
82
+ # --> allow to handle generically parenthesis always as argument of
83
+ # some function
84
+ class IdentityFuncTree < FuncTree
85
+ def leuqes(jh)
86
+ args.first.leuqes(jh)
87
+ end
88
+ end
89
+
90
+ # unary op eg. NOT
91
+ class UnopTree < Tree
92
+ def leuqes(jh)
93
+ case @value
94
+ when :not
95
+ Sequel.~(@children.first.leuqes(jh))
96
+ else
97
+ raise OData::FilterParseError
98
+ end
99
+ end
100
+ end
101
+
102
+ # Bin ops
103
+ class BinopTree < Tree
104
+ def leuqes(jh)
105
+ leuqes_op = case @value
106
+ when :eq
107
+ :'='
108
+ when :ne
109
+ :'!='
110
+ when :le
111
+ :<=
112
+ when :ge
113
+ :'>='
114
+ when :lt
115
+ :<
116
+ when :gt
117
+ :>
118
+ when :or
119
+ :OR
120
+ when :and
121
+ :AND
122
+ else
123
+ raise OData::FilterParseError
124
+ end
125
+
126
+ Sequel::SQL::BooleanExpression.new(leuqes_op,
127
+ @children[0].leuqes(jh),
128
+ @children[1].leuqes(jh))
129
+ end
130
+ end
131
+
132
+ # Arguments or lists
133
+ class ArgTree
134
+ end
135
+
136
+ # Numbers (floating point, ints, dec)
137
+ class FPNumber
138
+ def leuqes(_jh)
139
+ Sequel.lit(@value)
140
+ end
141
+
142
+ def leuqes_starts_like(_jh)
143
+ "#{@value.to_s}%"
144
+ end
145
+
146
+ def leuqes_ends_like(_jh)
147
+ "%#{@value.to_s}"
148
+ end
149
+
150
+ def leuqes_substringof_like(_jh)
151
+ "%#{@value.to_s}%"
152
+ end
153
+ end
154
+
155
+ # Literals are unquoted words
156
+ class Literal
157
+ def leuqes(jh)
158
+ if jh.start_model.db_schema.has_key?(@value.to_sym)
159
+ Sequel[jh.start_model.table_name][@value.to_sym]
160
+ else
161
+ raise OData::Filter::Parser::ErrorWrongColumnName
162
+ end
163
+ end
164
+
165
+ # non stantard extensions to support things like
166
+ # substringof(Rhum, name) ????
167
+ def leuqes_starts_like(_jh)
168
+ "#{@value}%"
169
+ end
170
+
171
+ def leuqes_ends_like(_jh)
172
+ "%#{@value}"
173
+ end
174
+
175
+ def leuqes_substringof_like(_jh)
176
+ "%#{@value}%"
177
+ end
178
+ end
179
+
180
+ # Qualit (qualified lits) are words separated by /
181
+ class Qualit
182
+ def leuqes(jh)
183
+ jh.add(@path)
184
+ talias = jh.start_model.get_alias_sym(@path)
185
+ Sequel[talias][@attrib.to_sym]
186
+ end
187
+ end
188
+
189
+ # Quoted Strings
190
+ class QString
191
+ def leuqes(_jh)
192
+ @value
193
+ end
194
+
195
+ def leuqes_starts_like(_jh)
196
+ "#{@value}%"
197
+ end
198
+
199
+ def leuqes_ends_like(_jh)
200
+ "%#{@value}"
201
+ end
202
+
203
+ def leuqes_substringof_like(_jh)
204
+ "%#{@value}%"
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,59 @@
1
+ # top level OData namespace
2
+ module OData
3
+ module Filter
4
+ class Parser
5
+ # Input tokenizer
6
+ module Token
7
+ 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
+ BINOPSRGX = '[eE][qQ]|[LlgGNn][eETt]|and|AND|or|OR'.freeze
12
+ NOTRGX = 'not|NOT'.freeze
13
+ FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'.freeze
14
+ QUALITRGX = '\w+(?:\/\w+)+'.freeze
15
+ RGX = /(#{FUNCRGX})|([\(\),])|(#{BINOPSRGX})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
16
+ def each_typed_token(inp)
17
+ typ = nil
18
+
19
+ inp.scan(RGX) do |groups|
20
+ idx = nil
21
+ found = nil
22
+ groups.each_with_index do |tok, i|
23
+ if (found = tok)
24
+ idx = i
25
+ break
26
+ end
27
+ end
28
+ typ = case idx
29
+ when 0
30
+ :FuncTree
31
+ when 1
32
+ case found
33
+ when '(', ')'
34
+ :Delimiter
35
+ when ','
36
+ :Separator
37
+ end
38
+ when 2
39
+ :BinopTree
40
+ when 3
41
+ :UnopTree
42
+ when 4
43
+ :QString
44
+ when 5
45
+ :FPNumber
46
+ when 6
47
+ :Qualit
48
+ when 7
49
+ :Literal
50
+ when 8
51
+ :unmatchedQuote
52
+ end
53
+ yield found, typ
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,368 @@
1
+ require_relative './error.rb'
2
+
3
+ module OData
4
+ module Filter
5
+ # Base class for Leaves, Trees, RootTrees etc
6
+ class Node
7
+ attr_reader :value
8
+ def initialize(val, &block)
9
+ @value = val
10
+ instance_eval(&block) if block_given?
11
+ end
12
+
13
+ def ==(other)
14
+ @value == other.value
15
+ end
16
+ end
17
+
18
+ # Leaves are Nodes with a parent but no children
19
+ class Leave < Node
20
+ attr_accessor :parent
21
+ def accept?(tok, typ)
22
+ [false, Parser::ErrorInvalidToken(tok, typ)]
23
+ end
24
+
25
+ def check_types; end
26
+ end
27
+
28
+ # RootTrees have childrens but no parent
29
+ class RootTree < Node
30
+ attr_reader :children
31
+ attr_accessor :state
32
+ def initialize(val: :root, &block)
33
+ @children = []
34
+ super(val, &block)
35
+ end
36
+
37
+ def attach(child)
38
+ child.parent = self
39
+ @children << child
40
+ end
41
+
42
+ def detach(child)
43
+ child.parent = nil
44
+ @children.delete(child)
45
+ end
46
+
47
+ def ==(other)
48
+ super(other) && (@children == other.children)
49
+ end
50
+
51
+ def update_state(tok, typ) end
52
+
53
+ def accept?(tok, typ)
54
+ case typ
55
+ when :Literal, :Qualit, :QString, :FuncTree, :ArgTree, :UnopTree, :FPNumber
56
+ true
57
+ when :Delimiter
58
+ if tok == '('
59
+ true
60
+ else
61
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
62
+ end
63
+ else
64
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
65
+ end
66
+ end
67
+
68
+ def check_types
69
+ @children.each(&:check_types)
70
+ end
71
+ end
72
+
73
+ # Tree's have Parent and children
74
+ class Tree < RootTree
75
+ attr_accessor :parent
76
+
77
+ def initialize(val)
78
+ super(val: val)
79
+ end
80
+ end
81
+
82
+ # For functions... should have a single child---> the argument list
83
+ class FuncTree < Tree
84
+ def initialize(val)
85
+ super(val.downcase.to_sym)
86
+ end
87
+
88
+ def args
89
+ @children.first.children
90
+ end
91
+
92
+ def arity_full?(cursize)
93
+ cursize >= max_arity
94
+ end
95
+
96
+ def max_arity
97
+ case @value
98
+ when :replace
99
+ 3
100
+ when :concat, :substringof, :substring, :endswith, :startswith
101
+ 2
102
+ else
103
+ 1
104
+ end
105
+ end
106
+
107
+ def edm_type
108
+ case @value
109
+ when :concat, :substring
110
+ :string
111
+ when :length
112
+ :int
113
+ when :substringof, :endswith, :startswith
114
+ :bool
115
+ else
116
+ :any
117
+ end
118
+ end
119
+
120
+ def accept?(tok, typ)
121
+ case typ
122
+ when :BinopTree
123
+ true
124
+ else
125
+ super(tok, typ)
126
+ end
127
+ end
128
+
129
+ def check_types
130
+ case @value
131
+ when :length
132
+ argtyp = args.first.edm_type
133
+ if (argtyp != :any) && (argtyp != :string)
134
+ raise Parser::ErrorInvalidArgumentType.new(self,
135
+ expected: :string,
136
+ actual: argtyp)
137
+ end
138
+ end
139
+ super
140
+ end
141
+ end
142
+
143
+ # Indentity Func to use as "parent" func of parenthesis expressions
144
+ # --> allow to handle generically parenthesis always as argument of
145
+ # some function
146
+ class IdentityFuncTree < FuncTree
147
+ def initialize
148
+ super(:__indentity)
149
+ end
150
+
151
+ # we can have parenthesis with one expression inside everywhere
152
+ # only in FuncTree this is redefined for the function's arity
153
+ def arity_full?(cursize)
154
+ cursize >= 1
155
+ end
156
+
157
+ def edm_type
158
+ @children.first.edm_type
159
+ end
160
+ end
161
+
162
+ # unary op eg. NOT
163
+ class UnopTree < Tree
164
+ def initialize(val)
165
+ super(val.downcase.to_sym)
166
+ end
167
+
168
+ # reference:
169
+ # OData v4 par 5.1.1.9 Operator Precedence
170
+ def precedence
171
+ case @value
172
+ when :not
173
+ 7
174
+ else
175
+ 999
176
+ end
177
+ end
178
+
179
+ def edm_type
180
+ case @value
181
+ when :not
182
+ :bool
183
+ else
184
+ :any
185
+ end
186
+ end
187
+ end
188
+
189
+ # Bin ops
190
+ class BinopTree < Tree
191
+ def initialize(val)
192
+ @state = :open
193
+ super(val.downcase.to_sym)
194
+ end
195
+
196
+ # reference:
197
+ # OData v4 par 5.1.1.9 Operator Precedence
198
+ def precedence
199
+ case @value
200
+ when :or
201
+ 1
202
+ when :and
203
+ 2
204
+ when :eq, :ne
205
+ 3
206
+ when :gt, :ge, :lt, :le
207
+ 4
208
+ else
209
+ 999
210
+ end
211
+ end
212
+
213
+ def update_state(_tok, typ)
214
+ case typ
215
+ when :Literal, :Qualit, :QString, :FuncTree, :BinopTree, :UnopTree, :FPNumber
216
+ @state = :closed
217
+ end
218
+ end
219
+
220
+ def edm_type
221
+ case @value
222
+ when :or, :and, :eq, :ne, :gt, :ge, :lt, :le
223
+ :bool
224
+ else
225
+ :any
226
+ end
227
+ end
228
+ end
229
+
230
+ # Arguments or lists
231
+ class ArgTree < Tree
232
+ attr_reader :type
233
+ def initialize(val)
234
+ @type = :expression
235
+ @state = :open
236
+ super
237
+ end
238
+
239
+ def update_state(_tok, typ)
240
+ case typ
241
+ when :Delimiter
242
+ @state = :closed
243
+ when :Separator
244
+ @state = :sep
245
+ when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
246
+ @state = :val
247
+ end
248
+ end
249
+
250
+ def accept?(tok, typ)
251
+ case typ
252
+ when :Delimiter
253
+ if @value == '(' && tok == ')' && @state != :closed
254
+ if @parent.arity_full?(@children.size)
255
+ true
256
+ else
257
+ [false, Parser::ErrorInvalidArity.new(tok, typ, self)]
258
+ end
259
+ else
260
+ [false, Parser::ErrorUnmatchedClose.new(tok, typ, self)]
261
+ end
262
+ when :Separator
263
+ if @value == '(' && tok == ',' && @state == :val
264
+ true
265
+ else
266
+ if (@state == :sep)
267
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
268
+ else
269
+ true
270
+ end
271
+ end
272
+ when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
273
+ if (@state == :open) || (@state == :sep)
274
+ if @parent.arity_full?(@children.size)
275
+ [false, Parser::ErrorInvalidArity.new(tok, typ, self)]
276
+ else
277
+ true
278
+ end
279
+ else
280
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
281
+ end
282
+ when :BinopTree
283
+ true
284
+ else
285
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
286
+ end
287
+ end
288
+
289
+ def ==(other)
290
+ super(other) && @type == other.type && @state == other.state
291
+ end
292
+ end
293
+
294
+ # Numbers (floating point, ints, dec)
295
+ class FPNumber < Leave
296
+ def accept?(tok, typ)
297
+ case typ
298
+ when :Delimiter, :Separator, :BinopTree
299
+ true
300
+ else
301
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
302
+ end
303
+ end
304
+
305
+ # TODO
306
+ def edm_type
307
+ :number
308
+ end
309
+ end
310
+
311
+ # Literals are unquoted words without /
312
+ class Literal < Leave
313
+ def accept?(tok, typ)
314
+ case typ
315
+ when :Delimiter, :Separator, :BinopTree
316
+ true
317
+ else
318
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
319
+ end
320
+ end
321
+
322
+ def edm_type
323
+ :any
324
+ end
325
+ end
326
+
327
+ # Qualit (qualified lits) are words separated by /
328
+ # path/path/path/attrib
329
+ class Qualit < Literal
330
+ REGEXP = /((?:\w+\/)+)(\w+)/.freeze
331
+ attr_reader :path
332
+ attr_reader :attrib
333
+ def initialize(val)
334
+ super(val)
335
+ # split into path + attrib
336
+ if (md = REGEXP.match(val))
337
+ @path = md[1].chomp('/')
338
+ @attrib = md[2]
339
+ else
340
+ raise Parser::Error.new(self, Qualit)
341
+ end
342
+ end
343
+ end
344
+
345
+ # Quoted Strings
346
+ class QString < Leave
347
+ DoubleQuote = "''".freeze
348
+ SingleQuote = "'".freeze
349
+ def initialize(val)
350
+ # unescape double quotes
351
+ super(val.gsub(DoubleQuote, SingleQuote))
352
+ end
353
+
354
+ def accept?(tok, typ)
355
+ case typ
356
+ when :Delimiter, :Separator, :BinopTree
357
+ true
358
+ else
359
+ [false, Parser::ErrorInvalidToken.new(tok, typ, self)]
360
+ end
361
+ end
362
+
363
+ def edm_type
364
+ :string
365
+ end
366
+ end
367
+ end
368
+ end