safrano 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  require 'odata/error.rb'
4
2
 
3
+ require_relative 'filter/parse.rb'
4
+ require_relative 'filter/sequel.rb'
5
+
5
6
  # a few helper method
6
7
  class String
7
8
  MASK_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
@@ -31,502 +32,30 @@ class String
31
32
  "'$#{cnt}'"
32
33
  end
33
34
  yield tmpstr
34
- # tmpstr2.gsub(UNMASK_RGX) do |_m|
35
- # k = Regexp.last_match(2).to_s
36
- # "'#{Regexp.last_match(1)}#{repl[k]}#{Regexp.last_match(3)}'"
37
- # end
38
35
  end
39
36
  end
40
37
 
41
38
  # filter base class and subclass in our OData namespace
42
39
  module OData
43
- # handles associations in filter arguments
44
- module FilterWithAssoc
45
- def get_assoc
46
- @assoc, @fn = @fn.split('/') if @fn.include?('/')
47
- end
48
-
49
- def get_qualified_fn(dtcx)
50
- seqtn = if dtcx.respond_to? :table_name
51
- @assoc ? @assoc.to_sym : dtcx.table_name
52
- else
53
- @assoc ? @assoc.to_sym : dtcx.model.table_name
54
- end
55
- Sequel[seqtn][@fn.to_sym]
56
- end
57
- end
58
-
59
- # base class for filtering
60
- class Filter
61
- attr_reader :assoc
62
- attr_reader :assocs
63
- QUO = /'((?:[^']|(?:\'{2}))*)'/.freeze
64
- NOTQ = /((?:[^'\s\)])*)/.freeze
65
-
66
- def initialize(matx)
67
- @matx = matx
68
- @assocs = Set.new
69
- end
70
-
71
- class << self
72
- attr_reader :regexp
73
- end
74
-
75
- # input : the filter string
76
- # returns a filter object that should have a apply_to(cx) method
77
- def self.new_by_parse(filterstr, dtset = nil)
78
- # handle complex flat expressions (flat = without relationships)
79
- # with Sequel
80
- ComplexFilter.new(filterstr, dtset)
81
- end
82
-
83
- # try to handle a simple expression like < fieldname EQ value >
84
- # or < substringof(a,b) >
85
- # NO parenthesis and no AND/OR handled here
86
- def self.new_full_match(filterstr)
87
- full_reg = Regexp.new(/\A#{regexp}\z/)
88
- # this needs to return nil, not false, or some testcase will fail
89
- return nil unless (matx = filterstr.match(full_reg))
90
-
91
- # actually it's self.new(matx)
92
- new(matx)
93
- end
94
-
95
- def apply_to_dataset(dtcx)
96
- dtcx
97
- end
98
-
99
- def apply_associations(dtcx)
100
- @assocs.each { |aj| dtcx = dtcx.association_join(aj) }
101
- dtcx
102
- end
103
- end
104
-
105
- # well...
106
- class EqualFilter < Filter
107
- include FilterWithAssoc
108
- EQL = '[eE][qQ]|[LlgGNn][eETt]'.freeze
109
-
110
- def self.qualified_regexp(paths_rx)
111
- /\s*(#{paths_rx})\s+(#{EQL})\s+(?:#{QUO}|#{NOTQ})\s*/
112
- end
113
-
114
- def initialize(matx)
115
- super(matx)
116
- @fn = matx[1]
117
- get_assoc if @fn
118
-
119
- @val = matx[3] ? matx[3].gsub("''", "'") : matx[4]
120
-
121
- @op = matx[2].downcase
122
- end
123
-
124
- # ugly hack... but working
125
- def gen_sql(dtcx)
126
- # handle ambiguous column names (joins) by qualifiying the names
127
- # seqfn = @assoc ? Sequel[@assoc.to_sym][@fn.to_sym] : @fn.to_sym
128
- # handle ambiguous column names (joins) by qualifiying the names
129
- # for the main table as well
130
- # TODO: find a better way to differentiate between the two types
131
- # of dtcx : Model or Dataset
132
-
133
- # DONE: same handling for order and substring and everywhere where
134
- # qualified fieldnames could be needed (uses attrib path regexp)
135
- x = apply_to_dataset(dtcx)
136
- # ugly ugly hack :-(
137
- y = x.unordered.sql.sub(x.unfiltered.unordered.sql + ' WHERE', '')
138
- y
139
- end
140
-
141
- def apply_op_to_dataset(dtcx, lefval, rightval)
142
- case @op
143
- when 'eq'
144
- dtcx.where(lefval => rightval)
145
- when 'ne'
146
- dtcx.exclude(lefval => rightval)
147
- when 'le'
148
- dtcx.where { lefval <= rightval }
149
- when 'ge'
150
- dtcx.where { lefval >= rightval }
151
- when 'lt'
152
- dtcx.where { lefval < rightval }
153
- when 'gt'
154
- dtcx.where { lefval > rightval }
155
- else
156
- raise OData::FilterParseError
157
- end
158
- end
159
-
160
- def apply_to_dataset(dtcx)
161
- seqfn = get_qualified_fn(dtcx)
162
- # using @val in Procs below does not work reliably. Using a local var
163
- # seems to work better
164
- argval = @val
165
-
166
- apply_op_to_dataset(dtcx, seqfn, argval)
167
- end
168
-
169
- def apply_to_object(inp)
170
- case @op
171
- when 'eq'
172
- get_value(inp) == @val
173
- when 'ne'
174
- get_value(inp) != @val
175
- else
176
- raise OData::FilterParseError
177
- end
178
- end
179
- end
180
-
181
- class Func2a_EqualFilter < EqualFilter
182
- FUNC = 'concat'.freeze
183
- attr_reader :funcname
184
-
185
- # like for concat(Name, 'xyz') EQ 'blablux'
186
- def self.qualified_regexp(pathsrx)
187
- /\s*(#{FUNC})\((#{pathsrx}),\s*(?:#{QUO})\)\s+(#{EQL})\s+(?:#{QUO})\s*/
188
- end
189
-
190
- def initialize(matx)
191
- # super(matx)
192
- @funcname = matx[1]
193
- @fn = matx[2]
194
- get_assoc if @fn
195
- @farg = matx[3].gsub("''", "'")
196
- @op = matx[4].downcase
197
-
198
- @val = matx[5].gsub("''", "'")
199
- end
200
-
201
- def apply_to_dataset(dtcx)
202
- seqfn = get_qualified_fn(dtcx)
203
- # using @val in Procs below does not work reliably. Using a local var
204
- # seems to work better
205
- argval = @val
206
- seqfunc = case @funcname
207
- when 'concat'
208
- Sequel.join([seqfn, @farg])
209
- else
210
- raise OData::FilterParseError
211
- end
212
- apply_op_to_dataset(dtcx, seqfunc, argval)
213
- end
214
- end
215
- class Func2b_EqualFilter < EqualFilter
216
- FUNC = 'concat'.freeze
217
- attr_reader :funcname
218
-
219
- # like for concat('xyz', Name) EQ 'blablux'
220
- def self.qualified_regexp(pathsrx)
221
- /\s*(#{FUNC})\((?:#{QUO}),\s*(#{pathsrx})\)\s+(#{EQL})\s+(?:#{QUO})\s*/
222
- end
223
-
224
- def initialize(matx)
225
- # super(matx)
226
- @funcname = matx[1]
227
- @fn = matx[3]
228
- get_assoc if @fn
229
- @farg = matx[2].gsub("''", "'")
230
- @op = matx[4].downcase
231
-
232
- @val = matx[5].gsub("''", "'")
233
- end
234
-
235
- def apply_to_dataset(dtcx)
236
- seqfn = get_qualified_fn(dtcx)
237
- # using @val in Procs below does not work reliably. Using a local var
238
- # seems to work better
239
- argval = @val
240
- seqfunc = case @funcname
241
- when 'concat'
242
- Sequel.join([@farg, seqfn])
243
- else
244
- raise OData::FilterParseError
245
- end
246
- apply_op_to_dataset(dtcx, seqfunc, argval)
247
- end
248
- end
249
-
250
- class Func2c_EqualFilter < EqualFilter
251
- FUNC = 'concat'.freeze
252
- attr_reader :funcname
253
-
254
- # like for concat(first_ame, last_name) EQ 'blablux'
255
- def self.qualified_regexp(pathsrx)
256
- /\s*(#{FUNC})\((#{pathsrx}),\s*(#{pathsrx})\)\s+(#{EQL})\s+(?:#{QUO})\s*/
257
- end
258
-
259
- def get_assoc
260
- @assoc, @fn = @fn.split('/') if @fn.include?('/')
261
- assoc1, @fn1 = @fn1.split('/') if @fn1.include?('/')
262
- return unless assoc1 != @assoc
263
-
264
- # TODO... handle this
265
- raise OData::ServerError
266
- end
267
-
268
- def get_qualified_fn1(dtcx)
269
- seqtn = if dtcx.respond_to? :table_name
270
- @assoc ? @assoc.to_sym : dtcx.table_name
271
- else
272
- @assoc ? @assoc.to_sym : dtcx.model.table_name
273
- end
274
- Sequel[seqtn][@fn1.to_sym]
275
- end
276
-
277
- def initialize(matx)
278
- # super(matx)
279
- @funcname = matx[1]
280
- @fn = matx[2]
281
- @fn1 = matx[3]
282
- get_assoc if @fn
283
-
284
- @op = matx[4].downcase
285
-
286
- @val = matx[5].gsub("''", "'")
287
- end
288
-
289
- def apply_to_dataset(dtcx)
290
- seqfn = get_qualified_fn(dtcx)
291
- seqfn1 = get_qualified_fn1(dtcx)
292
- # using @val in Procs below does not work reliably. Using a local var
293
- # seems to work better
294
- argval = @val
295
- seqfunc = case @funcname
296
- when 'concat'
297
- Sequel.join([seqfn, seqfn1])
298
- else
299
- raise OData::FilterParseError
300
- end
301
- apply_op_to_dataset(dtcx, seqfunc, argval)
302
- end
303
- end
304
-
305
- # equality expressions with functions having 1 parameter
306
- # like for length(name) eq 2
307
- class FuncEqualFilter < EqualFilter
308
- FUNC = 'length|trim|tolower|toupper'.freeze
309
-
310
- attr_reader :funcname
311
-
312
- def self.qualified_regexp(pathsrx)
313
- /\s*(#{FUNC})\((#{pathsrx})\)\s+(#{EQL})\s+(?:#{QUO}|#{NOTQ})\s*/
314
- end
315
-
316
- def initialize(matx)
317
- super(matx)
318
- @funcname = matx[1]
319
- @fn = matx[2]
320
- get_assoc if @fn
321
-
322
- @op = matx[3].downcase
323
-
324
- @val = matx[4] ? matx[4].gsub("''", "'") : matx[5]
325
- end
326
-
327
- def apply_to_dataset(dtcx)
328
- seqfn = get_qualified_fn(dtcx)
329
- # using @val in Procs below does not work reliably. Using a local var
330
- # seems to work better
331
- argval = @val
332
- seqfunc = case @funcname
333
- when 'length'
334
- argval = Sequel.lit(@val)
335
- Sequel.char_length(seqfn)
336
- when 'trim'
337
- Sequel.trim(seqfn)
338
- when 'toupper'
339
- Sequel.function(:upper, seqfn)
340
- when 'tolower'
341
- Sequel.function(:lower, seqfn)
342
- else
343
- raise OData::FilterParseError
344
- end
345
-
346
- apply_op_to_dataset(dtcx, seqfunc, argval)
347
- end
348
- end
349
-
350
- # keyword based
351
- class FilterWithKeyword < Filter
352
- attr_reader :keyword
353
- end
354
-
355
- # for substringof('value', field)
356
- class SubstringOfFilterSig1 < FilterWithKeyword
357
- include FilterWithAssoc
358
-
359
- def self.qualified_regexp(paths_rx)
360
- # Note: QUOTED_RGX captures the string content without
361
- # start and end quote (no need to un-quote)
362
- # NOTQ_RGX captures an not-quoted value argument
363
- # (like for numbers)
364
- /(substringof)\((?:#{QUO}|#{NOTQ}),\s*(#{paths_rx})\)/
365
- end
366
-
367
- def initialize(matx)
368
- super(matx)
369
-
370
- @fn = matx[4]
371
- get_assoc if @fn
372
- # @val = matx[2].strip_single_quote
373
- @val = matx[2] ? matx[2].gsub("''", "'") : matx[3]
374
- @keyword = matx[1]
375
- end
376
-
377
- def whcl(dtcx)
378
- # handle ambiguous column names (joins) by qualifiying the names
379
- # seqfn = @assoc ? Sequel[@assoc.to_sym][@fn.to_sym] : @fn.to_sym
380
- seqfn = get_qualified_fn(dtcx)
381
- Sequel.like(seqfn, "%#{@val}%")
382
- end
383
-
384
- def gen_sql(dtcx)
385
- sql_ = ''
386
- dt = dtcx.respond_to?(:dataset) ? dtcx.dataset : dtcx
387
- dt.literal_append(sql_, whcl(dt))
388
- sql_
389
- end
390
-
391
- def apply_to_dataset(dtcx)
392
- dtcx.where(whcl(dtcx))
393
- end
394
- end
395
- # for substringof(field,'value')
396
- class SubstringOfFilterSig2 < FilterWithKeyword
397
- include FilterWithAssoc
398
-
399
- def initialize(matx)
400
- super(matx)
401
- @fn = matx[2]
402
- get_assoc if @fn
403
- @val = matx[3]
404
- @keyword = matx[1]
405
-
406
- raise FilterParseError
407
- end
408
-
409
- def apply_to_dataset(dtcx)
410
- dtcx
411
- end
412
- end
413
-
414
- # Filter by start/end with
415
- class StartOrEndsWithFilter < FilterWithKeyword
416
- include FilterWithAssoc
417
- # DONE: better handle quotes vs not-quotes. Unblanced quotes are now handled
418
- # example : startswith(year, 190')
419
-
420
- def self.qualified_regexp(paths)
421
- /(endswith|startswith)\((#{paths}),\s*(?:#{QUO}|#{NOTQ})\s*\)/
422
- end
423
-
424
- def initialize(matx)
425
- super(matx)
426
- # DONE: the regexp should only match on known field names...
427
- # --> use qualified_regexp(attr_paths_rgx)
428
- @fn = matx[2]
429
- get_assoc if @fn
430
- # double quotes need to be unescaped
431
- @val = matx[3] ? matx[3].gsub("''", "'") : matx[4]
432
- @keyword = matx[1]
433
- end
434
-
435
- def whcl(dtcx)
436
- seqfn = get_qualified_fn(dtcx)
437
- case @keyword
438
- when 'startswith'
439
- Sequel.like(seqfn, "#{@val}%")
440
- when 'endswith'
441
- Sequel.like(seqfn, "%#{@val}")
442
- else
443
- raise FilterParseError
444
- end
445
- end
446
-
447
- def gen_sql(dtcx)
448
- sql_ = ''
449
- dt = dtcx.respond_to?(:dataset) ? dtcx.dataset : dtcx
450
- dt.literal_append(sql_, whcl(dt))
451
- # puts "in gen_sql : sql_ == " , sql_
452
- sql_
40
+ # should handle everything by parsing
41
+ class FilterByParse
42
+ def initialize(filterstr, jh)
43
+ @filterstr = filterstr.dup
44
+ @ast = OData::Filter::Parser.new(@filterstr).build
45
+ @jh = jh
453
46
  end
454
47
 
455
48
  def apply_to_dataset(dtcx)
456
- dtcx.where(whcl(dtcx))
49
+ filtexpr = @ast.sequel_expr(@jh)
50
+ dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
457
51
  end
458
- end
459
52
 
460
- FILTER_CLASSES = [EqualFilter,
461
- SubstringOfFilterSig1,
462
- SubstringOfFilterSig2,
463
- StartOrEndsWithFilter].freeze
464
- def Filter.new_by_simple_full_match(filterstr)
465
- new_filt_obj = nil
466
- OData::FILTER_CLASSES.find do |fklas|
467
- new_filt_obj = fklas.new_full_match(filterstr)
468
- end
469
- new_filt_obj
470
- end
471
-
472
- # handles some combinations
473
- class ComplexFilter < Filter
474
- # list of active Subfilter classes. The ordering is important
475
- SUBFILTERS = [EqualFilter,
476
- FuncEqualFilter,
477
- Func2a_EqualFilter,
478
- Func2b_EqualFilter,
479
- Func2c_EqualFilter,
480
- SubstringOfFilterSig1,
481
- StartOrEndsWithFilter].freeze
482
- # for counting number of AND OR's
483
- ANDORRGX = /(AND|OR\s+)/i.freeze
484
-
485
- # for detecting consecutive AND OR
486
- ANDORERRRGX = /(AND|OR)\s+[\(\)]*(AND|OR)/i.freeze
487
-
488
- def initialize(filterstr, dtset)
489
- super(nil)
490
- @filterstr = filterstr.dup
491
-
492
- @attrib_paths_url_regexp = if dtset.is_a? Sequel::Model::ClassMethods
493
- dtset.attrib_paths_url_regexp.dup
494
- else
495
- dtset.model.attrib_paths_url_regexp.dup
496
- end
497
-
498
- @active_subfs = []
499
- @osql = filterstr.dup
500
- @dt = dtset
501
-
502
- md = nil
503
- @osql.with_mask_quoted_substrings! do |s|
504
- unless (@and_or_err = ANDORERRRGX.match(s))
505
- md = ANDORRGX.match(s)
506
-
507
- @expected_count = md.nil? ? 1 : md.size
508
- SUBFILTERS.each { |klass| init_subfilter(klass) }
509
- end
510
- end
53
+ def sequel_expr
54
+ @ast.sequel_expr(@jh)
511
55
  end
512
56
 
513
57
  def parse_error?
514
- @and_or_err || (@active_subfs.size != @expected_count)
515
- end
516
-
517
- def init_subfilter(subfiltclass)
518
- rgx = subfiltclass.qualified_regexp(@attrib_paths_url_regexp)
519
- @osql.gsub!(rgx) do |_mx|
520
- subf = subfiltclass.new(Regexp.last_match)
521
- @active_subfs << subf
522
- @assocs.add(subf.assoc.to_sym) if subf.assoc
523
- subf.gen_sql(@dt)
524
- end
525
- end
526
-
527
- def apply_to_dataset(dtcx)
528
- # new Sequel 5.6 requires -- lit --
529
- dtcx.where(Sequel.lit(@osql))
58
+ @ast.kind_of? StandardError
530
59
  end
531
60
  end
532
61
  end