safrano 0.2.0 → 0.3.0

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.
@@ -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