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