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 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', &not_null_matcher
291
+ searchlogic_suffix_condition '_not_nil', &not_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,13 @@
1
+ module ModernSearchlogic
2
+ module DefaultScoping
3
+ module ClassMethods
4
+ def searchlogic_default_scope
5
+ respond_to?(:scoped) ? scoped : all
6
+ end
7
+ end
8
+
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+ end
13
+ 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,9 @@
1
+ module ModernSearchlogic
2
+ class Railtie < Rails::Railtie
3
+ initializer 'modern_searchlogic.setup_activerecord' do
4
+ ActiveSupport.on_load(:active_record) do
5
+ ModernSearchlogic::ActiveRecordMethods.install
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,15 @@
1
+ require_relative 'search'
2
+
3
+ module ModernSearchlogic
4
+ module Searchable
5
+ module ClassMethods
6
+ def search(options = {})
7
+ Search.search(self, options)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend ClassMethods
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernSearchlogic
4
+ VERSION = "1.0.1"
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'modern_searchlogic/active_record_methods'
2
+ require 'modern_searchlogic/railtie'
3
+
4
+ module ModernSearchlogic
5
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :modern_searchlogic do
3
+ # # Task goes here
4
+ # 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: []