modern_searchlogic 1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +45 -0
- data/Rakefile +122 -0
- data/lib/modern_searchlogic/active_record_methods.rb +23 -0
- data/lib/modern_searchlogic/column_conditions.rb +303 -0
- data/lib/modern_searchlogic/default_scoping.rb +13 -0
- data/lib/modern_searchlogic/ordering.rb +20 -0
- data/lib/modern_searchlogic/railtie.rb +9 -0
- data/lib/modern_searchlogic/scope_procedure.rb +18 -0
- data/lib/modern_searchlogic/scope_tracking.rb +21 -0
- data/lib/modern_searchlogic/search.rb +90 -0
- data/lib/modern_searchlogic/searchable.rb +15 -0
- data/lib/modern_searchlogic/version.rb +5 -0
- data/lib/modern_searchlogic.rb +5 -0
- data/lib/tasks/modern_searchlogic_tasks.rake +4 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8c1285376c99c453d6e9af0bb21b1458c4f34d5ddd7cdec3ce4e14ceae3d5d6b
|
4
|
+
data.tar.gz: bce4a074d01473a88767f1ef8b11c77c8a127272a6dbd6907dc0d8977d489796
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8dbd45af5b7a11758a6e3323b62697f746e275033cbc492c669967fb9c570a3c2d805682c1635eff6d49d91a9302812dc9e879eb824322a6372164588d8968b9
|
7
|
+
data.tar.gz: d68579d00786da93e3db22cfafeecc5440455f4e3f4a90e38ed7c5eb712ccac63a1123fabe22281e9bcb61355d11b5fe19903ec1a1bdf986cf8882559c66aebf
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Andrew Warner
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# ModernSearchlogic
|
2
|
+
|
3
|
+
Searchlogic for Rails 3+!
|
4
|
+
|
5
|
+
Note: this is a fork of [Genius/modern_searchlogic](https://github.com/Genius/modern_searchlogic).
|
6
|
+
Unlike upstream, this repository is published to RubyGems (and has versioning).
|
7
|
+
|
8
|
+
## Supported Versions
|
9
|
+
|
10
|
+
The Gem is tested on Rails 3-8 (except 6, because I'm lazy), using the latest version available.
|
11
|
+
For the older versions, it uses Rails LTS so we don't need to maintain Ruby 3 patches here as well.
|
12
|
+
|
13
|
+
The gem *might* work on Ruby 2, but it is only tested against Ruby 3.3.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Just add the Gem to your gemspec, and the searchlogic methods will be available.
|
18
|
+
Refer to the searchlogic documentation for more details.
|
19
|
+
|
20
|
+
## Contributing
|
21
|
+
|
22
|
+
Optional, but required for running specs against Rails 3-5: a [Rails LTS](https://railslts.com) subscription.
|
23
|
+
If you do have one, create a `.bundle/config` with contents:
|
24
|
+
|
25
|
+
```yaml
|
26
|
+
---
|
27
|
+
BUNDLE_GEMS__RAILSLTS__COM: "theusername:thepassword"
|
28
|
+
```
|
29
|
+
|
30
|
+
- Install Ruby 3.3
|
31
|
+
- Run `bundler install`
|
32
|
+
- If you DON'T have a Rails LTS subscription, comment out the Rails 3-5 appraisals in the `Appraisal` file
|
33
|
+
- Run `bundler exec appraisal install`
|
34
|
+
- Start the database. You can use Docker compose to do this the easy way. If you go the manual route, make sure authentication is optional
|
35
|
+
- You are now ready!
|
36
|
+
|
37
|
+
## Running Specs
|
38
|
+
|
39
|
+
```shell
|
40
|
+
# Run for ALL Rails versions:
|
41
|
+
$ bundle exec rake test
|
42
|
+
|
43
|
+
# OR for a specific Rails version only (replace 7 with the major version of Rails):
|
44
|
+
$ bundle exec rake rspec7
|
45
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rake'
|
8
|
+
require 'pathname'
|
9
|
+
|
10
|
+
SUPPORTED_RAILS_VERSIONS = (3..8).to_a.filter { |it| it != 6 }
|
11
|
+
|
12
|
+
def convert_appraisal_to_gemfile(appraisal_name)
|
13
|
+
appraisal_name.tr('-', '_')
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_database(gemfile_path, test_app_dir)
|
17
|
+
puts "Setting up database..."
|
18
|
+
|
19
|
+
Dir.chdir(File.join(ENV['PROJECT_ROOT'], test_app_dir)) do
|
20
|
+
sh({ 'BUNDLE_GEMFILE' => gemfile_path }, 'bundle exec rake db:create:all') || exit(1)
|
21
|
+
sh({ 'BUNDLE_GEMFILE' => gemfile_path }, 'bundle exec rake db:environment:set RAILS_ENV=test') || exit(1)
|
22
|
+
sh({ 'BUNDLE_GEMFILE' => gemfile_path }, 'bundle exec rake db:schema:load') || exit(1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_spec_files
|
27
|
+
Dir.chdir(ENV['PROJECT_ROOT']) do
|
28
|
+
Dir.glob('spec/**/*_spec.rb')
|
29
|
+
.reject { |file| file.match?(/spec\/app_rails.*\//) }
|
30
|
+
.map { |file| File.expand_path(file) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_specs(gemfile_path, test_app_dir, spec_files)
|
35
|
+
current_rubylib = ENV['RUBYLIB'] || ''
|
36
|
+
spec_path = File.join(ENV['PROJECT_ROOT'], 'spec')
|
37
|
+
ENV['RUBYLIB'] = current_rubylib.empty? ? spec_path : "#{spec_path}:#{current_rubylib}"
|
38
|
+
|
39
|
+
ENV['RAILS_ROOT'] = File.join(ENV['PROJECT_ROOT'], test_app_dir)
|
40
|
+
|
41
|
+
Dir.chdir(File.join(ENV['PROJECT_ROOT'], test_app_dir)) do
|
42
|
+
ruby3_compat = File.join(ENV['PROJECT_ROOT'], 'spec', 'ruby3_compatibility')
|
43
|
+
|
44
|
+
cmd = [
|
45
|
+
'bundle', 'exec', 'ruby',
|
46
|
+
'-r', ruby3_compat,
|
47
|
+
'-e', "require 'rspec/core'; RSpec::Core::Runner.run(ARGV)",
|
48
|
+
*spec_files
|
49
|
+
]
|
50
|
+
|
51
|
+
sh({ 'BUNDLE_GEMFILE' => gemfile_path }, *cmd) || exit(1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def run_specs_for_version(appraisal_name, test_app_dir)
|
56
|
+
ENV['POSTGRES_URL'] ||= 'postgres://postgres:password@localhost:5432/postgres'
|
57
|
+
ENV['RAILS_ENV'] ||= 'test'
|
58
|
+
ENV['PROJECT_ROOT'] ||= `git rev-parse --show-toplevel`.strip
|
59
|
+
|
60
|
+
gemfile_name = convert_appraisal_to_gemfile(appraisal_name)
|
61
|
+
gemfile_path = File.join(ENV['PROJECT_ROOT'], 'gemfiles', "#{gemfile_name}.gemfile")
|
62
|
+
|
63
|
+
puts "Running specs for #{appraisal_name} in #{test_app_dir}"
|
64
|
+
puts "Using gemfile: #{gemfile_name}.gemfile"
|
65
|
+
|
66
|
+
setup_database(gemfile_path, test_app_dir)
|
67
|
+
spec_files = find_spec_files
|
68
|
+
run_specs(gemfile_path, test_app_dir, spec_files)
|
69
|
+
puts "Specs completed for #{appraisal_name}"
|
70
|
+
end
|
71
|
+
|
72
|
+
SUPPORTED_RAILS_VERSIONS.each do |version|
|
73
|
+
desc "Run specs for Rails #{version}"
|
74
|
+
task :"rspec#{version}" do
|
75
|
+
run_specs_for_version("rails-#{version}", "spec/app_rails#{version}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'Run specs for all versions of Rails'
|
80
|
+
task :test => SUPPORTED_RAILS_VERSIONS.map { |it| "rspec#{it}".to_sym }
|
81
|
+
|
82
|
+
desc 'Install appraisals'
|
83
|
+
task :install_appraisals do
|
84
|
+
sh('bundler exec appraisal install')
|
85
|
+
end
|
86
|
+
|
87
|
+
require 'pathname'
|
88
|
+
|
89
|
+
desc 'Set up symlinks for a new Rails version appraisal'
|
90
|
+
task :setup_symlinks, [:name] do |_t, args|
|
91
|
+
raise 'Please provide the app name (e.g. app_rails9)' unless args[:name]
|
92
|
+
|
93
|
+
base_dir = Pathname.new("spec/#{args[:name]}")
|
94
|
+
shared_dir = Pathname.new('spec/shared')
|
95
|
+
|
96
|
+
links = [
|
97
|
+
{ from: 'app/models', to: 'models' },
|
98
|
+
{ from: 'db/migrate', to: 'db/migrate' },
|
99
|
+
{ from: 'config/database.yml', to: 'config/database.yml' }
|
100
|
+
]
|
101
|
+
|
102
|
+
links.each do |link|
|
103
|
+
link_name = base_dir.join(link[:from])
|
104
|
+
target = shared_dir.join(link[:to])
|
105
|
+
parent_dir = link_name.dirname
|
106
|
+
sh("mkdir -p #{parent_dir}")
|
107
|
+
relative_target = target.relative_path_from(parent_dir)
|
108
|
+
sh("ln -sf #{relative_target} #{link_name}")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
require 'rdoc/task'
|
113
|
+
|
114
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
115
|
+
rdoc.rdoc_dir = 'rdoc'
|
116
|
+
rdoc.title = 'ModernSearchlogic'
|
117
|
+
rdoc.options << '--line-numbers'
|
118
|
+
rdoc.rdoc_files.include('README.rdoc')
|
119
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
120
|
+
end
|
121
|
+
|
122
|
+
Bundler::GemHelper.install_tasks
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative 'default_scoping'
|
2
|
+
require_relative 'scope_tracking'
|
3
|
+
require_relative 'column_conditions'
|
4
|
+
require_relative 'ordering'
|
5
|
+
require_relative 'scope_procedure'
|
6
|
+
require_relative 'searchable'
|
7
|
+
|
8
|
+
module ModernSearchlogic
|
9
|
+
module ActiveRecordMethods
|
10
|
+
def self.install
|
11
|
+
ActiveRecord::Base.__send__(:include, self)
|
12
|
+
ActiveRecord::Relation.__send__(:include, Ordering)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.include DefaultScoping
|
17
|
+
base.include ScopeTracking
|
18
|
+
base.include ColumnConditions
|
19
|
+
base.include ScopeProcedure
|
20
|
+
base.include Searchable
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
module ModernSearchlogic
|
2
|
+
module ColumnConditions
|
3
|
+
module ClassMethods
|
4
|
+
def respond_to_missing?(method, *)
|
5
|
+
super || valid_searchlogic_scope?(method)
|
6
|
+
end
|
7
|
+
|
8
|
+
def valid_searchlogic_scope?(method)
|
9
|
+
return false if connection.tables.empty? || method =~ /^define_method_/ || abstract_class?
|
10
|
+
|
11
|
+
searchlogic_scope_dynamically_defined?(method) ||
|
12
|
+
!!searchlogic_column_condition_method_block(method.to_s) ||
|
13
|
+
_defined_scopes.include?(method.to_sym)
|
14
|
+
end
|
15
|
+
|
16
|
+
def dynamically_define_searchlogic_method(method)
|
17
|
+
return true if searchlogic_scope_dynamically_defined?(method)
|
18
|
+
return false unless searchlogic_scope = searchlogic_column_condition_method_block(method.to_s)
|
19
|
+
singleton_class.__send__(:define_method, method, &searchlogic_scope[:block])
|
20
|
+
self._dynamically_defined_searchlogic_scopes = self._dynamically_defined_searchlogic_scopes.merge(method => searchlogic_scope)
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def searchlogic_method_arity(method)
|
25
|
+
method = method.to_sym
|
26
|
+
raise ArgumentError, "Not a searchlogic scope" unless valid_searchlogic_scope?(method)
|
27
|
+
dynamically_define_searchlogic_method(method)
|
28
|
+
|
29
|
+
searchlogic_scope_dynamically_defined?(method) ?
|
30
|
+
_dynamically_defined_searchlogic_scopes[method][:arity] :
|
31
|
+
self.method(method).arity
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def searchlogic_scope_dynamically_defined?(method)
|
37
|
+
_dynamically_defined_searchlogic_scopes.key?(method)
|
38
|
+
end
|
39
|
+
|
40
|
+
def searchlogic_suffix_condition(suffix, options = {}, &method_block)
|
41
|
+
searchlogic_suffix_conditions[suffix] = [options, method_block]
|
42
|
+
end
|
43
|
+
|
44
|
+
def searchlogic_prefix_condition(prefix, &method_block)
|
45
|
+
searchlogic_prefix_conditions[prefix] = method_block
|
46
|
+
end
|
47
|
+
|
48
|
+
def searchlogic_extract_arel_compatible_value(value)
|
49
|
+
if value.respond_to?(:map) && !value.acts_like?(:string)
|
50
|
+
value.map { |v| searchlogic_extract_arel_compatible_value(v) }
|
51
|
+
elsif value.is_a?(ActiveRecord::Base)
|
52
|
+
value.id
|
53
|
+
else
|
54
|
+
value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def searchlogic_arel_alias(searchlogic_suffix, arel_method, options = {})
|
59
|
+
value_mapper = options.fetch(:map_value, -> (x) { searchlogic_extract_arel_compatible_value(x) })
|
60
|
+
|
61
|
+
searchlogic_suffix_condition "_#{searchlogic_suffix}", options do |column_name, *args|
|
62
|
+
values = coerce_and_validate_args_for_arel_aliases!(args, options)
|
63
|
+
arel_table[column_name].__send__(arel_method, value_mapper.call(values))
|
64
|
+
end
|
65
|
+
|
66
|
+
searchlogic_suffix_condition "_#{searchlogic_suffix}_any", options do |column_name, *args|
|
67
|
+
values = coerce_and_validate_args_for_arel_aliases!(args, options.merge(:any_or_all => true))
|
68
|
+
arel_table[column_name].__send__("#{arel_method}_any", values.map(&value_mapper))
|
69
|
+
end
|
70
|
+
|
71
|
+
searchlogic_suffix_condition "_#{searchlogic_suffix}_all", options do |column_name, *args|
|
72
|
+
values = coerce_and_validate_args_for_arel_aliases!(args, options.merge(:any_or_all => true))
|
73
|
+
arel_table[column_name].__send__("#{arel_method}_all", values.map(&value_mapper))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def searchlogic_active_record_alias(searchlogic_suffix, options = {}, &block)
|
78
|
+
searchlogic_suffix_condition "_#{searchlogic_suffix}" do |column_name, *args|
|
79
|
+
raise ArgumentError, "wrong number of arguments (0 for >= 1)" if args.empty?
|
80
|
+
raise ArgumentError, "unsupported searchlogic suffix #{searchlogic_suffix} passed" unless [:in, :not_in].include?(searchlogic_suffix)
|
81
|
+
|
82
|
+
relation = unscoped { instance_exec(column_name, args, &block) }
|
83
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
84
|
+
relation.where_clause&.ast
|
85
|
+
elsif [3, 4].include?(ActiveRecord::VERSION::MAJOR)
|
86
|
+
relation.where_values.reduce(&:and)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def searchlogic_suffix_condition_match(method_name)
|
92
|
+
suffix_regexp = searchlogic_suffix_conditions.keys.join('|')
|
93
|
+
if match = method_name.match(/\A(#{column_names_regexp}(?:_or_#{column_names_regexp})*)(#{suffix_regexp})\z/)
|
94
|
+
options, method_block = searchlogic_suffix_conditions.fetch(match[2])
|
95
|
+
column_names = match[1].split('_or_')
|
96
|
+
|
97
|
+
arity = calculate_arity(method_block)
|
98
|
+
|
99
|
+
{
|
100
|
+
arity: arity,
|
101
|
+
block: lambda do |*args|
|
102
|
+
validate_argument_count!(arity, args.length) if arity >= 0
|
103
|
+
arel_nodes = column_names.map do |n|
|
104
|
+
instance_exec(n, *args, &method_block)
|
105
|
+
end
|
106
|
+
where(arel_nodes.reduce(:or))
|
107
|
+
end
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def searchlogic_prefix_match(method_name)
|
113
|
+
prefix_regexp = searchlogic_prefix_conditions.keys.join('|')
|
114
|
+
if match = method_name.match(/\A(#{prefix_regexp})(#{column_names_regexp})\z/)
|
115
|
+
method_block = searchlogic_prefix_conditions.fetch(match[1])
|
116
|
+
|
117
|
+
arity = calculate_arity(method_block)
|
118
|
+
|
119
|
+
{
|
120
|
+
arity: arity,
|
121
|
+
block: lambda do |*args|
|
122
|
+
validate_argument_count!(method_block.arity - 1, args.length) if method_block.arity >= 1
|
123
|
+
instance_exec(match[2], *args, &method_block)
|
124
|
+
end
|
125
|
+
}
|
126
|
+
elsif match = method_name.match(/\A(#{prefix_regexp})(#{association_names_regexp})_(\S+)\z/)
|
127
|
+
prefix, association_name, rest = match.to_a.drop(1)
|
128
|
+
|
129
|
+
searchlogic_association_finder_method(association_by_name.fetch(association_name.to_sym), prefix + rest)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def searchlogic_association_suffix_match(method_name)
|
134
|
+
if match = method_name.match(/\A(#{association_names_regexp})_(\S+)\z/)
|
135
|
+
searchlogic_association_finder_method(association_by_name.fetch(match[1].to_sym), match[2])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def searchlogic_column_boolean_match(method_name)
|
140
|
+
if match = method_name.match(/^not_(.*)/)
|
141
|
+
column_name = match[1]
|
142
|
+
if boolean_column?(column_name)
|
143
|
+
{arity: 0, block: lambda { where(column_name => false) }}
|
144
|
+
end
|
145
|
+
elsif boolean_column?(method_name)
|
146
|
+
{arity: 0, block: lambda { where(method_name => true)}}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def boolean_column?(name)
|
151
|
+
column = columns_hash[name.to_s]
|
152
|
+
column && column.type == :boolean
|
153
|
+
end
|
154
|
+
|
155
|
+
def searchlogic_association_finder_method(association, method_name)
|
156
|
+
method_name = method_name.to_sym
|
157
|
+
if !association.options[:polymorphic] && association.klass.valid_searchlogic_scope?(method_name)
|
158
|
+
arity = association.klass.searchlogic_method_arity(method_name)
|
159
|
+
|
160
|
+
{
|
161
|
+
arity: arity,
|
162
|
+
block: lambda do |*args|
|
163
|
+
joins(association.name).merge(association.klass.__send__(method_name, *args))
|
164
|
+
end
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def association_by_name
|
170
|
+
reflect_on_all_associations.each.with_object({}) do |assoc, obj|
|
171
|
+
obj[assoc.name] = assoc
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def association_names_regexp
|
176
|
+
association_by_name.keys.join('|')
|
177
|
+
end
|
178
|
+
|
179
|
+
def searchlogic_column_condition_method_block(method)
|
180
|
+
return if self == ActiveRecord::Base
|
181
|
+
|
182
|
+
method = method.to_s
|
183
|
+
searchlogic_prefix_match(method) ||
|
184
|
+
searchlogic_suffix_condition_match(method) ||
|
185
|
+
searchlogic_association_suffix_match(method) ||
|
186
|
+
searchlogic_column_boolean_match(method)
|
187
|
+
end
|
188
|
+
|
189
|
+
def column_names_regexp
|
190
|
+
"(?:#{column_names.join('|')})"
|
191
|
+
end
|
192
|
+
|
193
|
+
def method_missing(method, *args, &block)
|
194
|
+
return super if connection.tables.empty? || abstract_class?
|
195
|
+
return super unless dynamically_define_searchlogic_method(method)
|
196
|
+
|
197
|
+
__send__(method, *args, &block)
|
198
|
+
end
|
199
|
+
|
200
|
+
def coerce_and_validate_args_for_arel_aliases!(args, options)
|
201
|
+
any_or_all = options[:any_or_all]
|
202
|
+
|
203
|
+
if options[:takes_array_args]
|
204
|
+
args = [any_or_all ? args : args.flatten]
|
205
|
+
elsif any_or_all
|
206
|
+
args = [args.flatten]
|
207
|
+
end
|
208
|
+
|
209
|
+
if any_or_all
|
210
|
+
raise ArgumentError, "wrong number of arguments (0 for >= 1)" if args.first.length.zero?
|
211
|
+
elsif args.length != 1
|
212
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
|
213
|
+
end
|
214
|
+
|
215
|
+
args.first
|
216
|
+
end
|
217
|
+
|
218
|
+
def validate_argument_count!(expected_args, actual_args)
|
219
|
+
if expected_args != actual_args
|
220
|
+
raise ArgumentError, "wrong number of arguments (#{actual_args} for #{expected_args})"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def calculate_arity(block)
|
225
|
+
block.arity > 0 ? block.arity - 1 : block.arity + 1
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.included(base)
|
230
|
+
base.extend ClassMethods
|
231
|
+
|
232
|
+
base.class_eval do
|
233
|
+
class_attribute :searchlogic_suffix_conditions
|
234
|
+
self.searchlogic_suffix_conditions = {}
|
235
|
+
|
236
|
+
class_attribute :searchlogic_prefix_conditions
|
237
|
+
self.searchlogic_prefix_conditions = {}
|
238
|
+
|
239
|
+
searchlogic_arel_alias :equals, :eq
|
240
|
+
searchlogic_arel_alias :eq, :eq
|
241
|
+
searchlogic_arel_alias :is, :eq
|
242
|
+
searchlogic_arel_alias :does_not_equal, :not_eq
|
243
|
+
searchlogic_arel_alias :ne, :not_eq
|
244
|
+
searchlogic_arel_alias :not_eq, :not_eq
|
245
|
+
searchlogic_arel_alias :not, :not_eq
|
246
|
+
searchlogic_arel_alias :is_not, :not_eq
|
247
|
+
searchlogic_arel_alias :greater_than, :gt
|
248
|
+
searchlogic_arel_alias :gt, :gt
|
249
|
+
searchlogic_arel_alias :less_than, :lt
|
250
|
+
searchlogic_arel_alias :lt, :lt
|
251
|
+
searchlogic_arel_alias :greater_than_or_equal_to, :gteq
|
252
|
+
searchlogic_arel_alias :gte, :gteq
|
253
|
+
searchlogic_arel_alias :less_than_or_equal_to, :lteq
|
254
|
+
searchlogic_arel_alias :lte, :lteq
|
255
|
+
searchlogic_active_record_alias :in do |column, values|
|
256
|
+
has_nil = values.include?(nil)
|
257
|
+
values = values.flatten.compact
|
258
|
+
subs = [values]
|
259
|
+
if has_nil
|
260
|
+
subs << nil
|
261
|
+
end
|
262
|
+
where(column => searchlogic_extract_arel_compatible_value(subs))
|
263
|
+
end
|
264
|
+
searchlogic_active_record_alias :not_in do |column, values|
|
265
|
+
values = searchlogic_extract_arel_compatible_value(values.flatten)
|
266
|
+
query = values.map { "#{connection.quote_table_name(arel_table.name)}.#{connection.quote_column_name(column)} != ?" }.join(" AND ")
|
267
|
+
where(query, *values)
|
268
|
+
end
|
269
|
+
|
270
|
+
searchlogic_arel_alias :like, :matches, :map_value => -> (val) { "%#{val}%" }
|
271
|
+
searchlogic_arel_alias :begins_with, :matches, :map_value => -> (val) { "#{val}%" }
|
272
|
+
searchlogic_arel_alias :ends_with, :matches, :map_value => -> (val) { "%#{val}" }
|
273
|
+
searchlogic_arel_alias :not_like, :does_not_match, :map_value => -> (val) { "%#{val}%" }
|
274
|
+
searchlogic_arel_alias :not_begin_with, :does_not_match, :map_value => -> (val) { "#{val}%" }
|
275
|
+
searchlogic_arel_alias :not_end_with, :does_not_match, :map_value => -> (val) { "%#{val}" }
|
276
|
+
|
277
|
+
searchlogic_suffix_condition '_blank' do |column_name|
|
278
|
+
arel_table[column_name].eq(nil).or(arel_table[column_name].eq(''))
|
279
|
+
end
|
280
|
+
|
281
|
+
searchlogic_suffix_condition '_present' do |column_name|
|
282
|
+
arel_table[column_name].not_eq(nil).and(arel_table[column_name].not_eq(''))
|
283
|
+
end
|
284
|
+
|
285
|
+
null_matcher = lambda { |column_name| arel_table[column_name].eq(nil) }
|
286
|
+
searchlogic_suffix_condition '_null', &null_matcher
|
287
|
+
searchlogic_suffix_condition '_nil', &null_matcher
|
288
|
+
|
289
|
+
not_null_matcher = lambda { |column_name| arel_table[column_name].not_eq(nil) }
|
290
|
+
searchlogic_suffix_condition '_not_null', ¬_null_matcher
|
291
|
+
searchlogic_suffix_condition '_not_nil', ¬_null_matcher
|
292
|
+
|
293
|
+
searchlogic_prefix_condition 'descend_by_' do |column_name|
|
294
|
+
order(arel_table[column_name].desc)
|
295
|
+
end
|
296
|
+
|
297
|
+
searchlogic_prefix_condition 'ascend_by_' do |column_name|
|
298
|
+
order(arel_table[column_name].asc)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ModernSearchlogic
|
2
|
+
module Ordering
|
3
|
+
def self.included(base)
|
4
|
+
base.module_eval do
|
5
|
+
define_method(:order_with_modern_searchlogic) do |*args|
|
6
|
+
args.reduce(self) do |scope, arg|
|
7
|
+
expression = arg.to_s
|
8
|
+
if expression.match(/^(ascend|descend)_by_(.*)/) && respond_to?(expression)
|
9
|
+
scope.send(expression)
|
10
|
+
else
|
11
|
+
scope.order_without_modern_searchlogic(arg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
alias_method :order_without_modern_searchlogic, :order
|
16
|
+
alias_method :order, :order_with_modern_searchlogic
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ModernSearchlogic
|
2
|
+
module ScopeProcedure
|
3
|
+
def self.included(base)
|
4
|
+
base.singleton_class.class_eval do
|
5
|
+
def scope_procedure(name, options = nil)
|
6
|
+
if options.is_a?(Proc)
|
7
|
+
define_singleton_method(name, &options)
|
8
|
+
else
|
9
|
+
define_singleton_method(name) do |*args|
|
10
|
+
public_send(options, *args)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
self._defined_scopes << name.to_sym
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ModernSearchlogic
|
2
|
+
module ScopeTracking
|
3
|
+
module ClassMethods
|
4
|
+
def scope(name, body, &block)
|
5
|
+
super(name, body, &block).tap do |*|
|
6
|
+
self._defined_scopes |= [name.to_sym]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend ClassMethods
|
13
|
+
base.class_eval do
|
14
|
+
class_attribute :_defined_scopes
|
15
|
+
self._defined_scopes = Set.new
|
16
|
+
class_attribute :_dynamically_defined_searchlogic_scopes
|
17
|
+
self._dynamically_defined_searchlogic_scopes = {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module ModernSearchlogic
|
2
|
+
class Search
|
3
|
+
def self.search(model_class, options = {})
|
4
|
+
new(model_class).tap do |s|
|
5
|
+
options.each do |k, v|
|
6
|
+
k = k.to_sym
|
7
|
+
s.apply_search(k, v)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(model_class, options = {})
|
13
|
+
@model_class, @options = model_class, options
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(method, *)
|
17
|
+
super ||
|
18
|
+
searchlogic_default_scope.respond_to?(method) ||
|
19
|
+
search_scope_method?(method)
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_search(scope_name, value)
|
23
|
+
options.merge!(scope_name.to_sym => value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_search_value(scope_name)
|
27
|
+
options[scope_name]
|
28
|
+
end
|
29
|
+
|
30
|
+
def order=(new_order)
|
31
|
+
options[:order] = new_order
|
32
|
+
end
|
33
|
+
|
34
|
+
def order
|
35
|
+
options[:order]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :model_class, :options
|
41
|
+
delegate :searchlogic_default_scope, :valid_searchlogic_scope?, :to => :model_class
|
42
|
+
|
43
|
+
def materialize_scope
|
44
|
+
scope = searchlogic_default_scope
|
45
|
+
|
46
|
+
options.each do |k, v|
|
47
|
+
if k.to_s == 'order'
|
48
|
+
if model_class.valid_searchlogic_scope?(v) && model_class.searchlogic_method_arity(v).zero?
|
49
|
+
scope = scope.__send__(v)
|
50
|
+
end
|
51
|
+
elsif model_class.valid_searchlogic_scope?(k)
|
52
|
+
if model_class.searchlogic_method_arity(k).zero?
|
53
|
+
unless v.to_s == 'false'
|
54
|
+
scope = scope.__send__(k)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
scope = scope.__send__(k, *Array.wrap([v]))
|
58
|
+
end
|
59
|
+
else
|
60
|
+
scope = scope.where(k => v)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
scope
|
65
|
+
end
|
66
|
+
|
67
|
+
def search_scope_method?(method)
|
68
|
+
method.to_s =~ /\A(\S+?)(=)?\z/ && valid_searchlogic_scope?($1)
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(method, *args, &block)
|
72
|
+
if search_scope_method?(method)
|
73
|
+
applied_args = args.many? ? args : args.first
|
74
|
+
if method.to_s.ends_with?('=')
|
75
|
+
apply_search(method.to_s.chomp('='), applied_args)
|
76
|
+
else
|
77
|
+
if args.present? || model_class.searchlogic_method_arity(method).zero?
|
78
|
+
self.class.new(model_class, options.merge(method => args.present? ? applied_args : true))
|
79
|
+
else
|
80
|
+
get_search_value(method)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
elsif searchlogic_default_scope.respond_to?(method)
|
84
|
+
materialize_scope.__send__(method, *args, &block)
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: modern_searchlogic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Warner
|
8
|
+
- Reece Dunham
|
9
|
+
- Genius Tech Team
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2025-09-23 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.14
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: 3.2.14
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: appraisal
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rake
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
description: Because it's rampant through your codebase, and you can't upgrade to
|
58
|
+
Rails 3 otherwise.
|
59
|
+
email:
|
60
|
+
- wwarner.andrew@gmail.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- MIT-LICENSE
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- lib/modern_searchlogic.rb
|
69
|
+
- lib/modern_searchlogic/active_record_methods.rb
|
70
|
+
- lib/modern_searchlogic/column_conditions.rb
|
71
|
+
- lib/modern_searchlogic/default_scoping.rb
|
72
|
+
- lib/modern_searchlogic/ordering.rb
|
73
|
+
- lib/modern_searchlogic/railtie.rb
|
74
|
+
- lib/modern_searchlogic/scope_procedure.rb
|
75
|
+
- lib/modern_searchlogic/scope_tracking.rb
|
76
|
+
- lib/modern_searchlogic/search.rb
|
77
|
+
- lib/modern_searchlogic/searchable.rb
|
78
|
+
- lib/modern_searchlogic/version.rb
|
79
|
+
- lib/tasks/modern_searchlogic_tasks.rake
|
80
|
+
homepage: https://github.com/RDIL/modern_searchlogic
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
metadata: {}
|
84
|
+
post_install_message:
|
85
|
+
rdoc_options: []
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubygems_version: 3.5.22
|
100
|
+
signing_key:
|
101
|
+
specification_version: 4
|
102
|
+
summary: Searchlogic, but for AREL (Rails 3+).
|
103
|
+
test_files: []
|