pursuit 0.4.3 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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