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.
- data/MIT-LICENSE +20 -0
- data/Manifest.txt +25 -0
- data/README +98 -0
- data/Rakefile +50 -0
- data/db/migrate/001_create_searchable.rb +33 -0
- data/init.rb +25 -0
- data/install.rb +1 -0
- data/lib/search_api.rb +13 -0
- data/lib/search_api/active_record_bridge.rb +385 -0
- data/lib/search_api/active_record_integration.rb +272 -0
- data/lib/search_api/bridge.rb +75 -0
- data/lib/search_api/callbacks.rb +65 -0
- data/lib/search_api/errors.rb +11 -0
- data/lib/search_api/search.rb +473 -0
- data/lib/search_api/sql_fragment.rb +98 -0
- data/lib/search_api/text_criterion.rb +132 -0
- data/searchapi.gemspec +35 -0
- data/tasks/search_api_tasks.rake +8 -0
- data/test/active_record_bridge_test.rb +488 -0
- data/test/active_record_integration_test.rb +49 -0
- data/test/bridge_test.rb +69 -0
- data/test/callbacks_test.rb +157 -0
- data/test/mock_model.rb +54 -0
- data/test/search_test.rb +340 -0
- data/uninstall.rb +1 -0
- metadata +98 -0
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'search_api'
|
2
|
+
|
3
|
+
module SearchApi
|
4
|
+
|
5
|
+
# Module that holds SearchApi integration patterns.
|
6
|
+
module Integration
|
7
|
+
|
8
|
+
# Module that holds integration of SearchApi into ActiveRecord.
|
9
|
+
module ActiveRecord
|
10
|
+
|
11
|
+
# This module allows the ActiveRecord::Base classes to transparently
|
12
|
+
# integrate SearchApi::Search::Base features.
|
13
|
+
#
|
14
|
+
# It is included in a ActiveRecord::Base subclass by calling has_search_api:
|
15
|
+
#
|
16
|
+
# class People < ActiveRecord::Base
|
17
|
+
# has_search_api
|
18
|
+
#
|
19
|
+
# # define age search key
|
20
|
+
# search :age do |search|
|
21
|
+
# { :conditions => ['birth_date BETWEEN ? AND ?',
|
22
|
+
# (Date.today-search.age.years),
|
23
|
+
# (Date.today-(search.age-1).years+1.day)]}
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# People.find(:all, :conditions => {:first_name => 'Roger', :age => 30})
|
28
|
+
|
29
|
+
module Base
|
30
|
+
# Modifies the class including that module so that :find, :count and :with_scope
|
31
|
+
# methods have support for search keys added by the search method.
|
32
|
+
#
|
33
|
+
# <b>Don't include yourself this module !</b> Instead, use
|
34
|
+
# ActiveRecord::Base.has_search_api method.
|
35
|
+
def self.append_features(base)
|
36
|
+
super
|
37
|
+
base.alias_method_chain(:find, :search_support)
|
38
|
+
base.alias_method_chain(:count, :search_support)
|
39
|
+
base.alias_method_chain(:with_scope, :search_support)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Alteration of the :find method that has support for search keys added by the search method.
|
43
|
+
def find_with_search_support(*args)
|
44
|
+
options = if args.last.is_a?(Hash) then args.last else {} end
|
45
|
+
if options[:conditions].nil? || options[:conditions].is_a?(Hash)
|
46
|
+
send(:with_scope, :find => search_class.new(options.delete(:conditions)).find_options) do
|
47
|
+
find_without_search_support(*args)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
find_without_search_support(*args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Alteration of the :count method that has support for search keys added by the search method.
|
55
|
+
def count_with_search_support(*args)
|
56
|
+
options = if args.last.is_a?(Hash) then args.last else {} end
|
57
|
+
if options[:conditions].nil? || options[:conditions].is_a?(Hash)
|
58
|
+
send(:with_scope, :find => search_class.new(options.delete(:conditions)).find_options) do
|
59
|
+
count_without_search_support(*args)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
count_without_search_support(*args)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Alteration of the :with_scope method that has support for search keys added by the search method.
|
67
|
+
def with_scope_with_search_support(method_scoping = {}, action = :merge, &block)
|
68
|
+
if method_scoping[:find] && method_scoping[:find][:conditions] && method_scoping[:find][:conditions].is_a?(Hash)
|
69
|
+
with_scope_without_search_support(:find => search_class.new(method_scoping[:find].delete(:conditions)).find_options) do
|
70
|
+
with_scope_without_search_support(method_scoping, action, &block)
|
71
|
+
end
|
72
|
+
else
|
73
|
+
with_scope_without_search_support(method_scoping, action, &block)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Extends the keys that conditions hashes can hold.
|
78
|
+
#
|
79
|
+
# ActiveRecord::Base.find can take a <tt>:conditions</tt> option. This option
|
80
|
+
# can be raw SQL, a SQL fragment such as <tt>['a=?',1]</tt>, or a condition hash
|
81
|
+
# such as <tt>{:column1 => value, ;column2 => value}</tt>.
|
82
|
+
#
|
83
|
+
# <b>The search method allows you to extend the keys that condition hash
|
84
|
+
# can hold.</b>
|
85
|
+
#
|
86
|
+
# For instance, assuming a :birth_date column exists in your table, you
|
87
|
+
# can define the :age search key:
|
88
|
+
#
|
89
|
+
# class People < ActiveRecord::Base
|
90
|
+
# has_search_api
|
91
|
+
#
|
92
|
+
# # define age search key
|
93
|
+
# search :age do |search|
|
94
|
+
# { :conditions => ['birth_date BETWEEN ? AND ?',
|
95
|
+
# (Date.today-search.age.years),
|
96
|
+
# (Date.today-(search.age-1).years+1.day)]}
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# The options parameter allows you to define some search keys without
|
101
|
+
# providing a block:
|
102
|
+
#
|
103
|
+
# class People < ActiveRecord::Base
|
104
|
+
# has_search_api
|
105
|
+
#
|
106
|
+
# search :keyword, :operator => :full_text, :columns => [:first_name, :last_name, :email]
|
107
|
+
# search :email_domain, :operator => :ends_with, :column => :email
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# For further details, see:
|
111
|
+
# - how search attributes are defined: SearchApi::Search::Base.search_accessor;
|
112
|
+
# - which options are understood: SearchApi::Bridge::ActiveRecord#rewrite_search_attribute_builder method.
|
113
|
+
def search(name, options={}, &block)
|
114
|
+
search_class.search_accessor(name, options, &block)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
# This module allows the ActiveRecord::Base associations to transparently
|
120
|
+
# integrate SearchApi::Search::Base features.
|
121
|
+
#
|
122
|
+
# class People < ActiveRecord::Base
|
123
|
+
# belongs_to :company
|
124
|
+
# has_search_api
|
125
|
+
#
|
126
|
+
# # define age search key
|
127
|
+
# search :age do |search|
|
128
|
+
# { :conditions => ['birth_date BETWEEN ? AND ?',
|
129
|
+
# (Date.today-search.age.years),
|
130
|
+
# (Date.today-(search.age-1).years+1.day)]}
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# some_company.people.find(:all, :conditions => {:first_name => 'Roger', :age => 30})
|
135
|
+
module Associations
|
136
|
+
|
137
|
+
# Module that holds integration of SearchApi::Search::Base into
|
138
|
+
# ActiveRecord::Associations::HasManyAssociation,
|
139
|
+
# ActiveRecord::Associations::HasAndBelongsToManyAssociation, and
|
140
|
+
# ActiveRecord::Associations::HasManyThroughAssociation.
|
141
|
+
module Find
|
142
|
+
def self.append_features(base) #:nodoc:
|
143
|
+
super
|
144
|
+
base.alias_method_chain(:find, :search_support)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Alteration of the :find method that has support for search keys added by the search method.
|
148
|
+
def find_with_search_support(*args)
|
149
|
+
if @reflection.klass.respond_to?(:search_class)
|
150
|
+
options = if args.last.is_a?(Hash) then args.last else {} end
|
151
|
+
if options[:conditions].nil? || options[:conditions].is_a?(Hash)
|
152
|
+
@reflection.klass.send(:with_scope, :find => @reflection.klass.search_class.new(options.delete(:conditions)).find_options) do
|
153
|
+
find_without_search_support(*args)
|
154
|
+
end
|
155
|
+
else
|
156
|
+
find_without_search_support(*args)
|
157
|
+
end
|
158
|
+
else
|
159
|
+
find_without_search_support(*args)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Module that holds integration of SearchApi::Search::Base into ActiveRecord::Associations::HasManyAssociation.
|
165
|
+
module Count
|
166
|
+
def self.append_features(base) #:nodoc:
|
167
|
+
super
|
168
|
+
base.alias_method_chain(:count, :search_support)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Alteration of the :count method that has support for search keys added by the search method.
|
172
|
+
def count_with_search_support(*args)
|
173
|
+
if @reflection.klass.respond_to?(:search_class)
|
174
|
+
options = if args.last.is_a?(Hash) then args.last else {} end
|
175
|
+
if options[:conditions].nil? || options[:conditions].is_a?(Hash)
|
176
|
+
@reflection.klass.send(:with_scope, :find => @reflection.klass.search_class.new(options.delete(:conditions)).find_options) do
|
177
|
+
count_without_search_support(*args)
|
178
|
+
end
|
179
|
+
else
|
180
|
+
count_without_search_support(*args)
|
181
|
+
end
|
182
|
+
else
|
183
|
+
count_without_search_support(*args)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
class ActiveRecord::Base
|
194
|
+
class << self
|
195
|
+
|
196
|
+
# This method has following consequences:
|
197
|
+
#
|
198
|
+
# - The ActiveRecord::Base class is made searchable.
|
199
|
+
#
|
200
|
+
# Practically speaking, a SearchApi::Search::Base subclass that targets
|
201
|
+
# this model is created, prefilled with many automatic search keys
|
202
|
+
# (see SearchApi::Bridge::ActiveRecord).
|
203
|
+
#
|
204
|
+
# - The ActiveRecord::Base class is able to define its own condition hash keys.
|
205
|
+
#
|
206
|
+
# Practically speaking, the SearchApi::Integration::ActiveRecord::Base
|
207
|
+
# methods are included, and specifically its <tt>search</tt> method that allows to
|
208
|
+
# define custom keys for condition hashes.
|
209
|
+
#
|
210
|
+
# Example:
|
211
|
+
#
|
212
|
+
# class People < ActiveRecord::Base
|
213
|
+
# has_search_api
|
214
|
+
#
|
215
|
+
# # define age search key
|
216
|
+
# search :age do |search|
|
217
|
+
# { :conditions => ['birth_date BETWEEN ? AND ?',
|
218
|
+
# (Date.today-search.age.years),
|
219
|
+
# (Date.today-(search.age-1).years+1.day)]}
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# People.search_class # => the SearchApi::Search::Base subclass for People.
|
224
|
+
# People.find(:all, :conditions => { :age => 30 })
|
225
|
+
#
|
226
|
+
# Optional block is for advanced purpose only. It is executed as a
|
227
|
+
# <tt>class_eval</tt> block for the SearchApi::Search::Base subclass.
|
228
|
+
#
|
229
|
+
# class People < ActiveRecord::Base
|
230
|
+
# has_search_api do
|
231
|
+
# ...
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
def has_search_api(&block) # :yields:
|
235
|
+
# Creates a new SearchApi::Search::Base subclass
|
236
|
+
search_class = Class.new(::SearchApi::Search::Base)
|
237
|
+
|
238
|
+
# Tells the SearchApi::Search::Base subclass which models it searches in
|
239
|
+
search_class.model(self, :type_cast=>true)
|
240
|
+
|
241
|
+
# Let given block define search keys
|
242
|
+
search_class.class_eval(&block) if block
|
243
|
+
|
244
|
+
(class << self; self; end).instance_eval do
|
245
|
+
# The search_class method returns the SearchApi::Search::Base subclass.
|
246
|
+
define_method(:search_class) { search_class }
|
247
|
+
|
248
|
+
# Alter class behavior so that the SearchApi::Search::Base subclass seemlessly integrates.
|
249
|
+
include ::SearchApi::Integration::ActiveRecord::Base
|
250
|
+
end
|
251
|
+
|
252
|
+
nil # don't pollute class creation
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
# Modify associations behaviors
|
259
|
+
|
260
|
+
ActiveRecord::Associations::HasManyAssociation.module_eval do
|
261
|
+
include ::SearchApi::Integration::ActiveRecord::Associations::Find
|
262
|
+
include ::SearchApi::Integration::ActiveRecord::Associations::Count
|
263
|
+
end
|
264
|
+
|
265
|
+
ActiveRecord::Associations::HasAndBelongsToManyAssociation.module_eval do
|
266
|
+
include ::SearchApi::Integration::ActiveRecord::Associations::Find
|
267
|
+
end
|
268
|
+
|
269
|
+
ActiveRecord::Associations::HasManyThroughAssociation.module_eval do
|
270
|
+
include ::SearchApi::Integration::ActiveRecord::Associations::Find
|
271
|
+
end
|
272
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module SearchApi
|
2
|
+
|
3
|
+
# Module that holds SearchApi::Bridge::Base and its default subclasses.
|
4
|
+
module Bridge
|
5
|
+
|
6
|
+
# Base class for SearchApi bridges.
|
7
|
+
#
|
8
|
+
# Such a bridge have following responsabilities:
|
9
|
+
#
|
10
|
+
# - <b>predefining search attributes</b>, when the model of a SearchApi::Search::Base
|
11
|
+
# subclass is set.
|
12
|
+
#
|
13
|
+
# This is done by the automatic_search_attribute_builders method.
|
14
|
+
#
|
15
|
+
# - <b>rewriting SearchAttributeBuilder instances</b>.
|
16
|
+
#
|
17
|
+
# The SearchApi::Search::Base class defines a generic, dull, way to define search attributes.
|
18
|
+
#
|
19
|
+
# Precisely speaking, the SearchApi::Search::Base.add_search_attribute method,
|
20
|
+
# the one that actually defines search attributes, has strong
|
21
|
+
# requirements on its SearchAttributeBuilder parameter.
|
22
|
+
#
|
23
|
+
# A bridge is able to define a domain-specific way to define search
|
24
|
+
# attributes. This is the bridge's reponsability
|
25
|
+
# to translate these domain-specific SearchAttributeBuilder instances
|
26
|
+
# into strict SearchAttributeBuilder instances, that
|
27
|
+
# SearchApi::Search::Base.add_search_attribute can use.
|
28
|
+
#
|
29
|
+
# This is done by the rewrite_search_attribute_builder method.
|
30
|
+
#
|
31
|
+
# - <b>merging find options</b>, in order to build a single option from
|
32
|
+
# those built by several search attributes.
|
33
|
+
#
|
34
|
+
# This is done by the merge_find_options method.
|
35
|
+
|
36
|
+
class Base
|
37
|
+
|
38
|
+
# This method is called when a SearchApi::Search::Base's model is set, in order to
|
39
|
+
# predefine some relevant search keys.
|
40
|
+
#
|
41
|
+
# Returns an Array of SearchAttributeBuilder instances.
|
42
|
+
#
|
43
|
+
# Each builder can be used as an argument for SearchApi::Search::Base.search_accessor.
|
44
|
+
#
|
45
|
+
# Default SearchApi::Bridge::Base behavior is to return an empty Array: there is no
|
46
|
+
# automatic search attributes by default.
|
47
|
+
def automatic_search_attribute_builders(options)
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# This method is called when a SearchApi::Search::Base.search_accessor is
|
53
|
+
# called, to allow SearchApi::Bridge::Base subclasses to handle special builder options.
|
54
|
+
#
|
55
|
+
# Rewrites a SearchAttributeBuilder.
|
56
|
+
#
|
57
|
+
# On output, search_attribute_builder should be a valid
|
58
|
+
# SearchApi::Search::Base.add_search_attribute argument.
|
59
|
+
#
|
60
|
+
# Default SearchApi::Bridge::Base behavior is to leave the builder untouched.
|
61
|
+
#
|
62
|
+
# Subclasses may take the chance to use some specific options.
|
63
|
+
def rewrite_search_attribute_builder(search_attribute_builder)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# This methods returns a merge of options in options_array.
|
68
|
+
#
|
69
|
+
# Default SearchApi::Bridge::Base behavior is to raise NotImplementedError.
|
70
|
+
def merge_find_options(options_array)
|
71
|
+
raise NotImplementedError.new
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module SearchApi
|
2
|
+
module Search
|
3
|
+
module Callbacks
|
4
|
+
CALLBACKS = %w(before_find_options)
|
5
|
+
|
6
|
+
def self.append_features(base) #:nodoc:
|
7
|
+
super
|
8
|
+
|
9
|
+
base.class_eval do
|
10
|
+
%w(find_options).each do |method|
|
11
|
+
alias_method_chain method, :callbacks
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
CALLBACKS.each do |method|
|
16
|
+
base.class_eval <<-"end_eval"
|
17
|
+
def self.#{method}(*callbacks, &block)
|
18
|
+
callbacks << block if block_given?
|
19
|
+
write_inheritable_array(#{method.to_sym.inspect}, callbacks)
|
20
|
+
end
|
21
|
+
end_eval
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_options_with_callbacks
|
26
|
+
callback(:before_find_options)
|
27
|
+
find_options_without_callbacks
|
28
|
+
end
|
29
|
+
|
30
|
+
def before_find_options
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def callback(method)
|
37
|
+
callbacks_for(method).each do |callback|
|
38
|
+
result = case callback
|
39
|
+
when Symbol
|
40
|
+
self.send(callback)
|
41
|
+
when String
|
42
|
+
eval(callback, binding)
|
43
|
+
when Proc, Method
|
44
|
+
callback.call(self)
|
45
|
+
else
|
46
|
+
if callback.respond_to?(method)
|
47
|
+
callback.send(method, self)
|
48
|
+
else
|
49
|
+
raise SearchApiError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
|
50
|
+
end
|
51
|
+
end
|
52
|
+
return false if result == false
|
53
|
+
end
|
54
|
+
|
55
|
+
result = send(method) if respond_to?(method)
|
56
|
+
|
57
|
+
return result
|
58
|
+
end
|
59
|
+
|
60
|
+
def callbacks_for(method)
|
61
|
+
self.class.read_inheritable_attribute(method.to_sym) or []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# This modules provides a framework for encapsulating and performing searches.
|
2
|
+
#
|
3
|
+
# - SearchApi::Search module holds the abstract search features and behaviors.
|
4
|
+
# - SearchApi::Bridge module allows SearchApi::Search to apply to actual classes, and particularly to ActiveRecord::Base.
|
5
|
+
# - SearchApi::Integration makes ActiveRecord transparently use search features.
|
6
|
+
|
7
|
+
module SearchApi
|
8
|
+
|
9
|
+
# Base for all SearchApi errors.
|
10
|
+
class SearchApiError < StandardError; end
|
11
|
+
end
|
@@ -0,0 +1,473 @@
|
|
1
|
+
module SearchApi
|
2
|
+
|
3
|
+
# The module holds all SearchApi search features
|
4
|
+
|
5
|
+
module Search
|
6
|
+
|
7
|
+
# The SearchApi::Search::Base class is able to encapsulate search parameters for a given model.
|
8
|
+
#
|
9
|
+
# In order to search for instances of your model Stuff, you must:
|
10
|
+
#
|
11
|
+
# - ensure Stuff responds to <tt>search_api_bridge</tt> method.
|
12
|
+
# This is, by default, the case of ActiveRecord::Base subclasses.
|
13
|
+
#
|
14
|
+
# - define a subclass of SearchApi::Search::Base that uses Stuff as a model:
|
15
|
+
#
|
16
|
+
# class StuffSearch < SearchApi::Search::Base
|
17
|
+
# # search for Stuff
|
18
|
+
# model Stuff
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Assuming Stuff is an ActiveRecord::Base subclass, automatic search attributes
|
22
|
+
# are defined in StuffSearch. You can immediately use them:
|
23
|
+
#
|
24
|
+
# Those two statements are strictly equivalent:
|
25
|
+
#
|
26
|
+
# Stuff.find(:all, {:birth_date => Time.now})
|
27
|
+
# Stuff.find(:all, StuffSearch.new(:birth_date => Time.now).find_options)
|
28
|
+
#
|
29
|
+
# So far, so good. But that's not very funky.
|
30
|
+
#
|
31
|
+
# You can also define your own search attributes:
|
32
|
+
#
|
33
|
+
# class StuffSearch < SearchApi::Search::Base
|
34
|
+
# # search for Stuff
|
35
|
+
# model Stuff
|
36
|
+
# search_accessor :max_age do |search|
|
37
|
+
# { :conditions => ['birth_date > ?', Time.now - search.max_age.years]}
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# This allows you to perform searches on age:
|
42
|
+
#
|
43
|
+
# Stuff.find(:all, StuffSearch.new(:max_age => 20).find_options)
|
44
|
+
#
|
45
|
+
# You can mix search keys:
|
46
|
+
#
|
47
|
+
# Stuff.find(:all, StuffSearch.new(:max_age => 20, :sex => 'M').find_options)
|
48
|
+
class Base
|
49
|
+
|
50
|
+
VALID_SEARCH_ATTRIBUTE_OPTIONS = [ :store_as, :default ]
|
51
|
+
|
52
|
+
class << self
|
53
|
+
|
54
|
+
# Without any argument, returns the model of this SearchApi::Search::Base class.
|
55
|
+
#
|
56
|
+
# With a single argument, this method defines the model of this SearchApi::Search::Base class.
|
57
|
+
#
|
58
|
+
# The model must respond_to the <tt>search_api_bridge</tt> method,
|
59
|
+
# which should return an object that acts like SearchApi::Bridge::Base.
|
60
|
+
#
|
61
|
+
# The model can't be defined twice.
|
62
|
+
#
|
63
|
+
# Some automatic search accessors may be defined when the model is set.
|
64
|
+
# See:
|
65
|
+
# - Bridge::Base#automatic_search_attribute_builders
|
66
|
+
# - Bridge::ActiveRecord#automatic_search_attribute_builders
|
67
|
+
#
|
68
|
+
# Example:
|
69
|
+
# class StuffSearch < SearchApi::Search::Base
|
70
|
+
# # search for Stuff
|
71
|
+
# model Stuff
|
72
|
+
# ...
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# StuffSearch.model # => Stuff
|
76
|
+
def model(*args)
|
77
|
+
# returns model when no arguments
|
78
|
+
return @model if args.empty?
|
79
|
+
|
80
|
+
# can't set model twice
|
81
|
+
raise "model is already set" if @model
|
82
|
+
|
83
|
+
# fetch optional options
|
84
|
+
options = if args.last.is_a?(Hash) then args.pop else {} end
|
85
|
+
|
86
|
+
# make sure model is the only last argument
|
87
|
+
raise ArgumentError.new("Bad arguments for model") unless args.length == 1
|
88
|
+
|
89
|
+
|
90
|
+
model = args.first
|
91
|
+
|
92
|
+
# assert model responds_to search_api_bridge
|
93
|
+
raise ArgumentError.new("#{model} doesn't respond to search_api_bridge") unless model.respond_to?(:search_api_bridge)
|
94
|
+
|
95
|
+
# set model
|
96
|
+
@model = model
|
97
|
+
|
98
|
+
# infer automatics search accessors from model
|
99
|
+
add_automatic_search_attributes(options)
|
100
|
+
|
101
|
+
nil # don't pollute class creation
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# This is how you add search attributes to your SearchApi::Search::Base class.
|
106
|
+
#
|
107
|
+
# Adding a search attribute has the following consequences:
|
108
|
+
# - A writer, a reader, an interrogation reader, and a ignored reader are defined.
|
109
|
+
#
|
110
|
+
# Writer and reader act as usual. Interrogation reader acts as ActiveRecord::Base's one.
|
111
|
+
#
|
112
|
+
# Ignorer reader tells whether the search attribute is ignored or not.
|
113
|
+
#
|
114
|
+
# # Defines following StuffSearch instance methods:
|
115
|
+
# # - :a, :a=, :a? and :a_ignored?
|
116
|
+
# # - :b, :b=, :b? and :b_ignored?
|
117
|
+
# class StuffSearch < SearchApi::Search::Base
|
118
|
+
# model Stuff
|
119
|
+
# search_accessor :a, :b
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# - The method <tt>find_options_for_[search attribute]</tt> is defined,
|
123
|
+
# if block is provided.
|
124
|
+
#
|
125
|
+
# The optional block takes a single parameter: a SearchApi::Search::Base instance.
|
126
|
+
#
|
127
|
+
# Its result should be enough to define a model search.
|
128
|
+
#
|
129
|
+
# In case of ActiveRecord models, it should be a valid Hash that can be used as
|
130
|
+
# ActiveRecord::Base.find argument.
|
131
|
+
#
|
132
|
+
# Example:
|
133
|
+
# class StuffSearch < SearchApi::Search::Base
|
134
|
+
# model Stuff
|
135
|
+
# search_accessor :max_age do |search|
|
136
|
+
# { :conditions => ['birth_date > ?', Time.now - search.max_age.years]}
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# You can avoid passing a block, and define the <tt>find_options_for_[search attribute]</tt>
|
141
|
+
# method later:
|
142
|
+
#
|
143
|
+
# class StuffSearch < SearchApi::Search::Base
|
144
|
+
# model Stuff
|
145
|
+
# search_accessor :max_age
|
146
|
+
# def find_options_for_max_age
|
147
|
+
# { :conditions => ['birth_date > ?', Time.now - max_age.years]}
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
|
151
|
+
def search_accessor(*args, &block)
|
152
|
+
|
153
|
+
# extract SearchAttributeBuilder instances from arguments
|
154
|
+
|
155
|
+
search_attributes_builders = if block.nil? && args.length == 1 && args.first.is_a?(SearchAttributeBuilder)
|
156
|
+
# argument is a single SearchAttributeBuilder instance
|
157
|
+
|
158
|
+
args
|
159
|
+
|
160
|
+
else
|
161
|
+
# arguments are search attribute names and options
|
162
|
+
|
163
|
+
options = if args.last.is_a?(Hash) then args.pop else {} end
|
164
|
+
args.map do |search_attribute|
|
165
|
+
SearchAttributeBuilder.new(search_attribute, options, &block)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# define search attributes from builders
|
171
|
+
|
172
|
+
search_attributes_builders.each do |builder|
|
173
|
+
rewrite_search_attribute_builder(builder)
|
174
|
+
add_search_attribute(builder)
|
175
|
+
end
|
176
|
+
|
177
|
+
nil # don't pollute class creation
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# Returns an unordered Array of all search attributes defined through search_accessor.
|
182
|
+
#
|
183
|
+
# Example:
|
184
|
+
# class StuffSearch < SearchApi::Search::Base
|
185
|
+
# search_accessor :search_key1, :search_key2
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# StuffSearch.search_attributes # => [:search_key1, :search_key2]
|
189
|
+
def search_attributes
|
190
|
+
read_inheritable_attribute(:search_attributes) || []
|
191
|
+
end
|
192
|
+
|
193
|
+
protected
|
194
|
+
|
195
|
+
# <b>Unless you're an SearchApi::Bridge::Base subclass designer, you should use
|
196
|
+
# search_accessor method instead.</b>
|
197
|
+
#
|
198
|
+
# search_attribute_builder is a SearchAttributeBuilder instance.
|
199
|
+
#
|
200
|
+
# This methods adds a search attribute to that SearchApi::Search::Base class:
|
201
|
+
# - search_attribute_builder.name is the name of the search attribute,
|
202
|
+
# - search_attribute_builder.options are options for defining the
|
203
|
+
# search attribute,
|
204
|
+
# - search_attribute_builder.block is an optional proc that implement
|
205
|
+
# the search attribute behavior.
|
206
|
+
#
|
207
|
+
# search_attribute_builder.options keys must be in VALID_SEARCH_ATTRIBUTE_OPTIONS.
|
208
|
+
#
|
209
|
+
# Adding a search attributes, precisely, means:
|
210
|
+
#
|
211
|
+
# - A writer, a reader, an interrogation reader, and a ignored reader are defined.
|
212
|
+
#
|
213
|
+
# Writer and reader act as usual. Interrogation reader acts as ActiveRecord::Base's one.
|
214
|
+
#
|
215
|
+
# Ignorer reader tells whether the search attribute is ignored or not.
|
216
|
+
#
|
217
|
+
# # Defines following StuffSearch instance methods:
|
218
|
+
# # - :a,
|
219
|
+
# # - :a=
|
220
|
+
# # - :a?
|
221
|
+
# # - :a_ignored?
|
222
|
+
# class StuffSearch < SearchApi::Search::Base
|
223
|
+
# model Stuff
|
224
|
+
# add_search_attribute(SearchApi::Search::SearchAttributeBuilder.new(:a))
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# - The method <tt>find_options_for_[search attribute]</tt> is defined,
|
228
|
+
# if the builder's block is set.
|
229
|
+
#
|
230
|
+
# That block takes a single parameter: a SearchApi::Search::Base instance. Its result
|
231
|
+
# should be enough to define a model search.
|
232
|
+
#
|
233
|
+
# In case of ActiveRecord models, it should be a valid Hash that can be used as
|
234
|
+
# ActiveRecord::Base.find argument.
|
235
|
+
#
|
236
|
+
# # Defines following StuffSearch instance methods:
|
237
|
+
# # - :a,
|
238
|
+
# # - :a=
|
239
|
+
# # - :a?
|
240
|
+
# # - :a_ignored?
|
241
|
+
# # - :find_options_for_a
|
242
|
+
# class StuffSearch < SearchApi::Search::Base
|
243
|
+
# model Stuff
|
244
|
+
# add_search_attribute(SearchApi::Search::SearchAttributeBuilder.new(:max_age)) do |search|
|
245
|
+
# { :conditions => ['birth_date > ?', Time.now - search.max_age.years]}
|
246
|
+
# end
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# You can avoid defining the builder's block, and define the
|
250
|
+
# <tt>find_options_for_[search attribute]</tt> method yourself.
|
251
|
+
#
|
252
|
+
# class StuffSearch < SearchApi::Search::Base
|
253
|
+
# model Stuff
|
254
|
+
# add_search_attribute(SearchApi::Search::SearchAttributeBuilder.new(:max_age))
|
255
|
+
# def find_options_for_max_age
|
256
|
+
# { :conditions => ['birth_date > ?', Time.now - max_age.years]}
|
257
|
+
# end
|
258
|
+
# end
|
259
|
+
def add_search_attribute(search_attribute_builder)
|
260
|
+
search_attribute = search_attribute_builder.name
|
261
|
+
options = search_attribute_builder.options
|
262
|
+
block = search_attribute_builder.block
|
263
|
+
|
264
|
+
|
265
|
+
# check options
|
266
|
+
options ||= {}
|
267
|
+
invalid_options = options.keys - VALID_SEARCH_ATTRIBUTE_OPTIONS
|
268
|
+
raise ArgumentError.new("invalid options #{invalid_options.inspect}") unless invalid_options.empty?
|
269
|
+
|
270
|
+
|
271
|
+
# fill search_attributes array
|
272
|
+
write_inheritable_array(:search_attributes, [search_attribute])
|
273
|
+
|
274
|
+
|
275
|
+
# store default value
|
276
|
+
options[:default] ||= SearchApi::Search.ignored
|
277
|
+
write_inheritable_hash(:search_attribute_default_values, { search_attribute => options[:default]})
|
278
|
+
|
279
|
+
|
280
|
+
# define reader
|
281
|
+
attr_reader search_attribute
|
282
|
+
|
283
|
+
|
284
|
+
# define writer
|
285
|
+
if store_as_proc = options[:store_as]
|
286
|
+
define_method("#{search_attribute}=") do |value|
|
287
|
+
instance_variable_set("@#{search_attribute}", if SearchApi::Search.ignored?(value) then value else store_as_proc.call(value) end)
|
288
|
+
end
|
289
|
+
else
|
290
|
+
attr_writer search_attribute
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
# define interrogation reader
|
295
|
+
define_method("#{search_attribute}?") do
|
296
|
+
!!send(search_attribute)
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
# define ignored reader
|
301
|
+
define_method("#{search_attribute}_ignored?") do
|
302
|
+
SearchApi::Search.ignored?(send(search_attribute))
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
# Define find_options_for_xxx method
|
307
|
+
if block
|
308
|
+
# user-defined method
|
309
|
+
define_method("find_options_for_#{search_attribute}") do
|
310
|
+
if SearchApi::Search.ignored?(send(search_attribute))
|
311
|
+
{}
|
312
|
+
else
|
313
|
+
block.call(self)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
private
|
321
|
+
|
322
|
+
# Adds search accessors for automatic attributes
|
323
|
+
def add_automatic_search_attributes(options) #:nodoc:
|
324
|
+
# define a search attribute for each automatic search attribute
|
325
|
+
model.
|
326
|
+
search_api_bridge.
|
327
|
+
automatic_search_attribute_builders(options).
|
328
|
+
each do |builder|
|
329
|
+
search_accessor(builder)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
# Rewrites a SearchAttributeBuilder.
|
335
|
+
#
|
336
|
+
# On output, search_attribute_builder should be a valid
|
337
|
+
# SearchApi::Search::Base.add_search_attribute argument.
|
338
|
+
def rewrite_search_attribute_builder(search_attribute_builder)
|
339
|
+
model.search_api_bridge.rewrite_search_attribute_builder(search_attribute_builder) if model
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
# Initializes a search with a search attributes Hash.
|
347
|
+
def initialize(attributes=nil)
|
348
|
+
raise "Can't create an instance without model" if self.class.model.nil?
|
349
|
+
|
350
|
+
# initialize attributes with ignored value
|
351
|
+
self.attributes = self.class.search_attributes.inject((attributes || {}).dup) do |attributes, search_attribute|
|
352
|
+
if attributes.has_key?(search_attribute) || attributes.has_key?(search_attribute.to_s)
|
353
|
+
attributes
|
354
|
+
else
|
355
|
+
attributes.update(search_attribute => self.class.read_inheritable_attribute(:search_attribute_default_values)[search_attribute])
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
|
361
|
+
|
362
|
+
# Returns a Hash of search attributes.
|
363
|
+
def attributes
|
364
|
+
self.class.search_attributes.inject({}) do |attributes, search_attribute|
|
365
|
+
attributes.update(search_attribute => send(search_attribute))
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Sets search attributes via a Hash
|
370
|
+
def attributes=(attributes=nil)
|
371
|
+
(attributes || {}).each do |search_attribute, value|
|
372
|
+
send("#{search_attribute}=", value)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# Returns whether search_attribute is ignored.
|
378
|
+
def ignored?(search_attribute)
|
379
|
+
SearchApi::Search.ignored?(send(search_attribute))
|
380
|
+
end
|
381
|
+
|
382
|
+
# Ignore given search_attribute.
|
383
|
+
def ignore!(search_attribute)
|
384
|
+
send("#{search_attribute}=", SearchApi::Search.ignored)
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
|
389
|
+
# Returns an object that should be enough to define a model search.
|
390
|
+
#
|
391
|
+
# In case of ActiveRecord models, returns a valid Hash that can be used as
|
392
|
+
# ActiveRecord::Base.find argument.
|
393
|
+
def find_options
|
394
|
+
# collect all find_options for not ignored attributes
|
395
|
+
|
396
|
+
options_array = self.class.search_attributes.
|
397
|
+
|
398
|
+
# reject ignored attributes
|
399
|
+
reject { |search_attribute| ignored?(search_attribute) }.
|
400
|
+
|
401
|
+
# merge options for all attributes
|
402
|
+
map { |search_attribute| send("find_options_for_#{search_attribute}") }
|
403
|
+
|
404
|
+
|
405
|
+
# merge them options for not ignored attributes
|
406
|
+
|
407
|
+
self.class.model.search_api_bridge.merge_find_options(options_array)
|
408
|
+
end
|
409
|
+
|
410
|
+
|
411
|
+
protected
|
412
|
+
|
413
|
+
def find_options_for_hash(conditions) #:nodoc:
|
414
|
+
# merge options for not ignored attributes
|
415
|
+
model.
|
416
|
+
search_api_bridge.
|
417
|
+
merge_find_options(
|
418
|
+
)
|
419
|
+
conditions.
|
420
|
+
|
421
|
+
# reject ignored attributes
|
422
|
+
reject { |search_attribute, value| SearchApi::Search.ignored?(value) }.
|
423
|
+
|
424
|
+
# merge options for all attributes
|
425
|
+
map do |search_attribute, value|
|
426
|
+
send("find_options_for_#{search_attribute}")
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
protected
|
432
|
+
|
433
|
+
# Describes a search attribute with:
|
434
|
+
# - a name,
|
435
|
+
# - some options
|
436
|
+
# - an optional block
|
437
|
+
class SearchAttributeBuilder
|
438
|
+
# name is a search attribute name. It is read-only so that SearchApi::Bridge::Base.rewrite_search_attribute_builder is unable to rename attributes.
|
439
|
+
attr_reader :name
|
440
|
+
|
441
|
+
# options is an options Hash (never nil, may be empty Hash)
|
442
|
+
attr_accessor :options
|
443
|
+
|
444
|
+
# block is an optional proc (may be nil)
|
445
|
+
attr_accessor :block
|
446
|
+
|
447
|
+
def initialize(name, options={}, &block)
|
448
|
+
@name = name.to_sym
|
449
|
+
@options = options
|
450
|
+
@block = block
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Class of values ignored by Search instances.
|
455
|
+
class IgnoredValue
|
456
|
+
def inspect #:nodoc:
|
457
|
+
"<ignored>"
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
class << self
|
462
|
+
|
463
|
+
def ignored #:nodoc:
|
464
|
+
@ignore ||= IgnoredValue.new
|
465
|
+
end
|
466
|
+
|
467
|
+
def ignored?(value) #:nodoc:
|
468
|
+
value.is_a? IgnoredValue
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
end
|
473
|
+
end
|