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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubygem.yaml +46 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile +15 -0
  5. data/Gemfile.lock +127 -86
  6. data/LICENSE +174 -21
  7. data/README.md +210 -27
  8. data/bin/console +10 -0
  9. data/config.ru +2 -3
  10. data/lib/pursuit/aggregate_modifier_not_found.rb +20 -0
  11. data/lib/pursuit/aggregate_modifier_required.rb +20 -0
  12. data/lib/pursuit/aggregate_modifiers_not_available.rb +13 -0
  13. data/lib/pursuit/attribute_not_found.rb +20 -0
  14. data/lib/pursuit/constants.rb +1 -1
  15. data/lib/pursuit/error.rb +7 -0
  16. data/lib/pursuit/predicate_parser.rb +181 -0
  17. data/lib/pursuit/predicate_search.rb +83 -0
  18. data/lib/pursuit/predicate_transform.rb +231 -0
  19. data/lib/pursuit/query_error.rb +7 -0
  20. data/lib/pursuit/simple_search.rb +64 -0
  21. data/lib/pursuit/term_parser.rb +44 -0
  22. data/lib/pursuit/term_search.rb +69 -0
  23. data/lib/pursuit/term_transform.rb +35 -0
  24. data/lib/pursuit.rb +19 -5
  25. data/pursuit.gemspec +5 -18
  26. data/spec/internal/app/models/application_record.rb +5 -0
  27. data/spec/internal/app/models/product.rb +25 -9
  28. data/spec/internal/app/models/product_category.rb +23 -1
  29. data/spec/internal/app/models/product_variation.rb +26 -1
  30. data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
  31. data/spec/lib/pursuit/predicate_search_spec.rb +80 -0
  32. data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
  33. data/spec/lib/pursuit/simple_search_spec.rb +59 -0
  34. data/spec/lib/pursuit/term_parser_spec.rb +271 -0
  35. data/spec/lib/pursuit/term_search_spec.rb +71 -0
  36. data/spec/lib/pursuit/term_transform_spec.rb +105 -0
  37. data/spec/spec_helper.rb +2 -3
  38. data/travis/gemfiles/{5.2.gemfile → 7.1.gemfile} +2 -2
  39. metadata +38 -197
  40. data/.travis.yml +0 -25
  41. data/lib/pursuit/dsl.rb +0 -28
  42. data/lib/pursuit/railtie.rb +0 -13
  43. data/lib/pursuit/search.rb +0 -172
  44. data/lib/pursuit/search_options.rb +0 -86
  45. data/lib/pursuit/search_term_parser.rb +0 -46
  46. data/spec/lib/pursuit/dsl_spec.rb +0 -22
  47. data/spec/lib/pursuit/search_options_spec.rb +0 -146
  48. data/spec/lib/pursuit/search_spec.rb +0 -516
  49. data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
  50. data/travis/gemfiles/6.0.gemfile +0 -8
  51. data/travis/gemfiles/6.1.gemfile +0 -8
  52. 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.3
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-01-23 00:00:00.000000000 Z
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: 7.1.0
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: 7.1.0
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: 7.1.0
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: 7.1.0
52
+ version: 8.0.0
53
53
  - !ruby/object:Gem::Dependency
54
- name: bundler
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: :development
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/dsl.rb
260
- - lib/pursuit/railtie.rb
261
- - lib/pursuit/search.rb
262
- - lib/pursuit/search_options.rb
263
- - lib/pursuit/search_term_parser.rb
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/dsl_spec.rb
272
- - spec/lib/pursuit/search_options_spec.rb
273
- - spec/lib/pursuit/search_spec.rb
274
- - spec/lib/pursuit/search_term_parser_spec.rb
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/5.2.gemfile
278
- - travis/gemfiles/6.0.gemfile
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
- - MIT
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.1
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
@@ -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
@@ -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