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,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