newellista-searchlogic 2.0.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,48 @@
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 is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
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
+ require 'rake/rdoctask'
35
+ Rake::RDocTask.new do |rdoc|
36
+ if File.exist?('VERSION.yml')
37
+ config = YAML.load(File.read('VERSION.yml'))
38
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
39
+ else
40
+ version = ""
41
+ end
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "search #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 2
3
+ :minor: 0
4
+ :patch: 2
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,20 @@
1
+ require "searchlogic/core_ext/proc"
2
+ require "searchlogic/core_ext/object"
3
+ require "searchlogic/named_scopes/conditions"
4
+ require "searchlogic/named_scopes/ordering"
5
+ require "searchlogic/named_scopes/associations"
6
+ require "searchlogic/named_scopes/alias_scope"
7
+ require "searchlogic/search"
8
+
9
+ Proc.send(:include, Searchlogic::CoreExt::Proc)
10
+ Object.send(:include, Searchlogic::CoreExt::Object)
11
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
12
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
13
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::Associations)
14
+ ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
15
+ ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
16
+
17
+ if defined?(ActionController)
18
+ require "searchlogic/rails_helpers"
19
+ ActionController::Base.helper(Searchlogic::RailsHelpers)
20
+ 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,126 @@
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
+ private
33
+ def method_missing(name, *args, &block)
34
+ if details = association_condition_details(name)
35
+ create_association_condition(details[:association], details[:column], details[:condition], args)
36
+ send(name, *args)
37
+ elsif details = association_alias_condition_details(name)
38
+ create_association_alias_condition(details[:association], details[:column], details[:condition], args)
39
+ send(name, *args)
40
+ elsif details = association_ordering_condition_details(name)
41
+ create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
42
+ send(name, *args)
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ def association_condition_details(name)
49
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
50
+ if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
51
+ {:association => $1, :column => $2, :condition => $3}
52
+ end
53
+ end
54
+
55
+ def create_association_condition(association_name, column, condition, args)
56
+ named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
57
+ end
58
+
59
+ def association_alias_condition_details(name)
60
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
61
+ if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
62
+ {:association => $1, :column => $2, :condition => $3}
63
+ end
64
+ end
65
+
66
+ def create_association_alias_condition(association, column, condition, args)
67
+ primary_condition = primary_condition(condition)
68
+ alias_name = "#{association}_#{column}_#{condition}"
69
+ primary_name = "#{association}_#{column}_#{primary_condition}"
70
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
71
+ (class << self; self; end).class_eval { alias_method alias_name, primary_name }
72
+ end
73
+
74
+ def association_ordering_condition_details(name)
75
+ associations = reflect_on_all_associations.collect { |assoc| assoc.name }
76
+ if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
77
+ {:order_as => $1, :association => $2, :column => $3}
78
+ end
79
+ end
80
+
81
+ def create_association_ordering_condition(association_name, order_as, column, args)
82
+ named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
83
+ end
84
+
85
+ def association_condition_options(association_name, association_condition, args)
86
+ association = reflect_on_association(association_name.to_sym)
87
+ scope = association.klass.send(association_condition, *args)
88
+ scope_options = association.klass.named_scope_options(association_condition)
89
+ arity = association.klass.named_scope_arity(association_condition)
90
+
91
+ if !arity || arity == 0
92
+ # The underlying condition doesn't require any parameters, so let's just create a simple
93
+ # named scope that is based on a hash.
94
+ options = scope.proxy_options
95
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
96
+ #add_left_outer_joins(options, association)
97
+ options
98
+ else
99
+ # The underlying condition requires parameters, let's match the parameters it requires
100
+ # and pass those onto the named scope. We can't use proxy_options because that returns the
101
+ # result after a value has been passed.
102
+ proc_args = []
103
+ if arity > 0
104
+ arity.times { |i| proc_args << "arg#{i}"}
105
+ else
106
+ positive_arity = arity * -1
107
+ positive_arity.times do |i|
108
+ if i == (positive_arity - 1)
109
+ proc_args << "*arg#{i}"
110
+ else
111
+ proc_args << "arg#{i}"
112
+ end
113
+ end
114
+ end
115
+ eval <<-"end_eval"
116
+ searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
117
+ options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
118
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
119
+ options
120
+ }
121
+ end_eval
122
+ end
123
+ end
124
+ end
125
+ end
126
+ 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