pursuit 0.4.3 → 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 +4 -4
- data/.github/workflows/rubygem.yaml +46 -0
- data/.ruby-version +1 -1
- data/Gemfile +15 -0
- data/Gemfile.lock +127 -86
- data/LICENSE +174 -21
- data/README.md +210 -27
- data/bin/console +10 -0
- data/config.ru +2 -3
- data/lib/pursuit/aggregate_modifier_not_found.rb +20 -0
- data/lib/pursuit/aggregate_modifier_required.rb +20 -0
- data/lib/pursuit/aggregate_modifiers_not_available.rb +13 -0
- data/lib/pursuit/attribute_not_found.rb +20 -0
- data/lib/pursuit/constants.rb +1 -1
- data/lib/pursuit/error.rb +7 -0
- data/lib/pursuit/predicate_parser.rb +181 -0
- data/lib/pursuit/predicate_search.rb +83 -0
- data/lib/pursuit/predicate_transform.rb +231 -0
- data/lib/pursuit/query_error.rb +7 -0
- data/lib/pursuit/simple_search.rb +64 -0
- data/lib/pursuit/term_parser.rb +44 -0
- data/lib/pursuit/term_search.rb +69 -0
- data/lib/pursuit/term_transform.rb +35 -0
- data/lib/pursuit.rb +19 -5
- data/pursuit.gemspec +5 -18
- data/spec/internal/app/models/application_record.rb +5 -0
- data/spec/internal/app/models/product.rb +25 -9
- data/spec/internal/app/models/product_category.rb +23 -1
- data/spec/internal/app/models/product_variation.rb +26 -1
- data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
- data/spec/lib/pursuit/predicate_search_spec.rb +80 -0
- data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
- data/spec/lib/pursuit/simple_search_spec.rb +59 -0
- data/spec/lib/pursuit/term_parser_spec.rb +271 -0
- data/spec/lib/pursuit/term_search_spec.rb +71 -0
- data/spec/lib/pursuit/term_transform_spec.rb +105 -0
- data/spec/spec_helper.rb +2 -3
- data/travis/gemfiles/{5.2.gemfile → 7.1.gemfile} +2 -2
- metadata +38 -197
- data/.travis.yml +0 -25
- data/lib/pursuit/dsl.rb +0 -28
- data/lib/pursuit/railtie.rb +0 -13
- data/lib/pursuit/search.rb +0 -172
- data/lib/pursuit/search_options.rb +0 -86
- data/lib/pursuit/search_term_parser.rb +0 -46
- data/spec/lib/pursuit/dsl_spec.rb +0 -22
- data/spec/lib/pursuit/search_options_spec.rb +0 -146
- data/spec/lib/pursuit/search_spec.rb +0 -516
- data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
- data/travis/gemfiles/6.0.gemfile +0 -8
- data/travis/gemfiles/6.1.gemfile +0 -8
- data/travis/gemfiles/7.0.gemfile +0 -8
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pursuit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nialto Services
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,9 +17,9 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 5.2.0
|
20
|
-
- - "
|
20
|
+
- - "<="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
22
|
+
version: 8.0.0
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +27,9 @@ dependencies:
|
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: 5.2.0
|
30
|
-
- - "
|
30
|
+
- - "<="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 8.0.0
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: activesupport
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -37,9 +37,9 @@ dependencies:
|
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: 5.2.0
|
40
|
-
- - "
|
40
|
+
- - "<="
|
41
41
|
- !ruby/object:Gem::Version
|
42
|
-
version:
|
42
|
+
version: 8.0.0
|
43
43
|
type: :runtime
|
44
44
|
prerelease: false
|
45
45
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -47,191 +47,23 @@ dependencies:
|
|
47
47
|
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
49
|
version: 5.2.0
|
50
|
-
- - "
|
50
|
+
- - "<="
|
51
51
|
- !ruby/object:Gem::Version
|
52
|
-
version:
|
52
|
+
version: 8.0.0
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
|
-
name:
|
54
|
+
name: parslet
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
56
56
|
requirements:
|
57
57
|
- - "~>"
|
58
58
|
- !ruby/object:Gem::Version
|
59
59
|
version: '2.0'
|
60
|
-
type: :
|
60
|
+
type: :runtime
|
61
61
|
prerelease: false
|
62
62
|
version_requirements: !ruby/object:Gem::Requirement
|
63
63
|
requirements:
|
64
64
|
- - "~>"
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '2.0'
|
67
|
-
- !ruby/object:Gem::Dependency
|
68
|
-
name: combustion
|
69
|
-
requirement: !ruby/object:Gem::Requirement
|
70
|
-
requirements:
|
71
|
-
- - "~>"
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
version: '1.3'
|
74
|
-
type: :development
|
75
|
-
prerelease: false
|
76
|
-
version_requirements: !ruby/object:Gem::Requirement
|
77
|
-
requirements:
|
78
|
-
- - "~>"
|
79
|
-
- !ruby/object:Gem::Version
|
80
|
-
version: '1.3'
|
81
|
-
- !ruby/object:Gem::Dependency
|
82
|
-
name: guard
|
83
|
-
requirement: !ruby/object:Gem::Requirement
|
84
|
-
requirements:
|
85
|
-
- - "~>"
|
86
|
-
- !ruby/object:Gem::Version
|
87
|
-
version: '2.18'
|
88
|
-
type: :development
|
89
|
-
prerelease: false
|
90
|
-
version_requirements: !ruby/object:Gem::Requirement
|
91
|
-
requirements:
|
92
|
-
- - "~>"
|
93
|
-
- !ruby/object:Gem::Version
|
94
|
-
version: '2.18'
|
95
|
-
- !ruby/object:Gem::Dependency
|
96
|
-
name: guard-rspec
|
97
|
-
requirement: !ruby/object:Gem::Requirement
|
98
|
-
requirements:
|
99
|
-
- - "~>"
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
version: '4.7'
|
102
|
-
type: :development
|
103
|
-
prerelease: false
|
104
|
-
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
requirements:
|
106
|
-
- - "~>"
|
107
|
-
- !ruby/object:Gem::Version
|
108
|
-
version: '4.7'
|
109
|
-
- !ruby/object:Gem::Dependency
|
110
|
-
name: pry
|
111
|
-
requirement: !ruby/object:Gem::Requirement
|
112
|
-
requirements:
|
113
|
-
- - "~>"
|
114
|
-
- !ruby/object:Gem::Version
|
115
|
-
version: '0.14'
|
116
|
-
type: :development
|
117
|
-
prerelease: false
|
118
|
-
version_requirements: !ruby/object:Gem::Requirement
|
119
|
-
requirements:
|
120
|
-
- - "~>"
|
121
|
-
- !ruby/object:Gem::Version
|
122
|
-
version: '0.14'
|
123
|
-
- !ruby/object:Gem::Dependency
|
124
|
-
name: rake
|
125
|
-
requirement: !ruby/object:Gem::Requirement
|
126
|
-
requirements:
|
127
|
-
- - "~>"
|
128
|
-
- !ruby/object:Gem::Version
|
129
|
-
version: '13.0'
|
130
|
-
type: :development
|
131
|
-
prerelease: false
|
132
|
-
version_requirements: !ruby/object:Gem::Requirement
|
133
|
-
requirements:
|
134
|
-
- - "~>"
|
135
|
-
- !ruby/object:Gem::Version
|
136
|
-
version: '13.0'
|
137
|
-
- !ruby/object:Gem::Dependency
|
138
|
-
name: rspec
|
139
|
-
requirement: !ruby/object:Gem::Requirement
|
140
|
-
requirements:
|
141
|
-
- - "~>"
|
142
|
-
- !ruby/object:Gem::Version
|
143
|
-
version: '3.12'
|
144
|
-
type: :development
|
145
|
-
prerelease: false
|
146
|
-
version_requirements: !ruby/object:Gem::Requirement
|
147
|
-
requirements:
|
148
|
-
- - "~>"
|
149
|
-
- !ruby/object:Gem::Version
|
150
|
-
version: '3.12'
|
151
|
-
- !ruby/object:Gem::Dependency
|
152
|
-
name: rspec-rails
|
153
|
-
requirement: !ruby/object:Gem::Requirement
|
154
|
-
requirements:
|
155
|
-
- - "~>"
|
156
|
-
- !ruby/object:Gem::Version
|
157
|
-
version: '6.0'
|
158
|
-
type: :development
|
159
|
-
prerelease: false
|
160
|
-
version_requirements: !ruby/object:Gem::Requirement
|
161
|
-
requirements:
|
162
|
-
- - "~>"
|
163
|
-
- !ruby/object:Gem::Version
|
164
|
-
version: '6.0'
|
165
|
-
- !ruby/object:Gem::Dependency
|
166
|
-
name: rubocop
|
167
|
-
requirement: !ruby/object:Gem::Requirement
|
168
|
-
requirements:
|
169
|
-
- - "~>"
|
170
|
-
- !ruby/object:Gem::Version
|
171
|
-
version: '1.44'
|
172
|
-
type: :development
|
173
|
-
prerelease: false
|
174
|
-
version_requirements: !ruby/object:Gem::Requirement
|
175
|
-
requirements:
|
176
|
-
- - "~>"
|
177
|
-
- !ruby/object:Gem::Version
|
178
|
-
version: '1.44'
|
179
|
-
- !ruby/object:Gem::Dependency
|
180
|
-
name: rubocop-rake
|
181
|
-
requirement: !ruby/object:Gem::Requirement
|
182
|
-
requirements:
|
183
|
-
- - "~>"
|
184
|
-
- !ruby/object:Gem::Version
|
185
|
-
version: '0.6'
|
186
|
-
type: :development
|
187
|
-
prerelease: false
|
188
|
-
version_requirements: !ruby/object:Gem::Requirement
|
189
|
-
requirements:
|
190
|
-
- - "~>"
|
191
|
-
- !ruby/object:Gem::Version
|
192
|
-
version: '0.6'
|
193
|
-
- !ruby/object:Gem::Dependency
|
194
|
-
name: rubocop-rspec
|
195
|
-
requirement: !ruby/object:Gem::Requirement
|
196
|
-
requirements:
|
197
|
-
- - "~>"
|
198
|
-
- !ruby/object:Gem::Version
|
199
|
-
version: '2.18'
|
200
|
-
type: :development
|
201
|
-
prerelease: false
|
202
|
-
version_requirements: !ruby/object:Gem::Requirement
|
203
|
-
requirements:
|
204
|
-
- - "~>"
|
205
|
-
- !ruby/object:Gem::Version
|
206
|
-
version: '2.18'
|
207
|
-
- !ruby/object:Gem::Dependency
|
208
|
-
name: sqlite3
|
209
|
-
requirement: !ruby/object:Gem::Requirement
|
210
|
-
requirements:
|
211
|
-
- - "~>"
|
212
|
-
- !ruby/object:Gem::Version
|
213
|
-
version: '1.6'
|
214
|
-
type: :development
|
215
|
-
prerelease: false
|
216
|
-
version_requirements: !ruby/object:Gem::Requirement
|
217
|
-
requirements:
|
218
|
-
- - "~>"
|
219
|
-
- !ruby/object:Gem::Version
|
220
|
-
version: '1.6'
|
221
|
-
- !ruby/object:Gem::Dependency
|
222
|
-
name: yard
|
223
|
-
requirement: !ruby/object:Gem::Requirement
|
224
|
-
requirements:
|
225
|
-
- - "~>"
|
226
|
-
- !ruby/object:Gem::Version
|
227
|
-
version: '0.9'
|
228
|
-
type: :development
|
229
|
-
prerelease: false
|
230
|
-
version_requirements: !ruby/object:Gem::Requirement
|
231
|
-
requirements:
|
232
|
-
- - "~>"
|
233
|
-
- !ruby/object:Gem::Version
|
234
|
-
version: '0.9'
|
235
67
|
description:
|
236
68
|
email:
|
237
69
|
- support@nialtoservices.co.uk
|
@@ -239,12 +71,12 @@ executables: []
|
|
239
71
|
extensions: []
|
240
72
|
extra_rdoc_files: []
|
241
73
|
files:
|
74
|
+
- ".github/workflows/rubygem.yaml"
|
242
75
|
- ".gitignore"
|
243
76
|
- ".rbenv-gemsets"
|
244
77
|
- ".rspec"
|
245
78
|
- ".rubocop.yml"
|
246
79
|
- ".ruby-version"
|
247
|
-
- ".travis.yml"
|
248
80
|
- Gemfile
|
249
81
|
- Gemfile.lock
|
250
82
|
- Guardfile
|
@@ -255,32 +87,41 @@ files:
|
|
255
87
|
- bin/setup
|
256
88
|
- config.ru
|
257
89
|
- lib/pursuit.rb
|
90
|
+
- lib/pursuit/aggregate_modifier_not_found.rb
|
91
|
+
- lib/pursuit/aggregate_modifier_required.rb
|
92
|
+
- lib/pursuit/aggregate_modifiers_not_available.rb
|
93
|
+
- lib/pursuit/attribute_not_found.rb
|
258
94
|
- lib/pursuit/constants.rb
|
259
|
-
- lib/pursuit/
|
260
|
-
- lib/pursuit/
|
261
|
-
- lib/pursuit/
|
262
|
-
- lib/pursuit/
|
263
|
-
- lib/pursuit/
|
95
|
+
- lib/pursuit/error.rb
|
96
|
+
- lib/pursuit/predicate_parser.rb
|
97
|
+
- lib/pursuit/predicate_search.rb
|
98
|
+
- lib/pursuit/predicate_transform.rb
|
99
|
+
- lib/pursuit/query_error.rb
|
100
|
+
- lib/pursuit/simple_search.rb
|
101
|
+
- lib/pursuit/term_parser.rb
|
102
|
+
- lib/pursuit/term_search.rb
|
103
|
+
- lib/pursuit/term_transform.rb
|
264
104
|
- pursuit.gemspec
|
105
|
+
- spec/internal/app/models/application_record.rb
|
265
106
|
- spec/internal/app/models/product.rb
|
266
107
|
- spec/internal/app/models/product_category.rb
|
267
108
|
- spec/internal/app/models/product_variation.rb
|
268
109
|
- spec/internal/config/database.yml
|
269
110
|
- spec/internal/db/schema.rb
|
270
111
|
- spec/internal/log/.keep
|
271
|
-
- spec/lib/pursuit/
|
272
|
-
- spec/lib/pursuit/
|
273
|
-
- spec/lib/pursuit/
|
274
|
-
- spec/lib/pursuit/
|
112
|
+
- spec/lib/pursuit/predicate_parser_spec.rb
|
113
|
+
- spec/lib/pursuit/predicate_search_spec.rb
|
114
|
+
- spec/lib/pursuit/predicate_transform_spec.rb
|
115
|
+
- spec/lib/pursuit/simple_search_spec.rb
|
116
|
+
- spec/lib/pursuit/term_parser_spec.rb
|
117
|
+
- spec/lib/pursuit/term_search_spec.rb
|
118
|
+
- spec/lib/pursuit/term_transform_spec.rb
|
275
119
|
- spec/lib/pursuit_spec.rb
|
276
120
|
- spec/spec_helper.rb
|
277
|
-
- travis/gemfiles/
|
278
|
-
|
279
|
-
- travis/gemfiles/6.1.gemfile
|
280
|
-
- travis/gemfiles/7.0.gemfile
|
281
|
-
homepage: https://github.com/nialtoservices/pursuit
|
121
|
+
- travis/gemfiles/7.1.gemfile
|
122
|
+
homepage: https://github.com/NialtoServices/pursuit
|
282
123
|
licenses:
|
283
|
-
-
|
124
|
+
- Apache-2.0
|
284
125
|
metadata:
|
285
126
|
rubygems_mfa_required: 'true'
|
286
127
|
yard.run: yri
|
@@ -299,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
299
140
|
- !ruby/object:Gem::Version
|
300
141
|
version: '0'
|
301
142
|
requirements: []
|
302
|
-
rubygems_version: 3.4.
|
143
|
+
rubygems_version: 3.4.10
|
303
144
|
signing_key:
|
304
145
|
specification_version: 4
|
305
146
|
summary: Advanced key-based searching for ActiveRecord objects.
|
data/.travis.yml
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
rvm:
|
3
|
-
- 3.2.0
|
4
|
-
gemfile:
|
5
|
-
- travis/gemfiles/5.2.gemfile
|
6
|
-
- travis/gemfiles/6.0.gemfile
|
7
|
-
- travis/gemfiles/6.1.gemfile
|
8
|
-
- travis/gemfiles/7.0.gemfile
|
9
|
-
services:
|
10
|
-
- postgresql
|
11
|
-
before_install:
|
12
|
-
- gem update --system
|
13
|
-
- gem install bundler
|
14
|
-
before_script:
|
15
|
-
- psql -c 'CREATE DATABASE pursuit_test;' -U postgres
|
16
|
-
addons:
|
17
|
-
postgresql: 14
|
18
|
-
apt:
|
19
|
-
packages:
|
20
|
-
- postgresql-14
|
21
|
-
- postgresql-client-14
|
22
|
-
env:
|
23
|
-
global:
|
24
|
-
- DATABASE_URL="postgresql://127.0.0.1:5432/pursuit_test"
|
25
|
-
- RSPEC_DEFAULT_FORMATTER=doc
|
data/lib/pursuit/dsl.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pursuit
|
4
|
-
module DSL
|
5
|
-
def self.included(base)
|
6
|
-
base.extend ClassMethods
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def searchable(&block)
|
11
|
-
if respond_to?(:search_options) || respond_to?(:search)
|
12
|
-
raise "#{self} already has #search and #search_options defined."
|
13
|
-
end
|
14
|
-
|
15
|
-
options = SearchOptions.new(self, &block)
|
16
|
-
|
17
|
-
define_singleton_method(:search_options) do
|
18
|
-
options
|
19
|
-
end
|
20
|
-
|
21
|
-
define_singleton_method(:search) do |query|
|
22
|
-
search = Pursuit::Search.new(options)
|
23
|
-
search.perform(query)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/pursuit/railtie.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pursuit
|
4
|
-
class Railtie < Rails::Railtie
|
5
|
-
initializer 'pursuit.active_record.inject_dsl' do
|
6
|
-
ActiveSupport.on_load(:active_record) do
|
7
|
-
require 'pursuit/dsl'
|
8
|
-
|
9
|
-
ActiveRecord::Base.include(Pursuit::DSL)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
data/lib/pursuit/search.rb
DELETED
@@ -1,172 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pursuit
|
4
|
-
class Search
|
5
|
-
# @return [SearchOptions] The options to use when building the search query.
|
6
|
-
#
|
7
|
-
attr_reader :options
|
8
|
-
|
9
|
-
# Create a new instance to search a specific ActiveRecord record class.
|
10
|
-
#
|
11
|
-
# @param options [SearchOptions]
|
12
|
-
#
|
13
|
-
def initialize(options)
|
14
|
-
@options = options
|
15
|
-
end
|
16
|
-
|
17
|
-
# Perform a search for the specified query.
|
18
|
-
#
|
19
|
-
# @param query [String] The query to transform into a SQL search.
|
20
|
-
# @return [ActiveRecord::Relation] The search results.
|
21
|
-
#
|
22
|
-
def perform(query)
|
23
|
-
options.record_class.where(build_arel(query))
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def build_arel(query)
|
29
|
-
parser = SearchTermParser.new(query, keys: options.keys)
|
30
|
-
unkeyed_arel = build_arel_for_unkeyed_term(parser.unkeyed_term)
|
31
|
-
keyed_arel = build_arel_for_keyed_terms(parser.keyed_terms)
|
32
|
-
|
33
|
-
if unkeyed_arel && keyed_arel
|
34
|
-
unkeyed_arel.and(keyed_arel)
|
35
|
-
else
|
36
|
-
unkeyed_arel || keyed_arel
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def build_arel_for_unkeyed_term(value)
|
41
|
-
return nil if value.blank?
|
42
|
-
|
43
|
-
sanitized_value = "%#{ActiveRecord::Base.sanitize_sql_like(value)}%"
|
44
|
-
options.unkeyed_attributes.reduce(nil) do |chain, (_attribute_name, node_builder)|
|
45
|
-
node = node_builder.call.matches(sanitized_value)
|
46
|
-
chain ? chain.or(node) : node
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def build_arel_for_keyed_terms(terms)
|
51
|
-
return nil if terms.blank?
|
52
|
-
|
53
|
-
terms.reduce(nil) do |chain, term|
|
54
|
-
attribute_name = term.key.to_sym
|
55
|
-
reflection = options.relations.key?(attribute_name) ? options.record_class.reflections[term.key] : nil
|
56
|
-
node = if reflection.present?
|
57
|
-
attribute_names = options.relations[attribute_name]
|
58
|
-
build_arel_for_reflection(reflection, attribute_names, term.operator, term.value)
|
59
|
-
else
|
60
|
-
node_builder = options.keyed_attributes[attribute_name]
|
61
|
-
build_arel_for_node(node_builder.call, term.operator, term.value)
|
62
|
-
end
|
63
|
-
|
64
|
-
chain ? chain.and(node) : node
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def build_arel_for_node(node, operator, value) # rubocop:disable Metrics/CyclomaticComplexity
|
69
|
-
sanitized_value = ActiveRecord::Base.sanitize_sql_like(value)
|
70
|
-
sanitized_value = sanitized_value.to_i if sanitized_value =~ /^[0-9]+$/
|
71
|
-
|
72
|
-
case operator
|
73
|
-
when '>' then node.gt(sanitized_value)
|
74
|
-
when '>=' then node.gteq(sanitized_value)
|
75
|
-
when '<' then node.lt(sanitized_value)
|
76
|
-
when '<=' then node.lteq(sanitized_value)
|
77
|
-
when '*=' then node.matches("%#{sanitized_value}%")
|
78
|
-
when '!*=' then node.does_not_match("%#{sanitized_value}%")
|
79
|
-
when '!=' then node.not_eq(sanitized_value)
|
80
|
-
when '=='
|
81
|
-
if value.present?
|
82
|
-
node.eq(sanitized_value)
|
83
|
-
else
|
84
|
-
node.eq(nil).or(node.eq(''))
|
85
|
-
end
|
86
|
-
else
|
87
|
-
raise "The operator '#{operator}' is not supported."
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def build_arel_for_reflection(reflection, attribute_names, operator, value)
|
92
|
-
nodes = build_arel_for_reflection_join(reflection)
|
93
|
-
count_nodes = build_arel_for_relation_count(nodes, operator, value) unless reflection.belongs_to?
|
94
|
-
return count_nodes if count_nodes.present?
|
95
|
-
|
96
|
-
match_nodes = attribute_names.reduce(nil) do |chain, attribute_name|
|
97
|
-
node = build_arel_for_node(reflection.klass.arel_table[attribute_name], operator, value)
|
98
|
-
chain ? chain.or(node) : node
|
99
|
-
end
|
100
|
-
|
101
|
-
return nil if match_nodes.blank?
|
102
|
-
|
103
|
-
nodes.where(match_nodes).project(1).exists
|
104
|
-
end
|
105
|
-
|
106
|
-
def build_arel_for_reflection_join(reflection)
|
107
|
-
if reflection.belongs_to?
|
108
|
-
build_arel_for_belongs_to_reflection_join(reflection)
|
109
|
-
else
|
110
|
-
build_arel_for_has_reflection_join(reflection)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def build_arel_for_belongs_to_reflection_join(reflection)
|
115
|
-
reflection_table = reflection.klass.arel_table
|
116
|
-
reflection_table.where(
|
117
|
-
reflection_table[reflection.join_primary_key].eq(
|
118
|
-
options.record_class.arel_table[reflection.join_foreign_key]
|
119
|
-
)
|
120
|
-
)
|
121
|
-
end
|
122
|
-
|
123
|
-
# rubocop:disable Metrics/AbcSize
|
124
|
-
def build_arel_for_has_reflection_join(reflection)
|
125
|
-
reflection_table = reflection.klass.arel_table
|
126
|
-
reflection_through = reflection.through_reflection
|
127
|
-
|
128
|
-
if reflection_through.present?
|
129
|
-
# :has_one through / :has_many through
|
130
|
-
reflection_through_table = reflection_through.klass.arel_table
|
131
|
-
reflection_table.join(reflection_through_table).on(
|
132
|
-
reflection_through_table[reflection.foreign_key].eq(reflection_table[reflection.klass.primary_key])
|
133
|
-
).where(
|
134
|
-
reflection_through_table[reflection_through.foreign_key].eq(
|
135
|
-
options.record_class.arel_table[options.record_class.primary_key]
|
136
|
-
)
|
137
|
-
)
|
138
|
-
else
|
139
|
-
# :has_one / :has_many
|
140
|
-
reflection_table.where(
|
141
|
-
reflection_table[reflection.foreign_key].eq(
|
142
|
-
options.record_class.arel_table[options.record_class.primary_key]
|
143
|
-
)
|
144
|
-
)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
# rubocop:enable Metrics/AbcSize
|
148
|
-
|
149
|
-
def build_arel_for_relation_count(nodes, operator, value) # rubocop:disable Metrics/CyclomaticComplexity
|
150
|
-
node_builder = proc do |klass|
|
151
|
-
count = ActiveRecord::Base.sanitize_sql_like(value).to_i
|
152
|
-
klass.new(nodes.project(Arel.star.count), count)
|
153
|
-
end
|
154
|
-
|
155
|
-
case operator
|
156
|
-
when '>' then node_builder.call(Arel::Nodes::GreaterThan)
|
157
|
-
when '>=' then node_builder.call(Arel::Nodes::GreaterThanOrEqual)
|
158
|
-
when '<' then node_builder.call(Arel::Nodes::LessThan)
|
159
|
-
when '<=' then node_builder.call(Arel::Nodes::LessThanOrEqual)
|
160
|
-
else
|
161
|
-
return nil unless value =~ /^([0-9]+)$/
|
162
|
-
|
163
|
-
case operator
|
164
|
-
when '==' then node_builder.call(Arel::Nodes::Equality)
|
165
|
-
when '!=' then node_builder.call(Arel::Nodes::NotEqual)
|
166
|
-
when '*=' then node_builder.call(Arel::Nodes::Matches)
|
167
|
-
when '!*=' then node_builder.call(Arel::Nodes::DoesNotMatch)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
@@ -1,86 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pursuit
|
4
|
-
class SearchOptions
|
5
|
-
# @return [Struct] The structure which holds the search options for a single attribute.
|
6
|
-
#
|
7
|
-
AttributeOptions = Struct.new(:keyed, :unkeyed, :block)
|
8
|
-
|
9
|
-
# @return [Class<ActiveRecord::Base>] The `ActiveRecord::Base` child class to search.
|
10
|
-
#
|
11
|
-
attr_reader :record_class
|
12
|
-
|
13
|
-
# @return [Hash<Symbol, Array<Symbol>>] The attribute names of the record's relatives which can be searched.
|
14
|
-
#
|
15
|
-
attr_reader :relations
|
16
|
-
|
17
|
-
# @return [Hash<Symbol, AttributeOptions>] The attributes which can be searched.
|
18
|
-
#
|
19
|
-
attr_reader :attributes
|
20
|
-
|
21
|
-
# Create a new `SearchOptions` and call the passed block to setup the options.
|
22
|
-
#
|
23
|
-
# @params record_class [Class<ActiveRecord::Base>]
|
24
|
-
# @params block [Proc]
|
25
|
-
#
|
26
|
-
def initialize(record_class, &block)
|
27
|
-
@record_class = record_class
|
28
|
-
@relations = {}
|
29
|
-
@attributes = {}
|
30
|
-
|
31
|
-
block&.call(self)
|
32
|
-
end
|
33
|
-
|
34
|
-
# @return [Hash<Symbol, Proc>] The attributes which can be queried using a keyed term.
|
35
|
-
#
|
36
|
-
def keyed_attributes
|
37
|
-
attributes.each_with_object({}) do |(name, options), keyed_attributes|
|
38
|
-
keyed_attributes[name] = options.block if options.keyed
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# @return [Hash<Symbol, Proc>] The attributes which can be queried using an unkeyed term.
|
43
|
-
#
|
44
|
-
def unkeyed_attributes
|
45
|
-
attributes.each_with_object({}) do |(name, options), unkeyed_attributes|
|
46
|
-
unkeyed_attributes[name] = options.block if options.unkeyed
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# @return [Array<String>] The collection of all possible attributes which can be used as a keyed term.
|
51
|
-
#
|
52
|
-
def keys
|
53
|
-
keys = relations.keys + attributes.select { |_, a| a.keyed }.keys
|
54
|
-
keys.map(&:to_s).uniq
|
55
|
-
end
|
56
|
-
|
57
|
-
# Add a relation to search.
|
58
|
-
#
|
59
|
-
# @param name [Symbol] The name of the relationship attribute.
|
60
|
-
# @param attribute_names [Splat] The name of the attributes within the relationship to search.
|
61
|
-
#
|
62
|
-
def relation(name, *attribute_names)
|
63
|
-
relations[name] = attribute_names
|
64
|
-
nil
|
65
|
-
end
|
66
|
-
|
67
|
-
# Add an attribute to search.
|
68
|
-
#
|
69
|
-
# @param term_name [Symbol] The keyed search term (can be an existing attribute, or a custom value when
|
70
|
-
# passing either the `attribute_name` or a block returning an Arel node).
|
71
|
-
# @param attribute_name [Symbol] The attribute name to search (defaults to the keyword, when left blank and no
|
72
|
-
# block is passed).
|
73
|
-
# @param keyed [Boolean] `true` when the attribute should be searchable using a keyed term,
|
74
|
-
# `false` otherwise.
|
75
|
-
# @param unkeyed [Boolean] `true` when the attribute should be searchable using an unkeyed term,
|
76
|
-
# `false` otherwise.
|
77
|
-
# @param block [Proc] A block which returns the Arel node to query against. When left blank, the
|
78
|
-
# matching attribute from `.arel_table` is queried instead.
|
79
|
-
#
|
80
|
-
def attribute(term_name, attribute_name = nil, keyed: true, unkeyed: true, &block)
|
81
|
-
block ||= -> { record_class.arel_table[attribute_name || term_name] }
|
82
|
-
attributes[term_name] = AttributeOptions.new(keyed, unkeyed, block)
|
83
|
-
nil
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pursuit
|
4
|
-
class SearchTermParser
|
5
|
-
# @return [Struct] Represents a single keyed term extracted from a query.
|
6
|
-
#
|
7
|
-
KeyedTerm = Struct.new(:key, :operator, :value)
|
8
|
-
|
9
|
-
# @return [Array<Pursuit::SearchTermParser::KeyedTerm>] The keys which are permitted for use as keyed terms.
|
10
|
-
#
|
11
|
-
attr_reader :keyed_terms
|
12
|
-
|
13
|
-
# @return [String] The unkeyed term.
|
14
|
-
#
|
15
|
-
attr_reader :unkeyed_term
|
16
|
-
|
17
|
-
# Create a new search term parser by parsing the specified query into an 'unkeyed term' and 'keyed terms'.
|
18
|
-
#
|
19
|
-
# @param query [String] The query to parse.
|
20
|
-
# @param keys [Array<String>] The keys which are permitted for use as keyed terms.
|
21
|
-
#
|
22
|
-
def initialize(query, keys: [])
|
23
|
-
@keyed_terms = []
|
24
|
-
@unkeyed_term = query.gsub(/(\s+)?(\w+)(==|\*=|!=|!\*=|<=|>=|<|>)("([^"]+)?"|'([^']+)?'|[^\s]+)(\s+)?/) do |term|
|
25
|
-
key = Regexp.last_match(2)
|
26
|
-
next term unless keys.include?(key)
|
27
|
-
|
28
|
-
operator = Regexp.last_match(3)
|
29
|
-
value = Regexp.last_match(4)
|
30
|
-
value = value[1..-2] if value =~ /^(".*"|'.*')$/
|
31
|
-
|
32
|
-
@keyed_terms << KeyedTerm.new(key, operator, value)
|
33
|
-
|
34
|
-
# Both the starting and ending spaces surrounding the keyed term can be removed, so in this case we'll need to
|
35
|
-
# replace with a single space to ensure the unkeyed term's words are separated correctly.
|
36
|
-
if term =~ /^\s.*\s$/
|
37
|
-
' '
|
38
|
-
else
|
39
|
-
''
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
@unkeyed_term = @unkeyed_term.strip
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|