philtre 0.0.0 → 0.0.1

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.
@@ -4,20 +4,27 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'philtre/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "philtre"
7
+ spec.name = 'philtre'
8
8
  spec.version = Philtre::VERSION
9
- spec.authors = ["John Anderson"]
10
- spec.email = ["panic@semiosix.com"]
11
- spec.summary = %q{The Sequel equivalent for Ransack, Metasearch, Searchlogic}
12
- spec.description = %q{If this doesn't make you fall in love, I don't know what will.}
13
- spec.homepage = "https://github.com/djellemah/philtre"
14
- spec.license = "MIT"
9
+ spec.authors = ['John Anderson']
10
+ spec.email = ['panic@semiosix.com']
11
+ spec.summary = %q{http parameter-hash friendly filtering for Sequel}
12
+ spec.description = %q{Encode various filtering operations in http parameter hashes}
13
+ spec.homepage = 'http://github.com/djellemah/philtre'
14
+ spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.6"
22
- spec.add_development_dependency "rake"
21
+ spec.add_dependency 'sequel'
22
+ spec.add_dependency 'fastandand'
23
+ spec.add_dependency 'ripar', '~> 0.0.3'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.5'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rspec'
28
+ spec.add_development_dependency 'faker'
29
+ spec.add_development_dependency 'sqlite3'
23
30
  end
@@ -0,0 +1,57 @@
1
+ require 'rspec'
2
+ require 'faker'
3
+ require 'sequel'
4
+
5
+ require_relative '../lib/philtre/grinder.rb'
6
+ require_relative '../lib/philtre/sequel_extensions.rb'
7
+ require_relative '../lib/philtre/core_extensions.rb'
8
+
9
+ Sequel.extension :blank
10
+ Sequel.extension :core_extensions
11
+
12
+ describe Sequel::Dataset do
13
+ subject do
14
+ Sequel.mock[:t].filter( :name.lieu, :title.lieu ).order( :birth_year.lieu )
15
+ end
16
+
17
+ describe '#grind' do
18
+ it 'generates sql' do
19
+ subject.grind.sql.should == 'SELECT * FROM t'
20
+ end
21
+
22
+ it 'yields grinder' do
23
+ # predeclare so it survives the lambda
24
+ outer_grr = nil
25
+ subject.grind{|grr| outer_grr = grr }.sql.should == 'SELECT * FROM t'
26
+ outer_grr.should be_a(Philtre::Grinder)
27
+ end
28
+ end
29
+
30
+ it 'passes apply_unknown'
31
+
32
+ describe '#roller' do
33
+ it 'result has to_dataset' do
34
+ rlr = subject.roller do
35
+ where title: 'Exalted Fromaginess'
36
+ end
37
+
38
+ # This depends on Ripar, so it's a bit fragile
39
+ rlr.should respond_to(:__class__)
40
+ rlr.__class__.should == Ripar::Roller
41
+
42
+ rlr.should_not respond_to(:datset)
43
+ rlr.should respond_to(:to_dataset)
44
+ end
45
+ end
46
+
47
+ describe '#rolled' do
48
+ it 'gives back a rolled dataset' do
49
+ rlr = subject.rolled do
50
+ where title: 'Exalted Fromaginess'
51
+ end
52
+ rlr.should be_a(Sequel::Dataset)
53
+ rlr.should_not respond_to(:datset)
54
+ rlr.should_not respond_to(:to_dataset)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,502 @@
1
+ require 'rspec'
2
+ require 'faker'
3
+
4
+ require_relative '../lib/philtre/filter.rb'
5
+
6
+ # for blank?
7
+ Sequel.extension :blank
8
+
9
+ describe Philtre::Filter do
10
+ # must be in before otherwise it's unpleasant to hook the
11
+ # class in to the dataset.
12
+ before :all do
13
+ @dataset = Sequel.mock[:planks]
14
+ class Plank < Sequel::Model; end
15
+ # just stop whining and generate the bleedin' sql, k?
16
+ def @dataset.supports_regexp?; true end
17
+ end
18
+
19
+ attr_reader :dataset
20
+
21
+ describe '#initialize' do
22
+ it 'keeps parameters' do
23
+ filter = described_class.new one: 1, two: 2
24
+ filter.filter_parameters.keys.should == %i[one two]
25
+ end
26
+
27
+ it 'defaults parameters' do
28
+ described_class.new.filter_parameters.should == {}
29
+ end
30
+
31
+ it 'keeps empty parameters' do
32
+ described_class.new({}).filter_parameters.should == {}
33
+ end
34
+
35
+ it 'converts non-symbol keys' do
36
+ filter = described_class.new 'name' => Faker::Lorem.word, 'title' => Faker::Lorem.word, 'order' => 'owner'
37
+ filter.filter_parameters.keys.should == %i[name title order]
38
+ end
39
+
40
+ it 'treats nil as empty parameters' do
41
+ filter = described_class.new(nil)
42
+ filter.filter_parameters.should == {}
43
+ end
44
+
45
+ describe 'custom predicates' do
46
+ it 'from yield' do
47
+ outside = 'Outside Value'
48
+ filter = described_class.new custom_predicate: 'Special Value' do |predicates|
49
+ # yip, you really do have to use the define_method hack here
50
+ # to get outside values into the predicates module.
51
+ predicates.send :define_method, :custom_predicate do | val |
52
+ {special_field: val, other_special_field: outside}
53
+ end
54
+ end
55
+ filter.apply(dataset).sql.should == %q{SELECT * FROM planks WHERE ((special_field = 'Special Value') AND (other_special_field = 'Outside Value'))}
56
+ end
57
+
58
+ it 'from module_eval' do
59
+ filter = described_class.new custom_predicate: 'Special Value' do
60
+ # this is just a normal module block.
61
+ def custom_predicate( val )
62
+ {special_field: val}
63
+ end
64
+ end
65
+ filter.apply(dataset).sql.should == %q{SELECT * FROM planks WHERE (special_field = 'Special Value')}
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '#order_expressions' do
71
+ it 'defaults to asc' do
72
+ filter = described_class.new one: 1, two: 2, order: 'things'
73
+ @dataset.order( *filter.order_clause ).sql.should =~ /order by things asc$/i
74
+ end
75
+ end
76
+
77
+ describe '#order_expr' do
78
+ filter = described_class.new one: 1, two: 2, order: 'things'
79
+
80
+ it 'nil for nil' do
81
+ filter.order_expr(nil).should be_nil
82
+ end
83
+
84
+ it 'nil for blank' do
85
+ filter.order_expr('').should be_nil
86
+ end
87
+
88
+ it 'defaults to asc' do
89
+ filter = described_class.new one: 1, two: 2, order: 'things'
90
+ sqlfrag = filter.order_expr(:things).sql_literal(dataset)
91
+ sqlfrag.should == 'things ASC'
92
+ end
93
+ end
94
+
95
+ describe '#order_clause' do
96
+ it '[] for nil order parameter' do
97
+ filter = described_class.new one: 1, two: 2
98
+ filter.order_clause.should be_empty
99
+ end
100
+
101
+ it '[] for blank order parameter' do
102
+ filter = described_class.new one: 1, two: 2, order: ''
103
+ filter.order_clause.should be_empty
104
+ end
105
+
106
+ # These should really be part of describe '#order_expr'
107
+ it 'defaults to asc' do
108
+ filter = described_class.new one: 1, two: 2, order: 'things'
109
+ @dataset.order( *filter.order_clause ).sql.should =~ /order by things asc$/i
110
+ end
111
+
112
+ it 'respects desc' do
113
+ filter = described_class.new one: 1, two: 2, order: 'things_desc'
114
+ @dataset.order( *filter.order_clause ).sql.should =~ /order by things desc$/i
115
+ end
116
+
117
+ it 'respecs asc' do
118
+ filter = described_class.new one: 1, two: 2, order: 'things_desc'
119
+ @dataset.order( *filter.order_clause ).sql.should =~ /order by things desc$/i
120
+ end
121
+
122
+ it 'handles array' do
123
+ filter = described_class.new one: 1, two: 2, order: ['things_desc', 'stuff', 'orgle_asc']
124
+ @dataset.order( *filter.order_clause ).sql.should =~ /order by things desc, stuff asc, orgle asc$/i
125
+ end
126
+
127
+ it 'handles array with blanks' do
128
+ filter = described_class.new one: 1, two: 2, order: ['things_desc', nil, 'stuff', '', 'orgle_asc']
129
+ @dataset.order( *filter.order_clause ).sql.should =~ /order by things desc, stuff asc, orgle asc$/i
130
+ end
131
+ end
132
+
133
+ describe '#predicates' do
134
+ EASY_PREDICATES = %i[gt gte gteq lt lte lteq eq not_eq matches like not_like]
135
+ TRICKY_PREDICATES = %i[like_all like_any not_blank blank]
136
+
137
+ it 'creates predicates' do
138
+ described_class.predicates.predicate_names.sort.should == (EASY_PREDICATES + TRICKY_PREDICATES).sort
139
+ end
140
+
141
+ EASY_PREDICATES.each do |predicate|
142
+ it "_#{predicate} becomes expression" do
143
+ field = Faker::Lorem.word.to_sym
144
+ value = Faker::Lorem.word
145
+ expr = described_class.predicates.call field, value
146
+
147
+ expr.should be_a(Sequel::SQL::BooleanExpression)
148
+
149
+ expr.args.first.should be_a(Sequel::SQL::Identifier)
150
+ expr.args.first.should == Sequel.expr(field)
151
+ expr.args.last.should == value
152
+
153
+ expr.sql_literal(@dataset).should be_a(String)
154
+ end
155
+ end
156
+
157
+ describe 'like_all' do
158
+ it 'takes one' do
159
+ field = Faker::Lorem.word
160
+ value = Faker::Lorem.word
161
+ expr = described_class.predicates.call :"#{field}_like_all", value
162
+ expr.args.size.should == 1
163
+ expr.op.should == :NOOP
164
+
165
+ expr.args.first.op.should == :'~*'
166
+ ident, value = expr.args.first.args
167
+ ident.value.should == field
168
+ value.should == value
169
+ end
170
+
171
+ it 'takes many' do
172
+ field = Faker::Lorem.word
173
+ value = 3.times.map{ Faker::Lorem.word }
174
+ expr = described_class.predicates.call :"#{field}_like_all", value
175
+ expr.args.size.should == 3
176
+ expr.op.should == :AND
177
+ end
178
+ end
179
+
180
+ describe 'like_any' do
181
+ it 'takes one' do
182
+ field = Faker::Lorem.word
183
+ value = Faker::Lorem.word
184
+ expr = described_class.predicates.call :"#{field}_like_any", value
185
+ expr.args.size.should == 1
186
+ expr.op.should == :NOOP
187
+
188
+ expr.args.first.op.should == :'~*'
189
+ ident, value = expr.args.first.args
190
+ ident.value.should == field
191
+ value.should == value
192
+ end
193
+
194
+ it 'takes many' do
195
+ field = Faker::Lorem.word
196
+ value = 3.times.map{ Faker::Lorem.word }
197
+ expr = described_class.predicates.call :"#{field}_like_any", value
198
+ expr.args.size.should == 3
199
+ expr.op.should == :OR
200
+ end
201
+ end
202
+
203
+ it 'not_blank' do
204
+ field = Sequel.expr Faker::Lorem.word
205
+ expr = described_class.predicates.call :"#{field}_not_blank", Faker::Lorem.word
206
+
207
+ expr.op.should == :AND
208
+
209
+ # the not-nil part
210
+ expr.args.first.op.should == :'IS NOT'
211
+
212
+ # the not empty string part
213
+ expr.args.last.op.should == :'!='
214
+ expr.args.last.args.last.should == ''
215
+ end
216
+ end
217
+
218
+ describe '#to_expr' do
219
+ let(:filter){ described_class.new name: Faker::Lorem.word, title: Faker::Lorem.word }
220
+ it 'is == for name only' do
221
+ expr = Sequel.expr( filter.to_expr( :name, 'hallelujah' ) )
222
+ expr.op.should == :'='
223
+ expr.args.first.should be_a(Sequel::SQL::Identifier)
224
+ expr.args.first.value.should == 'name'
225
+ expr.args.last.should == 'hallelujah'
226
+ end
227
+
228
+ it 'like' do
229
+ expr = Sequel.expr( filter.to_expr( :owner_like, 'hallelujah' ) )
230
+ expr.op.should == :'~*'
231
+ expr.args.first.should be_a(Sequel::SQL::Identifier)
232
+ expr.args.first.value.should == 'owner'
233
+ expr.args.last.should == 'hallelujah'
234
+ end
235
+
236
+ it 'keeps blank values' do
237
+ filter.to_expr( :owner, '' ).should_not be_nil
238
+ filter.to_expr( :owner, nil ).should_not be_nil
239
+ filter.to_expr( :owner, [] ).should_not be_nil
240
+ end
241
+
242
+ it 'substitutes a field name' do
243
+ expr = Sequel.expr( filter.to_expr( :owner_like, 'hallelujah', :heavens__salutation ) )
244
+ expr.op.should == :'~*'
245
+ expr.args.first.should be_kind_of(Sequel::SQL::QualifiedIdentifier)
246
+ expr.args.first.column.should == 'salutation'
247
+ expr.args.first.table.should == 'heavens'
248
+ expr.args.last.should == 'hallelujah'
249
+ end
250
+
251
+ it 'must always be a Sequel::SQL::Expression' do
252
+ filter.predicates.extend_with do
253
+ def year_range(jumbled_years)
254
+ first, last = jumbled_years.sort.instance_eval{|ry| [ry.first, ry.last]}
255
+ { year: first..last }
256
+ end
257
+ end
258
+
259
+ expr = filter.to_expr( :year_range, [1984, 1970, 2012] )
260
+ expr.should be_a(Sequel::SQL::Expression)
261
+ expr.sql_literal(dataset).should == '((year >= 1970) AND (year <= 2012))'
262
+ end
263
+ end
264
+
265
+ describe '#expr_for' do
266
+ let(:filter){ described_class.new name: Faker::Lorem.word, title: Faker::Lorem.word, interstellar: '' }
267
+
268
+ it 'nil for no value' do
269
+ filter.expr_for(:bleh).should be_nil
270
+ end
271
+
272
+ it 'nil for no value' do
273
+ filter.expr_for(:interstellar).should be_nil
274
+ end
275
+
276
+ it 'expression for existing value' do
277
+ filter.expr_for(:name).should_not be_nil
278
+ filter.expr_for(:name).should be_a(Sequel::SQL::BooleanExpression)
279
+ end
280
+
281
+ it 'alternate name' do
282
+ expr = filter.expr_for(:name, :things__name)
283
+ expr.should_not be_nil
284
+ expr.should be_a(Sequel::SQL::BooleanExpression)
285
+
286
+ expr.args.first.tap do |field_expr|
287
+ field_expr.column.should == 'name'
288
+ field_expr.table.should == 'things'
289
+ end
290
+ end
291
+ end
292
+
293
+ describe '#order_for' do
294
+ let(:filter){ described_class.new name: Faker::Lorem.word, title: Faker::Lorem.word, order:[:name, :title, :year] }
295
+ let(:dataset){ Sequel.mock[:things] }
296
+
297
+ it 'nil for no parameter' do
298
+ filter.order_for( :icecream_count ).should be_nil
299
+ end
300
+
301
+ it 'ascending' do
302
+ filter.order_for(:year).sql_literal(dataset).should == 'year ASC'
303
+ end
304
+
305
+ it 'name clash' do
306
+ filter.order_for(:title).sql_literal(dataset).should == 'title ASC'
307
+ end
308
+ end
309
+
310
+ describe '#expressions' do
311
+ it 'generates expressions' do
312
+ expressions = described_class.new( trailer: 'large' ).expressions
313
+ expressions.size.should == 1
314
+ expressions.first.should be_a(Sequel::SQL::BooleanExpression)
315
+ expr, value = expressions.first.args
316
+ expr.should be_a(Sequel::SQL::Identifier)
317
+ expr.value.should == 'trailer'
318
+ value.should == 'large'
319
+ end
320
+
321
+ it 'handles stringified operators' do
322
+ expressions = described_class.new( trailer_gte: 'large' ).expressions
323
+ expressions.size.should == 1
324
+ expressions.first.should be_a(Sequel::SQL::BooleanExpression)
325
+ expr, value = expressions.first.args
326
+ expr.should be_a(Sequel::SQL::Identifier)
327
+ expr.value.should == 'trailer'
328
+ value.should == 'large'
329
+ end
330
+
331
+ it 'ignores order:' do
332
+ described_class.new(order: %w[one two tre]).expressions.should be_empty
333
+ end
334
+
335
+ it "ignores '' value" do
336
+ described_class.new( address: '' ).expressions.should be_empty
337
+ end
338
+
339
+ it 'ignores nil value' do
340
+ described_class.new( address: nil ).expressions.should be_empty
341
+ end
342
+
343
+ it 'accepts []' do
344
+ expressions = described_class.new( flavour: [] ).expressions
345
+ expressions.size.should == 1
346
+ expressions.first.should be_a(Sequel::SQL::BooleanExpression)
347
+ expr, value = expressions.first.args
348
+ expr.should be_a(Sequel::SQL::Identifier)
349
+ expr.value.should == 'flavour'
350
+ value.should == []
351
+ end
352
+ end
353
+
354
+ describe '#apply' do
355
+ let(:filter){ described_class.new name: Faker::Lorem.word, title: Faker::Lorem.word }
356
+
357
+ # make sure the Model dataset isn't impacted by setting the ordering
358
+ # on the filtered dataset.
359
+ it 'clones' do
360
+ orig_dataset = Plank.dataset
361
+ filter.filter_parameters[:order] = :title
362
+ filter.apply(Plank.dataset)
363
+ Plank.dataset.should == orig_dataset
364
+ end
365
+
366
+ it 'accepts Sequel::Model subclasses' do
367
+ ds = filter.apply(Plank)
368
+ ds.should be_a(Sequel::Dataset)
369
+ ds.sql.should =~ /planks/
370
+ end
371
+
372
+ it 'filter parameters' do
373
+ sql = filter.apply(@dataset).sql
374
+ sql.should =~ /select \* from planks where \(\(name = '\w+'\) and \(title = '\w+'\)\)$/i
375
+ end
376
+
377
+ it 'single order clause' do
378
+ filter.filter_parameters[:order] = :title
379
+ filter.apply(@dataset).sql.should =~ /order by.*title/i
380
+ end
381
+
382
+ it 'multiple order clause' do
383
+ filter.filter_parameters[:order] = [:title, :owner]
384
+ filter.apply(@dataset).sql.should =~ /order by.*title.*owner/i
385
+ end
386
+
387
+ it 'empty filter parameters' do
388
+ filter = described_class.new
389
+ filter.filter_parameters.should be_empty
390
+ filter.apply(@dataset).sql.should =~ /select \* from planks$/i
391
+ end
392
+
393
+ it 'no order clause' do
394
+ sql = filter.apply(@dataset).sql
395
+ sql.should_not =~ /order by/i
396
+ end
397
+
398
+ it 'no order clause keeps previous order clause' do
399
+ sql = filter.apply(@dataset.order(:watookal)).sql
400
+ sql.should =~ /order by/i
401
+ end
402
+
403
+ it 'excludes blank values' do
404
+ filter.filter_parameters[:name] = ''
405
+ sql = filter.apply(@dataset).sql
406
+ sql.should =~ /select \* from planks where \(title = '\w+'\)$/i
407
+ end
408
+
409
+ it 'excludes nil values' do
410
+ filter.filter_parameters[:name] = nil
411
+ sql = filter.apply(@dataset).sql
412
+ sql.should =~ /select \* from planks where \(title = '\w+'\)$/i
413
+ end
414
+ end
415
+
416
+ describe '#empty?' do
417
+ it 'true on no parameters' do
418
+ described_class.new.should be_empty
419
+ end
420
+
421
+ it 'false with parameters' do
422
+ described_class.new(one: 1, two: 2).should_not be_empty
423
+ end
424
+ end
425
+
426
+ describe '#subset' do
427
+ it 'has specified subset of parameter values' do
428
+ filter = described_class.new done_with: 'Hammers', fixed_by: 'Thor'
429
+ filter.subset( :done_with ).filter_parameters.keys.should == [:done_with]
430
+ end
431
+
432
+ it 'has block specified subset of parameter values' do
433
+ filter = described_class.new done_with: 'Hammers', fixed_by: 'Thor'
434
+ filter.subset{|k,v| k == :done_with}.filter_parameters.keys.should == [:done_with]
435
+ end
436
+
437
+ it 'keeps custom predicates' do
438
+ filter = described_class.new done_with: 'Hammers' do
439
+ def done_with( things )
440
+ Sequel.expr done: things
441
+ end
442
+ end
443
+
444
+ filter.subset( :done_with ).predicates.should respond_to(:done_with)
445
+ end
446
+ end
447
+
448
+ describe '#extract!' do
449
+ it 'gives back subset' do
450
+ filter = described_class.new first: 'James', second: 'McDonald', third: 'Fraser'
451
+ extracted = filter.extract!(:first)
452
+ extracted.to_h.size.should == 1
453
+ extracted.to_h.should have_key(:first)
454
+ end
455
+
456
+ it 'removes specified keys' do
457
+ filter = described_class.new first: 'James', second: 'McDonald', third: 'Fraser'
458
+ extracted = filter.extract!(:first, :third)
459
+ filter.to_h.size.should == 1
460
+ filter.to_h.should have_key(:second)
461
+ end
462
+ end
463
+
464
+ describe '#to_h' do
465
+ def filter
466
+ @filter ||= described_class.new first: 'James', second: 'McDonald', third: 'Fraser', fourth: '', fifth: nil
467
+ end
468
+
469
+ it 'all values' do
470
+ filter.to_h(true).size.should == filter.filter_parameters.size
471
+ end
472
+
473
+ it 'only non-blank values' do
474
+ filter.to_h.size.should == 3
475
+ end
476
+ end
477
+
478
+ describe '#clone' do
479
+ it 'plain clone' do
480
+ filter = described_class.new first: 'James', second: 'McDonald', third: 'Fraser'
481
+ cloned = filter.clone
482
+ cloned.filter_parameters.should == filter.filter_parameters
483
+ end
484
+
485
+ it 'clone with extras leaves original' do
486
+ value_hash = {first: 'James', second: 'McDonald', third: 'Fraser'}.freeze
487
+ filter = described_class.new value_hash
488
+ cloned = filter.clone( extra: 'Magoodies')
489
+
490
+ filter.filter_parameters.should == value_hash
491
+ end
492
+
493
+ it 'clone with extras adds values' do
494
+ value_hash = {first: 'James', second: 'McDonald', third: 'Fraser'}.freeze
495
+ filter = described_class.new value_hash
496
+ cloned = filter.clone( extra: 'Magoodies')
497
+
498
+ (cloned.filter_parameters.keys & filter.filter_parameters.keys).should == filter.filter_parameters.keys
499
+ (cloned.filter_parameters.keys - filter.filter_parameters.keys).should == [:extra]
500
+ end
501
+ end
502
+ end