albanpeignier-searchapi 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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