albanpeignier-searchapi 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,98 @@
1
+ module SearchApi
2
+
3
+ # Utility class that implements logic for sql fragments (such as ["escaped_sql = ?", dirty_string])
4
+
5
+ class SqlFragment < Array
6
+ # A class that makes it more easy to manipulate SQL fragments : 'a=1' or ['a=?', 1]
7
+ #
8
+ # c = SqlFragment() => []
9
+ # c = SqlFragment('') => []
10
+ # c = SqlFragment('a=1') => ["a=1"]
11
+ # c = SqlFragment(['a=?', 1]) => ["a=?", 1]
12
+ # c.or(SqlFragment()) => ["a=?", 1]
13
+ # c.or(SqlFragment('b=2')) => ["(a=?) OR (b=2)", 1]
14
+ # c.or!(['b=?', 2]) => ["(a=?) OR (b=?)", 1, 2]
15
+ # c.and('c=3') => ["((a=?) OR (b=?)) AND (c=3)", 1, 2]
16
+ # c << ['c=?', 3] => ["((a=?) OR (b=?)) AND (c=?)", 1, 2, 3]
17
+ # c => ["((a=?) OR (b=?)) AND (c=?)", 1, 2, 3]
18
+
19
+ class << self
20
+ def sanitize(*args)
21
+ self.new(*args).sanitize
22
+ end
23
+ end
24
+
25
+ attr_reader :logical_operator
26
+
27
+ def initialize(*args)
28
+ if args.length > 1
29
+ else
30
+ args = args.first
31
+ end
32
+
33
+ # default operator is AND
34
+ self.logical_operator = :and
35
+
36
+ return if args.nil? or args.empty?
37
+
38
+ if args.is_a? Array
39
+ replace(args)
40
+ else
41
+ self.push(args)
42
+ end
43
+ end
44
+
45
+ def logical_operator=(logical_operator)
46
+ @logical_operator = (logical_operator || :and)
47
+ end
48
+
49
+ def and(inSqlFragment)
50
+ inSqlFragment = SqlFragment.new(inSqlFragment) unless inSqlFragment.is_a?(SqlFragment)
51
+ return self if inSqlFragment.empty?
52
+ return inSqlFragment if empty?
53
+ SqlFragment(["(#{sqlString}) AND (#{inSqlFragment[0]})"] + sqlParameters + inSqlFragment[1..-1])
54
+ end
55
+ def and!(inSqlFragment) replace(self.and(inSqlFragment)) end
56
+
57
+ def or(inSqlFragment)
58
+ inSqlFragment = SqlFragment.new(inSqlFragment) unless inSqlFragment.is_a?(SqlFragment)
59
+ return self if inSqlFragment.empty?
60
+ return inSqlFragment if empty?
61
+ SqlFragment(["(#{sqlString}) OR (#{inSqlFragment[0]})"] + sqlParameters + inSqlFragment[1..-1])
62
+ end
63
+ def or!(inSqlFragment) replace(self.or(inSqlFragment)) end
64
+
65
+ def <<(inSqlFragment)
66
+ case logical_operator
67
+ when :and!, :and
68
+ and!(inSqlFragment)
69
+ when :or!, :or
70
+ or!(inSqlFragment)
71
+ else
72
+ raise "Unsupported logical_operator #{logical_operator.inspect}"
73
+ end
74
+ end
75
+
76
+ def not
77
+ return self if empty?
78
+ SqlFragment(["NOT(#{sqlString})"]+sqlParameters)
79
+ end
80
+
81
+ def sqlString
82
+ self[0]
83
+ end
84
+ def sqlParameters
85
+ self[1..-1]
86
+ end
87
+
88
+ def sanitize
89
+ fragment = self
90
+ ActiveRecord::Base.instance_eval do sanitize_sql(fragment) end
91
+ end
92
+ end
93
+ end
94
+
95
+
96
+ def SqlFragment(*args)
97
+ SearchApi::SqlFragment.new(*args)
98
+ end
@@ -0,0 +1,132 @@
1
+ module SearchApi
2
+
3
+ # Utility class that implements fulltext search.
4
+ #
5
+ # Includes some Google-like features.
6
+
7
+ class TextCriterion
8
+ # t = TextCriterion.new('bonjour +les +amis -toto -"allons bon" define:"salut poulette"')
9
+ # t.meta_keywords => {"define"=>["salut poulette"]}
10
+ # t.mandatory_keywords => ["les", "amis"]
11
+ # t.optional_keywords => ["bonjour"],
12
+ # t.negative_keywords => ["toto", "allons bon"],
13
+ # t.positive_keywords = t.mandatory_keywords+t.optional_keywords
14
+
15
+ #
16
+ attr_accessor :meta_keywords
17
+ attr_accessor :mandatory_keywords
18
+ attr_accessor :negative_keywords
19
+ attr_accessor :optional_keywords
20
+
21
+ def initialize(inSearchString='', inOptions= {})
22
+ # inOptions may contain :
23
+ # :exclude => string, Regexp, or list of Strings and Regexp to exclude (strings are case insensitive)
24
+
25
+ @options = inOptions
26
+ @options[:exclude] = [@options[:exclude]] unless @options[:exclude].nil? || (@options[:exclude].is_a?(Enumerable) && !@options[:exclude].is_a?(String))
27
+ @options[:parse_meta?] = true if @options[:parse_meta?].nil?
28
+
29
+ @meta_keywords = {}
30
+ @mandatory_keywords = []
31
+ @negative_keywords = []
32
+ @optional_keywords = []
33
+
34
+ unless inSearchString.blank?
35
+
36
+ currentMeta = nil
37
+
38
+ splitter = /
39
+ (
40
+ [-+]?\b[^ ":]+\b:?
41
+ )
42
+ |
43
+ (
44
+ [-+]?"[^"]*"
45
+ )
46
+ /x
47
+
48
+ inSearchString.gsub(/\s+/, ' ').scan(splitter).each { |keyword|
49
+ keyword=(keyword[0]||keyword[1]).gsub(/"/, '')
50
+
51
+ if currentMeta
52
+ @meta_keywords[currentMeta] ||= []
53
+ @meta_keywords[currentMeta] << keyword
54
+ currentMeta = nil
55
+ else
56
+ case keyword
57
+ when /^-/
58
+ @negative_keywords << keyword[1..-1] unless exclude_keyword?(keyword[1..-1])
59
+ when /^\+/
60
+ @mandatory_keywords << keyword[1..-1] unless exclude_keyword?(keyword[1..-1])
61
+ when /:$/
62
+ if @options[:parse_meta?]
63
+ currentMeta = keyword[0..-2]
64
+ else
65
+ @optional_keywords << keyword unless exclude_keyword?(keyword)
66
+ end
67
+ else
68
+ @optional_keywords << keyword unless exclude_keyword?(keyword)
69
+ end
70
+ end
71
+ }
72
+
73
+ # if everything is excluded, look for the whole search string
74
+ @optional_keywords << inSearchString if @meta_keywords.empty? && @mandatory_keywords.empty? && @negative_keywords.empty? && @optional_keywords.empty?
75
+ end
76
+ end
77
+
78
+ def to_s
79
+ chunks = []
80
+ chunks += @mandatory_keywords.map { |x| (x =~ / /) ? "+\"#{x}\"" : "+#{x}" } unless @mandatory_keywords.blank?
81
+ chunks += @negative_keywords.map { |x| (x =~ / /) ? "-\"#{x}\"" : "-#{x}" } unless @negative_keywords.blank?
82
+ chunks += @optional_keywords.map { |x| (x =~ / /) ? "\"#{x}\"" : x.to_s } unless @optional_keywords.blank?
83
+ chunks += @meta_keywords.inject([]) { |s, key_value|
84
+ key, value = key_value
85
+ if value.is_a?(Array)
86
+ s += value.map { |x| (x =~ / /) ? "#{key}:\"#{x}\"" : "#{key}:#{x}" }
87
+ else
88
+ s << ((value =~ / /) ? "#{key}:\"#{value}\"" : "#{key}:#{value}")
89
+ end
90
+ s
91
+ } if @meta_keywords
92
+ chunks.join(' ')
93
+ end
94
+
95
+ def positive_keywords
96
+ @mandatory_keywords + @optional_keywords
97
+ end
98
+
99
+ def condition(inFields)
100
+ conditions = SqlFragment.new
101
+
102
+ conditions << (@mandatory_keywords.inject(SqlFragment.new) { |cv, value|
103
+ value = "%#{value}%"
104
+ cv.and(inFields.inject(SqlFragment.new) { |cf, field|
105
+ cf.or(["#{field} like ?", value])
106
+ })
107
+ })
108
+
109
+ conditions << (@negative_keywords.inject(SqlFragment.new) { |cv, value|
110
+ value = "%#{value}%"
111
+ cv.and(inFields.inject(SqlFragment.new) { |cf, field|
112
+ cf.or(["#{field} is not null AND #{field} like ?", value])
113
+ })
114
+ }.not)
115
+
116
+ conditions << (@optional_keywords.inject(SqlFragment.new) { |cv, value|
117
+ value = "%#{value}%"
118
+ cv.or(inFields.inject(SqlFragment.new) { |cf, field|
119
+ cf.or(["#{field} like ?", value])
120
+ })
121
+ })
122
+
123
+ conditions
124
+ end
125
+
126
+ protected
127
+ def exclude_keyword?(inKeyword)
128
+ return false unless @options[:exclude]
129
+ return @options[:exclude].any? { |exclude| inKeyword =~ (exclude.is_a?(Regexp) ? exclude : Regexp.new(Regexp.escape(exclude), 'i')) }
130
+ end
131
+ end
132
+ end
data/searchapi.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{searchapi}
5
+ s.version = "0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Gwendal Rou\303\251"]
9
+ s.date = %q{2009-03-31}
10
+ s.email = ["gr@pierlis.com"]
11
+ s.extra_rdoc_files = ["Manifest.txt"]
12
+ s.files = ["MIT-LICENSE", "Manifest.txt", "README", "Rakefile", "db/migrate/001_create_searchable.rb", "init.rb", "install.rb", "lib/search_api.rb", "lib/search_api/active_record_bridge.rb", "lib/search_api/active_record_integration.rb", "lib/search_api/bridge.rb", "lib/search_api/callbacks.rb", "lib/search_api/errors.rb", "lib/search_api/search.rb", "lib/search_api/sql_fragment.rb", "lib/search_api/text_criterion.rb", "searchapi.gemspec", "tasks/search_api_tasks.rake", "test/active_record_bridge_test.rb", "test/active_record_integration_test.rb", "test/bridge_test.rb", "test/callbacks_test.rb", "test/mock_model.rb", "test/search_test.rb", "uninstall.rb"]
13
+ s.has_rdoc = true
14
+ s.rdoc_options = ["--main", "README.txt"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{searchapi}
17
+ s.rubygems_version = %q{1.3.1}
18
+ s.summary = %q{Ruby on Rails plugin which purpose is to let the developper define Search APIs for ActiveRecord models}
19
+
20
+ if s.respond_to? :specification_version then
21
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
22
+ s.specification_version = 2
23
+
24
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
25
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
26
+ s.add_development_dependency(%q<hoe>, [">= 1.11.0"])
27
+ else
28
+ s.add_dependency(%q<activerecord>, [">= 0"])
29
+ s.add_dependency(%q<hoe>, [">= 1.11.0"])
30
+ end
31
+ else
32
+ s.add_dependency(%q<activerecord>, [">= 0"])
33
+ s.add_dependency(%q<hoe>, [">= 1.11.0"])
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ namespace :searchapi do
2
+ task :migrate_test_db => :environment do
3
+ RAILS_ENV = "test" unless defined?(RAILS_ENV)
4
+
5
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
6
+ ActiveRecord::Migrator.migrate(File.join(File.dirname(__FILE__), %w{.. db migrate}))
7
+ end
8
+ end
@@ -0,0 +1,488 @@
1
+ require 'test/unit'
2
+ RAILS_ENV = "test" unless defined?(RAILS_ENV)
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
4
+ require 'search_api'
5
+ require 'active_record_bridge'
6
+ require 'test/mock_model'
7
+
8
+ class ActiveRecordBridgeTest < Test::Unit::TestCase
9
+
10
+ def setup
11
+ begin
12
+ Searchable.find(:first)
13
+ rescue
14
+ raise "run 'rake searchapi_migrate_test_db' in the application directory before running searchapi tests"
15
+ end
16
+ end
17
+
18
+
19
+ def test_active_record_type_cast
20
+ # create a search class
21
+ search_class = Class.new(::SearchApi::Search::Base)
22
+
23
+ # define some search_accessors
24
+ search_class.class_eval do
25
+ model Searchable, :type_cast => true
26
+ end
27
+
28
+ search = search_class.new
29
+
30
+ # test number column
31
+ search.age = '12'
32
+ assert_equal 12, search.age
33
+
34
+ # test text column
35
+ search.name = ' thing '
36
+ assert_equal ' thing ', search.name
37
+
38
+ # test boolean column
39
+ search.funny = true
40
+ assert_equal true, search.funny
41
+
42
+ search.funny = false
43
+ assert_equal false, search.funny
44
+
45
+ search.funny = 1
46
+ assert_equal true, search.funny
47
+
48
+ search.funny = 0
49
+ assert_equal false, search.funny
50
+ end
51
+
52
+
53
+ def test_empty_search
54
+ # create a search class
55
+ search_class = Class.new(::SearchApi::Search::Base)
56
+
57
+ # define some search_accessors
58
+ search_class.class_eval do
59
+ model Searchable
60
+ end
61
+
62
+ assert_equivalent_request search_class,
63
+ nil,
64
+ nil
65
+ end
66
+
67
+
68
+ def test_eq_operator
69
+ # create a search class
70
+ search_class = Class.new(::SearchApi::Search::Base)
71
+
72
+ # define some search_accessors
73
+ search_class.class_eval do
74
+ model Searchable
75
+ search_accessor :years, :column => :age, :operator => :eq
76
+ end
77
+
78
+
79
+ # nil equality must behaves like ActiveRecord::Base
80
+ assert_equivalent_request search_class,
81
+ {:conditions => {:age => nil}},
82
+ {:years => nil}
83
+
84
+ # value equality must behaves like ActiveRecord::Base
85
+ assert_equivalent_request search_class,
86
+ {:conditions => {:age => 50}},
87
+ {:years => 50}
88
+
89
+ # array equality must behaves like ActiveRecord::Base
90
+ assert_equivalent_request search_class,
91
+ {:conditions => {:age => [11,23,54,92,42,24,25,26]}},
92
+ {:years => [11,23,54,92,42,24,25,26]}
93
+
94
+ # range equality must behaves like ActiveRecord::Base
95
+ assert_equivalent_request search_class,
96
+ {:conditions => {:age => 0..20}},
97
+ {:years => 0..20}
98
+ end
99
+
100
+
101
+ def test_neq_operator
102
+ # create a search class
103
+ search_class = Class.new(::SearchApi::Search::Base)
104
+
105
+ # define some search_accessors
106
+ search_class.class_eval do
107
+ model Searchable
108
+ search_accessor :not_funny, :column => :funny, :operator => :neq, :type_cast => true
109
+ end
110
+
111
+
112
+ # nil negation -> don't search (allow null values)
113
+ assert_equivalent_request search_class,
114
+ {:conditions => "funny IS NOT NULL"},
115
+ {:not_funny => nil}
116
+
117
+ # valued negation
118
+ assert_equivalent_request search_class,
119
+ {:conditions => ["funny = ? OR funny IS NULL", false]},
120
+ {:not_funny => true}
121
+ assert_equivalent_request search_class,
122
+ {:conditions => ["funny = ? OR funny IS NULL", true]},
123
+ {:not_funny => false}
124
+ end
125
+
126
+
127
+ def test_lt_operator
128
+ # create a search class
129
+ search_class = Class.new(::SearchApi::Search::Base)
130
+
131
+ # define some search_accessors
132
+ search_class.class_eval do
133
+ model Searchable
134
+ search_accessor :limit_age, :column => :age, :operator => :lt
135
+ end
136
+
137
+
138
+ # nil upper bound -> don't search (allow null values)
139
+ assert_equivalent_request search_class,
140
+ nil,
141
+ {:limit_age => nil}
142
+
143
+ # valued upper bound
144
+ assert_equivalent_request search_class,
145
+ {:conditions => "age < 50"},
146
+ {:limit_age => 50}
147
+ end
148
+
149
+
150
+ def test_lte_operator
151
+ # create a search class
152
+ search_class = Class.new(::SearchApi::Search::Base)
153
+
154
+ # define some search_accessors
155
+ search_class.class_eval do
156
+ model Searchable
157
+ search_accessor :limit_age, :column => :age, :operator => :lte
158
+ end
159
+
160
+
161
+ # nil upper bound -> don't search (allow null values)
162
+ assert_equivalent_request search_class,
163
+ nil,
164
+ {:limit_age => nil}
165
+
166
+ # valued upper bound
167
+ assert_equivalent_request search_class,
168
+ {:conditions => "age <= 50"},
169
+ {:limit_age => 50}
170
+ end
171
+
172
+
173
+ def test_gt_operator
174
+ # create a search class
175
+ search_class = Class.new(::SearchApi::Search::Base)
176
+
177
+ # define some search_accessors
178
+ search_class.class_eval do
179
+ model Searchable
180
+ search_accessor :limit_age, :column => :age, :operator => :gt
181
+ end
182
+
183
+
184
+ # nil lower bound -> don't search (allow null values)
185
+ assert_equivalent_request search_class,
186
+ nil,
187
+ {:limit_age => nil}
188
+
189
+ # valued lower bound
190
+ assert_equivalent_request search_class,
191
+ {:conditions => "age > 50"},
192
+ {:limit_age => 50}
193
+ end
194
+
195
+
196
+ def test_gte_operator
197
+ # create a search class
198
+ search_class = Class.new(::SearchApi::Search::Base)
199
+
200
+ # define some search_accessors
201
+ search_class.class_eval do
202
+ model Searchable
203
+ search_accessor :limit_age, :column => :age, :operator => :gte
204
+ end
205
+
206
+
207
+ # nil upper bound -> don't search (allow null values)
208
+ assert_equivalent_request search_class,
209
+ nil,
210
+ {:limit_age => nil}
211
+
212
+ # valued upper bound
213
+ assert_equivalent_request search_class,
214
+ {:conditions => "age >= 50"},
215
+ {:limit_age => 50}
216
+ end
217
+
218
+
219
+ def test_starts_with_operator
220
+ # create a search class
221
+ search_class = Class.new(::SearchApi::Search::Base)
222
+
223
+ # define some search_accessors
224
+ search_class.class_eval do
225
+ model Searchable
226
+ search_accessor :name_beginning, :column => :name, :operator => :starts_with, :type_cast => true
227
+ end
228
+
229
+
230
+ # nil beginning string -> don't search (allow null values)
231
+ assert_equivalent_request search_class,
232
+ nil,
233
+ {:name_beginning => nil}
234
+
235
+ # empty beginning string -> don't search (allow null values)
236
+ assert_equivalent_request search_class,
237
+ nil,
238
+ {:name_beginning => ''}
239
+
240
+ # valued beginning string
241
+ assert_equivalent_request search_class,
242
+ {:conditions => ["name like ?", 'Mary%']},
243
+ {:name_beginning => 'Mary'}
244
+ end
245
+
246
+
247
+ def test_ends_with_operator
248
+ # create a search class
249
+ search_class = Class.new(::SearchApi::Search::Base)
250
+
251
+ # define some search_accessors
252
+ search_class.class_eval do
253
+ model Searchable
254
+ search_accessor :name_ending, :column => :name, :operator => :ends_with, :type_cast => true
255
+ end
256
+
257
+
258
+ # nil ending string -> don't search (allow null values)
259
+ assert_equivalent_request search_class,
260
+ nil,
261
+ {:name_ending => nil}
262
+
263
+ # empty ending string -> don't search (allow null values)
264
+ assert_equivalent_request search_class,
265
+ nil,
266
+ {:name_ending => ''}
267
+
268
+ # valued ending string
269
+ assert_equivalent_request search_class,
270
+ {:conditions => ["name like ?", '%Ann']},
271
+ {:name_ending => 'Ann'}
272
+ end
273
+
274
+
275
+ def test_contains_operator
276
+ # create a search class
277
+ search_class = Class.new(::SearchApi::Search::Base)
278
+
279
+ # define some search_accessors
280
+ search_class.class_eval do
281
+ model Searchable
282
+ search_accessor :name_partial, :column => :name, :operator => :contains, :type_cast => true
283
+ end
284
+
285
+
286
+ # nil partial string -> don't search (allow null values)
287
+ assert_equivalent_request search_class,
288
+ nil,
289
+ {:name_partial => nil}
290
+
291
+ # empty partial string -> don't search (allow null values)
292
+ assert_equivalent_request search_class,
293
+ nil,
294
+ {:name_partial => ''}
295
+
296
+ # valued partial string
297
+ assert_equivalent_request search_class,
298
+ {:conditions => ["name like ?", '%ar%']},
299
+ {:name_partial => 'ar'}
300
+ end
301
+
302
+
303
+ def test_full_text_operator
304
+ # create a search class
305
+ search_class = Class.new(::SearchApi::Search::Base)
306
+
307
+ # define some search_accessors
308
+ search_class.class_eval do
309
+ model Searchable
310
+ search_accessor :keyword, :column => [:name, :city], :operator => :full_text
311
+ end
312
+
313
+
314
+ # nil full text search -> don't search (allow null values)
315
+ assert_equivalent_request search_class,
316
+ nil,
317
+ {:keyword => nil}
318
+
319
+ # empty full text search -> don't search (allow null values)
320
+ assert_equivalent_request search_class,
321
+ nil,
322
+ {:keyword => nil}
323
+
324
+ # valued full text search
325
+ assert_equivalent_request search_class,
326
+ {:conditions => ["((searchables.`name` like ?) OR (searchables.`city` like ?)) OR ((searchables.`name` like ?) OR (searchables.`city` like ?))", "%Mary%", "%Mary%", "%Paris%", "%Paris%"]},
327
+ {:keyword => 'Mary Paris'}
328
+ end
329
+
330
+
331
+ def test_multi_column_search
332
+ # create a search class
333
+ search_class = Class.new(::SearchApi::Search::Base)
334
+
335
+ # define some search_accessors
336
+ search_class.class_eval do
337
+ model Searchable
338
+ end
339
+
340
+
341
+ assert_equivalent_request search_class,
342
+ {:conditions => {:age=>(10..30).to_a, :city=>['Paris', 'London']}},
343
+ {:age => (10..30).to_a, :city => ['Paris', 'London']}
344
+ end
345
+
346
+
347
+ def test_automatic_search_attribute_builders
348
+ # create a search class
349
+ search_class = Class.new(::SearchApi::Search::Base)
350
+
351
+ # define some search_accessors
352
+ search_class.class_eval do
353
+ model Searchable
354
+ end
355
+
356
+ # assert columns names are there
357
+ assert Searchable.columns.map(&:name).all? { |column_name| search_class.search_attributes.include?(column_name.to_sym)}
358
+
359
+ # assert lower and upper bound search attributes are there
360
+ assert %w(min_age max_age).all? { |column_name| search_class.search_attributes.include?(column_name.to_sym)}
361
+ end
362
+
363
+
364
+ def test_equality_automatic_attributes
365
+ # create a search class
366
+ search_class = Class.new(::SearchApi::Search::Base)
367
+
368
+ # define some search_accessors
369
+ search_class.class_eval do
370
+ model Searchable
371
+ end
372
+
373
+
374
+ # nil equality must behaves like ActiveRecord::Base
375
+ conditions = {:age => nil}
376
+ assert_equivalent_request search_class,
377
+ {:conditions => conditions},
378
+ conditions
379
+
380
+ # value equality must behaves like ActiveRecord::Base
381
+ conditions = {:name => 'Mary'}
382
+ assert_equivalent_request search_class,
383
+ {:conditions => conditions},
384
+ conditions
385
+
386
+ # array equality must behaves like ActiveRecord::Base
387
+ conditions = {:age => [11,23,54,92,42,24,25,26]}
388
+ assert_equivalent_request search_class,
389
+ {:conditions => conditions},
390
+ conditions
391
+
392
+ # range equality must behaves like ActiveRecord::Base
393
+ conditions = {:age => 0..20}
394
+ assert_equivalent_request search_class,
395
+ {:conditions => conditions},
396
+ conditions
397
+ end
398
+
399
+
400
+ def test_lower_bound_automatic_attributes
401
+ # create a search class
402
+ search_class = Class.new(::SearchApi::Search::Base)
403
+
404
+ # define some search_accessors
405
+ search_class.class_eval do
406
+ model Searchable
407
+ end
408
+
409
+
410
+ # nil lower bound -> don't search (allow null values)
411
+ assert_equivalent_request search_class,
412
+ nil,
413
+ {:min_age => nil}
414
+
415
+ # valued lower bound
416
+ assert_equivalent_request search_class,
417
+ {:conditions => "age >= 50"},
418
+ {:min_age => 50}
419
+ end
420
+
421
+
422
+ def test_upper_bound_automatic_attributes
423
+ # create a search class
424
+ search_class = Class.new(::SearchApi::Search::Base)
425
+
426
+ # define some search_accessors
427
+ search_class.class_eval do
428
+ model Searchable
429
+ end
430
+
431
+
432
+ # nil upper bound -> don't search (allow null values)
433
+ assert_equivalent_request search_class,
434
+ nil,
435
+ {:max_age => nil}
436
+
437
+ # valued upper bound
438
+ assert_equivalent_request search_class,
439
+ {:conditions => "age <= 50"},
440
+ {:max_age => 50}
441
+ end
442
+
443
+
444
+ def test_block_defined_search
445
+ # create a search class
446
+ search_class = Class.new(::SearchApi::Search::Base)
447
+
448
+ # define some search_accessors
449
+ search_class.class_eval do
450
+ model Searchable
451
+ search_accessor :age_limit do |search|
452
+ { :conditions => ["age < ?", search.age_limit] }
453
+ end
454
+ end
455
+
456
+ assert_equivalent_request search_class,
457
+ {:conditions => "age < 50"},
458
+ {:age_limit => 50}
459
+ end
460
+
461
+
462
+ def test_valid_find_options
463
+ # create a search class
464
+ search_class = Class.new(::SearchApi::Search::Base)
465
+
466
+ # define some search_accessors
467
+ search_class.class_eval do
468
+ model Searchable
469
+ search_accessor :bad_hash do |search|
470
+ { :foo => :bar } # invalid key
471
+ end
472
+ end
473
+
474
+ assert_raise ArgumentError do
475
+ search_class.new(:bad_hash => true).find_options
476
+ end
477
+ end
478
+
479
+
480
+ protected
481
+
482
+ def assert_equivalent_request(search_class, traditional_find_options, search_options)
483
+ traditional_result_ids = Searchable.find(:all, traditional_find_options).map(&:id).sort
484
+ search_result_ids = Searchable.find(:all, search_class.new(search_options).find_options).map(&:id).sort
485
+ assert !traditional_result_ids.empty?
486
+ assert_equal traditional_result_ids, search_result_ids
487
+ end
488
+ end