joost-searchlogic 2.1.5.2

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/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "searchlogic"
8
+ gem.summary = "Searchlogic provides common named scopes and object based searching for ActiveRecord."
9
+ gem.email = "bjohnson@binarylogic.com"
10
+ gem.homepage = "http://github.com/binarylogic/searchlogic"
11
+ gem.authors = ["Ben Johnson of Binary Logic"]
12
+ gem.rubyforge_project = "searchlogic"
13
+ gem.add_dependency "activerecord", ">= 2.0.0"
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'spec/rake/spectask'
20
+ Spec::Rake::SpecTask.new(:spec) do |spec|
21
+ spec.libs << 'lib' << 'spec'
22
+ spec.spec_files = FileList['spec/**/*_spec.rb']
23
+ end
24
+
25
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.pattern = 'spec/**/*_spec.rb'
28
+ spec.rcov = true
29
+ end
30
+
31
+
32
+ task :default => :spec
33
+
34
+ begin
35
+ require 'rake/contrib/sshpublisher'
36
+ namespace :rubyforge do
37
+ desc "Release gem to RubyForge"
38
+ task :release => ["rubyforge:release:gem"]
39
+ end
40
+ rescue LoadError
41
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
42
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 2
3
+ :minor: 1
4
+ :patch: 5
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,31 @@
1
+ require "searchlogic/core_ext/proc"
2
+ require "searchlogic/core_ext/object"
3
+ require "searchlogic/active_record_consistency"
4
+ require "searchlogic/named_scopes/conditions"
5
+ require "searchlogic/named_scopes/ordering"
6
+ require "searchlogic/named_scopes/associations"
7
+ require "searchlogic/named_scopes/alias_scope"
8
+ require "searchlogic/search"
9
+
10
+ Proc.send(:include, Searchlogic::CoreExt::Proc)
11
+ Object.send(:include, Searchlogic::CoreExt::Object)
12
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
13
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
14
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Associations)
15
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
16
+ ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
17
+
18
+ # Try to use the search method, if it's available. Thinking sphinx and other plugins
19
+ # like to use that method as well.
20
+ if !ActiveRecord::Base.respond_to?(:search)
21
+ ActiveRecord::Base.class_eval do
22
+ class << self
23
+ alias_method :search, :searchlogic
24
+ end
25
+ end
26
+ end
27
+
28
+ if defined?(ActionController)
29
+ require "searchlogic/rails_helpers"
30
+ ActionController::Base.helper(Searchlogic::RailsHelpers)
31
+ end
@@ -0,0 +1,27 @@
1
+ module Searchlogic
2
+ # Active Record is pretty inconsistent with how their SQL is constructed. This
3
+ # method attempts to close the gap between the various inconsistencies.
4
+ module ActiveRecordConsistency
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ alias_method_chain :merge_joins, :searchlogic
8
+ end
9
+ end
10
+
11
+ # In AR multiple joins are sometimes in a single join query, and other time they
12
+ # are not. The merge_joins method in AR should account for this, but it doesn't.
13
+ # This fixes that problem.
14
+ def merge_joins_with_searchlogic(*args)
15
+ joins = merge_joins_without_searchlogic(*args)
16
+ joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
17
+ end
18
+ end
19
+ end
20
+
21
+ module ActiveRecord # :nodoc: all
22
+ class Base
23
+ class << self
24
+ include Searchlogic::ActiveRecordConsistency
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ module Searchlogic
2
+ module CoreExt
3
+ # Contains extensions for the Object class that Searchlogic uses.
4
+ module Object
5
+ # Searchlogic needs to know the expected type of the condition value so that it can properly cast
6
+ # the value in the Searchlogic::Search object. For example:
7
+ #
8
+ # search = User.search(:id_gt => "1")
9
+ #
10
+ # You would expect this:
11
+ #
12
+ # search.id_gt => 1
13
+ #
14
+ # Not this:
15
+ #
16
+ # search.id_gt => "1"
17
+ #
18
+ # Parameter values from forms are ALWAYS strings, so we have to cast them. Just like ActiveRecord
19
+ # does when you instantiate a new User object.
20
+ #
21
+ # The problem is that ruby has no variable types, so Searchlogic needs to know what type you are expecting
22
+ # for your named scope. So instead of this:
23
+ #
24
+ # named_scope :id_gt, lambda { |value| {:conditions => ["id > ?", value]} }
25
+ #
26
+ # You need to do this:
27
+ #
28
+ # named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
29
+ #
30
+ # If you are wanting a string, you don't have to do anything, because Searchlogic assumes you are want a string.
31
+ # If you want something else, you need to specify it as I did in the above example.
32
+ def searchlogic_lambda(type = :string, &block)
33
+ proc = lambda(&block)
34
+ proc.searchlogic_arg_type = type
35
+ proc
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module Searchlogic
2
+ module CoreExt
3
+ module Proc # :nodoc:
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ attr_accessor :searchlogic_arg_type
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
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, while at the same time letting
5
+ # Searchlogic know that this is a safe method.
6
+ module AliasScope
7
+ # The searchlogic Search class takes a hash and chains the values together as named scopes.
8
+ # For security reasons the only hash keys that are allowed must be mapped to named scopes.
9
+ # You can not pass the name of a class method and expect that to be called. In some instances
10
+ # you might create a class method that essentially aliases a named scope or represents a
11
+ # named scope procedure. Ex:
12
+ #
13
+ # User.named_scope :teenager, :conditions => ["age >= ? AND age <= ?", 13, 19]
14
+ #
15
+ # This is obviously a very basic example, but there is logic that is duplicated here. For
16
+ # more complicated named scopes this might make more sense, but to make my point you could
17
+ # do something like this instead
18
+ #
19
+ # class User
20
+ # def teenager
21
+ # age_gte(13).age_lte(19)
22
+ # end
23
+ # end
24
+ #
25
+ # As I stated above, you could not use this method with the Searchlogic::Search class because
26
+ # there is no way to tell that this is actually a named scope. Instead, Searchlogic lets you
27
+ # do something like this:
28
+ #
29
+ # User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
30
+ #
31
+ # It fits in better, at the same time Searchlogic will know this is an acceptable named scope.
32
+ def alias_scope(name, options = nil)
33
+ alias_scopes[name.to_sym] = options
34
+ (class << self; self end).instance_eval do
35
+ define_method name do |*args|
36
+ case options
37
+ when Symbol
38
+ send(options)
39
+ else
40
+ options.call(*args)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def alias_scopes # :nodoc:
47
+ @alias_scopes ||= {}
48
+ end
49
+
50
+ def alias_scope?(name) # :nodoc:
51
+ alias_scopes.key?(name.to_sym)
52
+ end
53
+
54
+ def condition?(name) # :nodoc:
55
+ super || alias_scope?(name)
56
+ end
57
+
58
+ def named_scope_options(name) # :nodoc:
59
+ super || alias_scopes[name.to_sym]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,131 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for associations.
4
+ module Associations
5
+ def condition?(name) # :nodoc:
6
+ super || association_condition?(name) || association_alias_condition?(name)
7
+ end
8
+
9
+ def primary_condition_name(name) # :nodoc:
10
+ if result = super
11
+ result
12
+ elsif association_condition?(name)
13
+ name.to_sym
14
+ elsif details = association_alias_condition_details(name)
15
+ "#{details[:association]}_#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ # Is the name of the method a valid name for an association condition?
22
+ def association_condition?(name)
23
+ !association_condition_details(name).nil?
24
+ end
25
+
26
+ # Is the ane of the method a valie name for an association alias condition?
27
+ # An alias being "gt" for "greater_than", etc.
28
+ def association_alias_condition?(name)
29
+ !association_alias_condition_details(name).nil?
30
+ end
31
+
32
+ # A convenience method for creating inner join sql to that your inner joins
33
+ # are consistent with how Active Record creates them.
34
+ def inner_joins(association_name)
35
+ ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
36
+ end
37
+
38
+ private
39
+ def method_missing(name, *args, &block)
40
+ if details = association_condition_details(name)
41
+ create_association_condition(details[:association], details[:column], details[:condition], args)
42
+ send(name, *args)
43
+ elsif details = association_alias_condition_details(name)
44
+ create_association_alias_condition(details[:association], details[:column], details[:condition], args)
45
+ send(name, *args)
46
+ elsif details = association_ordering_condition_details(name)
47
+ create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
48
+ send(name, *args)
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def association_condition_details(name)
55
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
56
+ if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
57
+ {:association => $1, :column => $2, :condition => $3}
58
+ end
59
+ end
60
+
61
+ def create_association_condition(association_name, column, condition, args)
62
+ named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
63
+ end
64
+
65
+ def association_alias_condition_details(name)
66
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
67
+ if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
68
+ {:association => $1, :column => $2, :condition => $3}
69
+ end
70
+ end
71
+
72
+ def create_association_alias_condition(association, column, condition, args)
73
+ primary_condition = primary_condition(condition)
74
+ alias_name = "#{association}_#{column}_#{condition}"
75
+ primary_name = "#{association}_#{column}_#{primary_condition}"
76
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
77
+ (class << self; self; end).class_eval { alias_method alias_name, primary_name }
78
+ end
79
+
80
+ def association_ordering_condition_details(name)
81
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
82
+ if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
83
+ {:order_as => $1, :association => $2, :column => $3} unless column_names.include?("#{$2}_#{$3}")
84
+ end
85
+ end
86
+
87
+ def create_association_ordering_condition(association_name, order_as, column, args)
88
+ named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
89
+ end
90
+
91
+ def association_condition_options(association_name, association_condition, args)
92
+ association = reflect_on_association(association_name.to_sym)
93
+ scope = association.klass.send(association_condition, *args)
94
+ scope_options = association.klass.named_scope_options(association_condition)
95
+ arity = association.klass.named_scope_arity(association_condition)
96
+
97
+ if !arity || arity == 0
98
+ # The underlying condition doesn't require any parameters, so let's just create a simple
99
+ # named scope that is based on a hash.
100
+ options = scope.proxy_options
101
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
102
+ options
103
+ else
104
+ # The underlying condition requires parameters, let's match the parameters it requires
105
+ # and pass those onto the named scope. We can't use proxy_options because that returns the
106
+ # result after a value has been passed.
107
+ proc_args = []
108
+ if arity > 0
109
+ arity.times { |i| proc_args << "arg#{i}"}
110
+ else
111
+ positive_arity = arity * -1
112
+ positive_arity.times do |i|
113
+ if i == (positive_arity - 1)
114
+ proc_args << "*arg#{i}"
115
+ else
116
+ proc_args << "arg#{i}"
117
+ end
118
+ end
119
+ end
120
+ eval <<-"end_eval"
121
+ searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
122
+ options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
123
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
124
+ options
125
+ }
126
+ end_eval
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,215 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for columns.
4
+ module Conditions
5
+ COMPARISON_CONDITIONS = {
6
+ :equals => [:is, :eq],
7
+ :does_not_equal => [:not_equal_to, :is_not, :not, :ne],
8
+ :less_than => [:lt, :before],
9
+ :less_than_or_equal_to => [:lte],
10
+ :greater_than => [:gt, :after],
11
+ :greater_than_or_equal_to => [:gte],
12
+ }
13
+
14
+ WILDCARD_CONDITIONS = {
15
+ :like => [:contains, :includes],
16
+ :begins_with => [:bw],
17
+ :ends_with => [:ew],
18
+ }
19
+
20
+ BOOLEAN_CONDITIONS = {
21
+ :null => [:nil],
22
+ :empty => []
23
+ }
24
+
25
+ CONDITIONS = {}
26
+
27
+ COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
28
+ CONDITIONS[condition] = aliases
29
+ CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
30
+ CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
31
+ end
32
+
33
+ BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
34
+
35
+ PRIMARY_CONDITIONS = CONDITIONS.keys
36
+ ALIAS_CONDITIONS = CONDITIONS.values.flatten
37
+
38
+ # Retrieves the options passed when creating the respective named scope. Ex:
39
+ #
40
+ # named_scope :whatever, :conditions => {:column => value}
41
+ #
42
+ # This method will return:
43
+ #
44
+ # :conditions => {:column => value}
45
+ #
46
+ # ActiveRecord hides this internally, so we have to try and pull it out with this
47
+ # method.
48
+ def named_scope_options(name)
49
+ key = scopes.key?(name.to_sym) ? name.to_sym : primary_condition_name(name)
50
+
51
+ if key
52
+ eval("options", scopes[key])
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ # The arity for a named scope's proc is important, because we use the arity
59
+ # to determine if the condition should be ignored when calling the search method.
60
+ # If the condition is false and the arity is 0, then we skip it all together. Ex:
61
+ #
62
+ # User.named_scope :age_is_4, :conditions => {:age => 4}
63
+ # User.search(:age_is_4 => false) == User.all
64
+ # User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
65
+ #
66
+ # We also use it when trying to "copy" the underlying named scope for association
67
+ # conditions.
68
+ def named_scope_arity(name)
69
+ options = named_scope_options(name)
70
+ options.respond_to?(:arity) ? options.arity : nil
71
+ end
72
+
73
+ # Returns the primary condition for the given alias. Ex:
74
+ #
75
+ # primary_condition(:gt) => :greater_than
76
+ def primary_condition(alias_condition)
77
+ CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
78
+ end
79
+
80
+ # Returns the primary name for any condition on a column. You can pass it
81
+ # a primary condition, alias condition, etc, and it will return the proper
82
+ # primary condition name. This helps simply logic throughout Searchlogic. Ex:
83
+ #
84
+ # primary_condition_name(:id_gt) => :id_greater_than
85
+ # primary_condition_name(:id_greater_than) => :id_greater_than
86
+ def primary_condition_name(name)
87
+ if primary_condition?(name)
88
+ name.to_sym
89
+ elsif details = alias_condition_details(name)
90
+ "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
91
+ else
92
+ nil
93
+ end
94
+ end
95
+
96
+ # Is the name of the method a valid condition that can be dynamically created?
97
+ def condition?(name)
98
+ primary_condition?(name) || alias_condition?(name)
99
+ end
100
+
101
+ # Is the name of the method a valid condition that can be dynamically created,
102
+ # AND is it a primary condition (not an alias). "greater_than" not "gt".
103
+ def primary_condition?(name)
104
+ !primary_condition_details(name).nil?
105
+ end
106
+
107
+ # Is the name of the method a valid condition that can be dynamically created,
108
+ # AND is it an alias condition. "gt" not "greater_than".
109
+ def alias_condition?(name)
110
+ !alias_condition_details(name).nil?
111
+ end
112
+
113
+ private
114
+ def method_missing(name, *args, &block)
115
+ if details = primary_condition_details(name)
116
+ create_primary_condition(details[:column], details[:condition])
117
+ send(name, *args)
118
+ elsif details = alias_condition_details(name)
119
+ create_alias_condition(details[:column], details[:condition], args)
120
+ send(name, *args)
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+ def primary_condition_details(name)
127
+ if name.to_s =~ /^(#{column_names.join("|")})_(#{PRIMARY_CONDITIONS.join("|")})$/
128
+ {:column => $1, :condition => $2}
129
+ end
130
+ end
131
+
132
+ def create_primary_condition(column, condition)
133
+ column_type = columns_hash[column.to_s].type
134
+ scope_options = case condition.to_s
135
+ when /^equals/
136
+ scope_options(condition, column_type, "#{table_name}.#{column} = ?")
137
+ when /^does_not_equal/
138
+ scope_options(condition, column_type, "#{table_name}.#{column} != ?")
139
+ when /^less_than_or_equal_to/
140
+ scope_options(condition, column_type, "#{table_name}.#{column} <= ?")
141
+ when /^less_than/
142
+ scope_options(condition, column_type, "#{table_name}.#{column} < ?")
143
+ when /^greater_than_or_equal_to/
144
+ scope_options(condition, column_type, "#{table_name}.#{column} >= ?")
145
+ when /^greater_than/
146
+ scope_options(condition, column_type, "#{table_name}.#{column} > ?")
147
+ when /^like/
148
+ scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :like)
149
+ when /^begins_with/
150
+ scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :begins_with)
151
+ when /^ends_with/
152
+ scope_options(condition, column_type, "#{table_name}.#{column} LIKE ?", :ends_with)
153
+ when "null"
154
+ {:conditions => "#{table_name}.#{column} IS NULL"}
155
+ when "empty"
156
+ {:conditions => "#{table_name}.#{column} = ''"}
157
+ end
158
+
159
+ named_scope("#{column}_#{condition}".to_sym, scope_options)
160
+ end
161
+
162
+ # This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
163
+ # Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
164
+ # fast as possible, which is why I didn't do the case statement inside of the lambda.
165
+ def scope_options(condition, column_type, sql, value_modifier = nil)
166
+ case condition.to_s
167
+ when /_(any|all)$/
168
+ searchlogic_lambda(column_type) { |*values|
169
+ return {} if values.empty?
170
+ values = values.flatten
171
+
172
+ values_to_sub = nil
173
+ if value_modifier.nil?
174
+ values_to_sub = values
175
+ else
176
+ values_to_sub = values.collect { |value| value_with_modifier(value, value_modifier) }
177
+ end
178
+
179
+ join = $1 == "any" ? " OR " : " AND "
180
+ {:conditions => [values.collect { |value| sql }.join(join), *values_to_sub]}
181
+ }
182
+ else
183
+ searchlogic_lambda(column_type) { |value| {:conditions => [sql, value_with_modifier(value, value_modifier)]} }
184
+ end
185
+ end
186
+
187
+ def value_with_modifier(value, modifier)
188
+ case modifier
189
+ when :like
190
+ "%#{value}%"
191
+ when :begins_with
192
+ "#{value}%"
193
+ when :ends_with
194
+ "%#{value}"
195
+ else
196
+ value
197
+ end
198
+ end
199
+
200
+ def alias_condition_details(name)
201
+ if name.to_s =~ /^(#{column_names.join("|")})_(#{ALIAS_CONDITIONS.join("|")})$/
202
+ {:column => $1, :condition => $2}
203
+ end
204
+ end
205
+
206
+ def create_alias_condition(column, condition, args)
207
+ primary_condition = primary_condition(condition)
208
+ alias_name = "#{column}_#{condition}"
209
+ primary_name = "#{column}_#{primary_condition}"
210
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
211
+ (class << self; self; end).class_eval { alias_method alias_name, primary_name }
212
+ end
213
+ end
214
+ end
215
+ end