rd_searchlogic 3.0.0.rc

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.
Files changed (34) hide show
  1. data/.gitignore +7 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +308 -0
  4. data/Rakefile +42 -0
  5. data/VERSION.yml +5 -0
  6. data/init.rb +1 -0
  7. data/lib/searchlogic/active_record/association_proxy.rb +19 -0
  8. data/lib/searchlogic/active_record/consistency.rb +49 -0
  9. data/lib/searchlogic/active_record/named_scope_tools.rb +102 -0
  10. data/lib/searchlogic/core_ext/object.rb +43 -0
  11. data/lib/searchlogic/core_ext/proc.rb +17 -0
  12. data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
  13. data/lib/searchlogic/named_scopes/association_conditions.rb +163 -0
  14. data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
  15. data/lib/searchlogic/named_scopes/conditions.rb +232 -0
  16. data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
  17. data/lib/searchlogic/named_scopes/ordering.rb +74 -0
  18. data/lib/searchlogic/rails_helpers.rb +79 -0
  19. data/lib/searchlogic/search.rb +259 -0
  20. data/lib/searchlogic.rb +89 -0
  21. data/rails/init.rb +1 -0
  22. data/spec/searchlogic/active_record/association_proxy_spec.rb +23 -0
  23. data/spec/searchlogic/active_record/consistency_spec.rb +28 -0
  24. data/spec/searchlogic/core_ext/object_spec.rb +9 -0
  25. data/spec/searchlogic/core_ext/proc_spec.rb +8 -0
  26. data/spec/searchlogic/named_scopes/alias_scope_spec.rb +23 -0
  27. data/spec/searchlogic/named_scopes/association_conditions_spec.rb +221 -0
  28. data/spec/searchlogic/named_scopes/association_ordering_spec.rb +29 -0
  29. data/spec/searchlogic/named_scopes/conditions_spec.rb +321 -0
  30. data/spec/searchlogic/named_scopes/or_conditions_spec.rb +66 -0
  31. data/spec/searchlogic/named_scopes/ordering_spec.rb +34 -0
  32. data/spec/searchlogic/search_spec.rb +459 -0
  33. data/spec/spec_helper.rb +146 -0
  34. metadata +123 -0
@@ -0,0 +1,67 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Adds the ability to create alias scopes that allow you to alias a named
4
+ # scope or create a named scope procedure. See the alias_scope method for a more
5
+ # detailed explanation.
6
+ module AliasScope
7
+ # In some instances you might create a class method that essentially aliases a named scope
8
+ # or represents a named scope procedure. Ex:
9
+ #
10
+ # class User
11
+ # def teenager
12
+ # age_gte(13).age_lte(19)
13
+ # end
14
+ # end
15
+ #
16
+ # This is obviously a very basic example, but notice how we are utilizing already existing named
17
+ # scopes so that we do not have to repeat ourself. This method makes a lot more sense when you are
18
+ # dealing with complicated named scope.
19
+ #
20
+ # There is a problem though. What if you want to use this in your controller's via the 'search' method:
21
+ #
22
+ # User.search(:teenager => true)
23
+ #
24
+ # You would expect that to work, but how does Searchlogic::Search tell the difference between your
25
+ # 'teenager' method and the 'destroy_all' method. It can't, there is no way to tell unless we actually
26
+ # call the method, which we obviously can not do.
27
+ #
28
+ # The being said, we need a way to tell searchlogic that this is method is safe. Here's how you do that:
29
+ #
30
+ # User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
31
+ #
32
+ # This feels better, it feels like our other scopes, and it provides a way to tell Searchlogic that this
33
+ # is a safe method.
34
+ def alias_scope(name, options = nil)
35
+ alias_scopes[name.to_sym] = options
36
+ (class << self; self end).instance_eval do
37
+ define_method name do |*args|
38
+ case options
39
+ when Symbol
40
+ send(options)
41
+ else
42
+ options.call(*args)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ alias_method :scope_procedure, :alias_scope
48
+
49
+ def alias_scopes # :nodoc:
50
+ read_inheritable_attribute(:alias_scopes) || write_inheritable_attribute(:alias_scopes, {})
51
+ end
52
+
53
+ def alias_scope?(name) # :nodoc:
54
+ return false if name.blank?
55
+ alias_scopes.key?(name.to_sym)
56
+ end
57
+
58
+ def condition?(name) # :nodoc:
59
+ super || alias_scope?(name)
60
+ end
61
+
62
+ def named_scope_options(name) # :nodoc:
63
+ super || alias_scopes[name.to_sym]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,163 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for associations. See the README for a detailed explanation.
4
+ module AssociationConditions
5
+ def condition?(name) # :nodoc:
6
+ super || association_condition?(name)
7
+ end
8
+
9
+ def _resolve_deep_association_conditions(condition_name, args)
10
+ if local_condition?(condition_name)
11
+ {:joins=>nil, :klass=>self, :condition=>condition_name}
12
+ elsif details = association_condition_details(condition_name)
13
+ result = details[:association].klass._resolve_deep_association_conditions(details[:condition], args)
14
+ return nil unless result #condition method did not resolve
15
+ this_association = details[:association].name
16
+ join_condition = result[:joins].nil? ? this_association : {this_association=>result[:joins]}
17
+ result.merge(:joins=>join_condition)
18
+ else #this condition method did not resolve
19
+ nil
20
+ end
21
+ end
22
+
23
+
24
+
25
+ private
26
+ def association_condition?(name)
27
+ !association_condition_details(name).nil? unless name.to_s.downcase.match("_or_")
28
+ end
29
+
30
+ def method_missing(name, *args, &block)
31
+ if !local_condition?(name) && details = association_condition_details(name)
32
+ create_scope_for_association(details[:association], details[:condition], args, details[:poly_class])
33
+ send(name, *args)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def create_scope_for_association(association, condition_name, args, poly_class = nil)
40
+ result = (poly_class || association.klass)._resolve_deep_association_conditions(condition_name, args)
41
+ return unless result
42
+
43
+ joins_result = result[:joins].nil? ? association.name : {association.name => result[:joins]}
44
+
45
+ lambda_containing_relational_algebra = eval <<-"end_eval"
46
+ lambda { |*args|
47
+ #{result[:klass].name}.#{result[:condition]}(*args).joins(#{joins_result.inspect})
48
+ }
49
+ end_eval
50
+
51
+ name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
52
+ scope(name, lambda_containing_relational_algebra)
53
+ end
54
+
55
+ def association_condition_details(name, last_condition = nil)
56
+ non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
57
+ poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
58
+ return nil if non_poly_assocs.empty? && poly_assocs.empty?
59
+
60
+ name_with_condition = [name, last_condition].compact.join('_')
61
+
62
+ association_name = nil
63
+ poly_type = nil
64
+ condition = nil
65
+
66
+ if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/
67
+ association_name = $1
68
+ condition = $2
69
+ elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+?)_type_(\w+)$/
70
+ association_name = $1
71
+ poly_type = $2
72
+ condition = $3
73
+ end
74
+
75
+ if association_name && condition
76
+ association = reflect_on_association(association_name.to_sym)
77
+ klass = poly_type ? poly_type.camelcase.constantize : association.klass
78
+ if klass.condition?(condition)
79
+ {:association => association, :poly_class => poly_type && klass, :condition => condition}
80
+ else
81
+ nil
82
+ end
83
+ end
84
+ end
85
+
86
+
87
+ # def association_condition_options(association, association_condition, args, poly_class = nil)
88
+ # klass = poly_class ? poly_class : association.klass
89
+ # relation = klass.send(association_condition, *args)
90
+ # scope_options = nil #klass.named_scope_options(association_condition)
91
+ # arity = -1 #klass.named_scope_arity(association_condition)
92
+ #
93
+ # if !arity || arity == 0
94
+ # # The underlying condition doesn't require any parameters, so let's just create a simple
95
+ # # named scope that is based on a hash.
96
+ # options = {}
97
+ # in_searchlogic_delegation { options = relation.scope(:find) }
98
+ # prepare_named_scope_options(options, association, poly_class)
99
+ # options
100
+ # else
101
+ # proc_args = arity_args(arity)
102
+ # arg_type = :string #(scope_options.respond_to?(:searchlogic_options) && scope_options.searchlogic_options[:type]) || :string
103
+ #
104
+ # eval <<-"end_eval"
105
+ # searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
106
+ # options = {}
107
+ #
108
+ # in_searchlogic_delegation do
109
+ # relation = klass.send(association_condition, #{proc_args.join(",")})
110
+ # options = {:conditions=>"users.username LIKE '%joe%'"} #relation.scope(:find) if relation
111
+ # end
112
+ #
113
+ # prepare_named_scope_options(options, association, poly_class)
114
+ # options
115
+ # }
116
+ # end_eval
117
+ # end
118
+ # end
119
+
120
+ # Used to match the new scopes parameters to the underlying scope. This way we can disguise the
121
+ # new scope as best as possible instead of taking the easy way out and using *args.
122
+ # def arity_args(arity)
123
+ # args = []
124
+ # if arity > 0
125
+ # arity.times { |i| args << "arg#{i}" }
126
+ # else
127
+ # positive_arity = arity * -1
128
+ # positive_arity.times do |i|
129
+ # if i == (positive_arity - 1)
130
+ # args << "*arg#{i}"
131
+ # else
132
+ # args << "arg#{i}"
133
+ # end
134
+ # end
135
+ # end
136
+ # args
137
+ # end
138
+
139
+ # #ADDED: this was removed from AR::Base ver2.x, redefined for use in prepare_named_scope_options
140
+ # def array_of_strings?(o)
141
+ # o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
142
+ # end
143
+ #
144
+ # def prepare_named_scope_options(options, association, poly_class = nil)
145
+ # options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
146
+ #
147
+ # klass = poly_class || association.klass
148
+ # # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
149
+ # options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
150
+ #
151
+ # poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
152
+ #
153
+ # if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
154
+ # options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
155
+ # elsif poly_class
156
+ # options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
157
+ # else
158
+ # options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
159
+ # end
160
+ # end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,44 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating order named scopes for associations:
4
+ #
5
+ # User.has_many :orders
6
+ # Order.has_many :line_items
7
+ # LineItem
8
+ #
9
+ # User.ascend_by_orders_line_items_id
10
+ #
11
+ # See the README for a more detailed explanation.
12
+ module AssociationOrdering
13
+ def condition?(name) # :nodoc:
14
+ super || association_ordering_condition?(name)
15
+ end
16
+
17
+ private
18
+ def association_ordering_condition?(name)
19
+ !association_ordering_condition_details(name).nil?
20
+ end
21
+
22
+ def method_missing(name, *args, &block)
23
+ if details = association_ordering_condition_details(name)
24
+ create_association_ordering_condition(details[:association], details[:order_as], details[:condition], args)
25
+ send(name, *args)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def association_ordering_condition_details(name)
32
+ associations = reflect_on_all_associations
33
+ association_names = associations.collect { |assoc| assoc.name }
34
+ if name.to_s =~ /^(ascend|descend)_by_(#{association_names.join("|")})_(\w+)$/
35
+ {:order_as => $1, :association => associations.find { |a| a.name == $2.to_sym }, :condition => $3}
36
+ end
37
+ end
38
+
39
+ def create_association_ordering_condition(association, order_as, condition, args)
40
+ scope("#{order_as}_by_#{association.name}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,232 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for columns. It allows you to do things like:
4
+ #
5
+ # User.first_name_like("ben")
6
+ # User.id_lt(10)
7
+ #
8
+ # Notice the constants in this class, they define which conditions Searchlogic provides.
9
+ #
10
+ # See the README for a more detailed explanation.
11
+ module Conditions
12
+ COMPARISON_CONDITIONS = {
13
+ :equals => [:is, :eq],
14
+ :does_not_equal => [:not_equal_to, :is_not, :not, :ne],
15
+ :less_than => [:lt, :before],
16
+ :less_than_or_equal_to => [:lte],
17
+ :greater_than => [:gt, :after],
18
+ :greater_than_or_equal_to => [:gte],
19
+ }
20
+
21
+ WILDCARD_CONDITIONS = {
22
+ :like => [:contains, :includes],
23
+ :not_like => [:does_not_include],
24
+ :begins_with => [:bw],
25
+ :not_begin_with => [:does_not_begin_with],
26
+ :ends_with => [:ew],
27
+ :not_end_with => [:does_not_end_with]
28
+ }
29
+
30
+ BOOLEAN_CONDITIONS = {
31
+ :null => [:nil],
32
+ :not_null => [:not_nil],
33
+ :empty => [],
34
+ :blank => [],
35
+ :not_blank => [:present]
36
+ }
37
+
38
+ CONDITIONS = {}
39
+
40
+ # Add any / all variations to every comparison and wildcard condition
41
+ COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
42
+ CONDITIONS[condition] = aliases
43
+ CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
44
+ CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
45
+ end
46
+
47
+ CONDITIONS[:equals_any] = CONDITIONS[:equals_any] + [:in]
48
+ CONDITIONS[:does_not_equal_all] = CONDITIONS[:does_not_equal_all] + [:not_in]
49
+
50
+ BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
51
+
52
+ PRIMARY_CONDITIONS = CONDITIONS.keys
53
+ ALIAS_CONDITIONS = CONDITIONS.values.flatten
54
+
55
+ def options
56
+ @options
57
+ end
58
+
59
+ # Is the name of the method a valid condition that can be dynamically created?
60
+ def condition?(name)
61
+ local_condition?(name)
62
+ end
63
+
64
+ private
65
+ def local_condition?(name)
66
+ return false if name.blank?
67
+ scope_names = scopes.keys.reject { |k| k == :scoped }
68
+ scope_names.include?(name.to_sym) || !condition_details(name).nil? || boolean_condition?(name)
69
+ end
70
+
71
+ def boolean_condition?(name)
72
+ column = columns_hash[name.to_s] || columns_hash[name.to_s.gsub(/^not_/, "")]
73
+ column && column.type == :boolean
74
+ end
75
+
76
+ def method_missing(name, *args, &block)
77
+ if details = condition_details(name)
78
+ create_scope(details[:column], details[:condition], args)
79
+ send(name, *args)
80
+ elsif boolean_condition?(name)
81
+ column = name.to_s.gsub(/^not_/, "")
82
+ scope name, where(column.to_sym => (name.to_s =~ /^not_/).nil?) #:conditions => {column => (name.to_s =~ /^not_/).nil?}
83
+ send(name)
84
+ else
85
+ super
86
+ end
87
+ end
88
+
89
+
90
+ def condition_details(method_name)
91
+ #TODO: cache these
92
+ column_name_matcher = column_names.join("|")
93
+ conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")
94
+
95
+ if method_name.to_s =~ /^(#{column_name_matcher})_(#{conditions_matcher})$/
96
+ {:column => $1, :condition => $2}
97
+ end
98
+ end
99
+
100
+ def create_scope(column, condition, args)
101
+ if PRIMARY_CONDITIONS.include?(condition.to_sym)
102
+ create_primary_scope(column, condition)
103
+ elsif ALIAS_CONDITIONS.include?(condition.to_sym)
104
+ create_aliased_scope(column, condition, args)
105
+ end
106
+ end
107
+
108
+ def create_primary_scope(column, condition)
109
+ column_type = columns_hash[column.to_s].type
110
+ skip_conversion = skip_time_zone_conversion_for_attributes.include?(columns_hash[column.to_s].name.to_sym)
111
+ match_keyword = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
112
+
113
+ scope_options = case condition.to_s
114
+ when /^equals/
115
+ #scope_options(condition, column_type, lambda { |a| attribute_condition("#{table_name}.#{column}", a) }, :skip_conversion => skip_conversion)
116
+ scope_options(condition, column_type, "#{table_name}.#{column} = ?", :skip_conversion => skip_conversion)
117
+ when /^does_not_equal/
118
+ scope_options(condition, column_type, "#{table_name}.#{column} != ?", :skip_conversion => skip_conversion)
119
+ when /^less_than_or_equal_to/
120
+ scope_options(condition, column_type, "#{table_name}.#{column} <= ?", :skip_conversion => skip_conversion)
121
+ when /^less_than/
122
+ scope_options(condition, column_type, "#{table_name}.#{column} < ?", :skip_conversion => skip_conversion)
123
+ when /^greater_than_or_equal_to/
124
+ scope_options(condition, column_type, "#{table_name}.#{column} >= ?", :skip_conversion => skip_conversion)
125
+ when /^greater_than/
126
+ scope_options(condition, column_type, "#{table_name}.#{column} > ?", :skip_conversion => skip_conversion)
127
+ when /^like/
128
+ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
129
+ when /^not_like/
130
+ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
131
+ when /^begins_with/
132
+ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
133
+ when /^not_begin_with/
134
+ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
135
+ when /^ends_with/
136
+ scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
137
+ when /^not_end_with/
138
+ scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
139
+ when "null"
140
+ lambda { where("#{table_name}.#{column} IS NULL")}
141
+ when "not_null"
142
+ lambda { where("#{table_name}.#{column} IS NOT NULL")}
143
+ when "empty"
144
+ lambda { where("#{table_name}.#{column} = ''")}
145
+ when "blank"
146
+ lambda { where("#{table_name}.#{column} = '' OR #{table_name}.#{column} IS NULL")}
147
+ when "not_blank"
148
+ lambda { where("#{table_name}.#{column} != '' AND #{table_name}.#{column} IS NOT NULL")}
149
+ end
150
+
151
+ scope("#{column}_#{condition}".to_sym, scope_options)
152
+ end
153
+
154
+ # This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
155
+ # Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
156
+ # fast as possible, which is why I didn't do the case statement inside of the lambda.
157
+ def scope_options(condition, column_type, sql, options = {})
158
+ case condition.to_s
159
+ when /_(any|all)$/
160
+ searchlogic_lambda(column_type, :skip_conversion => options[:skip_conversion]) { |*values|
161
+ unless values.empty?
162
+ values.flatten!
163
+ values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
164
+
165
+ join = $1 == "any" ? " OR " : " AND "
166
+
167
+ scope_sql = values.collect { |value| sql.is_a?(Proc) ? sql.call(value) : sql }.join(join)
168
+
169
+ lambda { where [scope_sql, *expand_range_bind_variables(values)]}
170
+ else
171
+ {}
172
+ end
173
+ }
174
+ else
175
+ searchlogic_lambda(column_type, :skip_conversion => options[:skip_conversion]) { |*values|
176
+ values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
177
+
178
+ scope_sql = sql.is_a?(Proc) ? sql.call(*values) : sql
179
+
180
+ {:conditions => [scope_sql, *expand_range_bind_variables(values)]}
181
+ }
182
+ end
183
+ end
184
+
185
+ def value_with_modifier(value, modifier)
186
+ case modifier
187
+ when :like
188
+ "%#{value}%"
189
+ when :begins_with
190
+ "#{value}%"
191
+ when :ends_with
192
+ "%#{value}"
193
+ else
194
+ value
195
+ end
196
+ end
197
+
198
+ def create_aliased_scope(column, condition, args)
199
+ primary_condition = primary_condition(condition)
200
+ alias_name = "#{column}_#{condition}"
201
+ primary_name = "#{column}_#{primary_condition}"
202
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
203
+ (class << self; self; end).class_eval { alias_method alias_name, primary_name }
204
+ end
205
+
206
+ # Returns the primary condition for the given alias. Ex:
207
+ #
208
+ # primary_condition(:gt) => :greater_than
209
+ def primary_condition(alias_condition)
210
+ CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
211
+ end
212
+
213
+ # Returns the primary name for any condition on a column. You can pass it
214
+ # a primary condition, alias condition, etc, and it will return the proper
215
+ # primary condition name. This helps simply logic throughout Searchlogic. Ex:
216
+ #
217
+ # condition_scope_name(:id_gt) => :id_greater_than
218
+ # condition_scope_name(:id_greater_than) => :id_greater_than
219
+ def condition_scope_name(name)
220
+ if details = condition_details(name)
221
+ if PRIMARY_CONDITIONS.include?(name.to_sym)
222
+ name
223
+ else
224
+ "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
225
+ end
226
+ else
227
+ nil
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,141 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for 'OR' conditions. Please see the README for a more
4
+ # detailed explanation.
5
+ module OrConditions
6
+ class NoConditionSpecifiedError < StandardError; end
7
+ class UnknownConditionError < StandardError; end
8
+
9
+ def condition?(name) # :nodoc:
10
+ super || or_condition?(name)
11
+ end
12
+
13
+ def named_scope_options(name) # :nodoc:
14
+ super || super(or_conditions(name).join("_or_"))
15
+ end
16
+
17
+ private
18
+ def or_condition?(name)
19
+ !or_conditions(name).nil?
20
+ end
21
+
22
+ def method_missing(name, *args, &block)
23
+ if conditions = or_conditions(name)
24
+ create_or_condition(conditions, args)
25
+ (class << self; self; end).class_eval { alias_method name, conditions.join("_or_") } if !respond_to?(name)
26
+ send(name, *args)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def or_conditions(name)
33
+ # First determine if we should even work on the name, we want to be as quick as possible
34
+ # with this.
35
+ if (parts = split_or_condition(name)).size > 1
36
+ conditions = interpolate_or_conditions(parts)
37
+ if conditions.any?
38
+ conditions
39
+ else
40
+ nil
41
+ end
42
+ end
43
+ end
44
+
45
+ def split_or_condition(name)
46
+ parts = name.to_s.split("_or_")
47
+ new_parts = []
48
+ parts.each do |part|
49
+ if part =~ /^equal_to(_any|_all)?$/
50
+ new_parts << new_parts.pop + "_or_equal_to"
51
+ else
52
+ new_parts << part
53
+ end
54
+ end
55
+ new_parts
56
+ end
57
+
58
+ # The purpose of this method is to convert the method name parts into actual condition names.
59
+ #
60
+ # Example:
61
+ #
62
+ # ["first_name", "last_name_like"]
63
+ # => ["first_name_like", "last_name_like"]
64
+ #
65
+ # ["id_gt", "first_name_begins_with", "last_name", "middle_name_like"]
66
+ # => ["id_gt", "first_name_begins_with", "last_name_like", "middle_name_like"]
67
+ #
68
+ # Basically if a column is specified without a condition the next condition in the list
69
+ # is what will be used. Once we are able to get a consistent list of conditions we can easily
70
+ # create a scope for it.
71
+ def interpolate_or_conditions(parts)
72
+ conditions = []
73
+ last_condition = nil
74
+
75
+ parts.reverse.each do |part|
76
+ if details = condition_details(part)
77
+ # We are a searchlogic defined scope
78
+ conditions << "#{details[:column]}_#{details[:condition]}"
79
+ last_condition = details[:condition]
80
+ elsif association_details = association_condition_details(part, last_condition)
81
+ path = full_association_path(part, last_condition, association_details[:association])
82
+ conditions << "#{path[:path].join("_").to_sym}_#{path[:column]}_#{path[:condition]}"
83
+ last_condition = path[:condition] || nil
84
+ elsif local_condition?(part)
85
+ # We are a custom scope
86
+ conditions << part
87
+ elsif column_names.include?(part)
88
+ # we are a column, use the last condition
89
+ if last_condition.nil?
90
+ raise NoConditionSpecifiedError.new("The '#{part}' column doesn't know which condition to use, if you use an exact column " +
91
+ "name you need to specify a condition sometime after (ex: id_or_created_at_lt), where id would use the 'lt' condition.")
92
+ end
93
+
94
+ conditions << "#{part}_#{last_condition}"
95
+ else
96
+ raise UnknownConditionError.new("The condition '#{part}' is not a valid condition, we could not find any scopes that match this.")
97
+ end
98
+ end
99
+
100
+ conditions.reverse
101
+ end
102
+
103
+ def full_association_path(part, last_condition, given_assoc)
104
+ path = [given_assoc.name]
105
+ part.sub!(/^#{given_assoc.name}_/, "")
106
+ klass = self
107
+ while klass = klass.send(:reflect_on_association, given_assoc.name)
108
+ klass = klass.klass
109
+ if details = klass.send(:association_condition_details, part, last_condition)
110
+ path << details[:association]
111
+ part = details[:condition]
112
+ given_assoc = details[:association]
113
+ elsif details = klass.send(:condition_details, part)
114
+ return { :path => path, :column => details[:column], :condition => details[:condition] }
115
+ end
116
+ end
117
+ { :path => path, :column => part, :condition => last_condition }
118
+ end
119
+
120
+ def create_or_condition(scopes, args)
121
+ scopes_options = scopes.collect { |scope, *args| send(scope, *args).proxy_options }
122
+ # We're using first scope to determine column's type
123
+ scope = named_scope_options(scopes.first)
124
+ column_type = scope.respond_to?(:searchlogic_options) ? scope.searchlogic_options[:type] : :string
125
+ scope scopes.join("_or_"), searchlogic_lambda(column_type) { |*args|
126
+ merge_scopes_with_or(scopes.collect { |scope| clone.send(scope, *args) })
127
+ }
128
+ end
129
+
130
+ def merge_scopes_with_or(scopes)
131
+ scopes_options = scopes.collect { |scope| scope.scope(:find) }
132
+ conditions = scopes_options.reject { |o| o[:conditions].nil? }.collect { |o| sanitize_sql(o[:conditions]) }
133
+ scope = scopes_options.inject(scoped({})) { |current_scope, options| current_scope.scoped(options) }
134
+ options = {}
135
+ in_searchlogic_delegation { options = scope.scope(:find) }
136
+ options.delete(:readonly) unless scopes.any? { |scope| scope.proxy_options.key?(:readonly) }
137
+ options.merge(:conditions => "(" + conditions.join(") OR (") + ")")
138
+ end
139
+ end
140
+ end
141
+ end