pursuit 1.0.1 → 1.1.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/gemfiles/ruby-2.6.gemfile +14 -0
  3. data/.github/gemfiles/ruby-2.7.gemfile +13 -0
  4. data/.github/gemfiles/ruby-3.0.gemfile +13 -0
  5. data/.github/gemfiles/ruby-3.1.gemfile +13 -0
  6. data/.github/gemfiles/ruby-3.2.gemfile +13 -0
  7. data/.github/gemfiles/ruby-3.3.gemfile +13 -0
  8. data/.github/gemfiles/ruby-3.4.gemfile +13 -0
  9. data/.github/gemfiles/ruby-head.gemfile +13 -0
  10. data/.github/gemfiles/ruby-jruby-head.gemfile +13 -0
  11. data/.github/gemfiles/ruby-jruby.gemfile +13 -0
  12. data/.github/gemfiles/ruby-truffleruby-head.gemfile +13 -0
  13. data/.github/gemfiles/ruby-truffleruby.gemfile +13 -0
  14. data/.github/workflows/continuous-integration.yaml +65 -0
  15. data/.rbenv-gemsets +1 -1
  16. data/.ruby-version +1 -1
  17. data/Gemfile +7 -7
  18. data/Gemfile.lock +144 -124
  19. data/README.md +21 -19
  20. data/lib/pursuit/constants.rb +1 -1
  21. data/lib/pursuit/predicate_parser.rb +4 -4
  22. data/lib/pursuit/predicate_search.rb +26 -23
  23. data/lib/pursuit/predicate_transform.rb +1 -1
  24. data/lib/pursuit/simple_search.rb +15 -13
  25. data/lib/pursuit/term_search.rb +18 -13
  26. data/pursuit.gemspec +11 -11
  27. data/spec/internal/app/models/product.rb +2 -29
  28. data/spec/internal/app/models/product_category.rb +2 -22
  29. data/spec/internal/app/models/product_variation.rb +7 -26
  30. data/spec/internal/app/searches/product_category_search.rb +50 -0
  31. data/spec/internal/app/searches/product_search.rb +53 -0
  32. data/spec/internal/app/searches/product_variation_search.rb +53 -0
  33. data/spec/lib/pursuit/predicate_search_spec.rb +65 -29
  34. data/spec/lib/pursuit/predicate_transform_spec.rb +16 -1
  35. data/spec/lib/pursuit/simple_search_spec.rb +33 -19
  36. data/spec/lib/pursuit/term_search_spec.rb +33 -19
  37. metadata +19 -9
  38. data/.github/workflows/rubygem.yaml +0 -46
  39. data/travis/gemfiles/7.1.gemfile +0 -8
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductVariationSearch
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # Search for records matching the specified predicate.
8
+ #
9
+ # @param query [String] The query with a predicate.
10
+ # @return [ActiveRecord::Relation] The current relation filtered by the predicate.
11
+ #
12
+ def predicate_search(query)
13
+ @predicate_search ||= Pursuit::PredicateSearch.new(default_table: arel_table) do
14
+ permit_attribute :title
15
+ permit_attribute :stock_status
16
+ permit_attribute :currency
17
+ permit_attribute :amount
18
+ permit_attribute :product, Product.arel_table[:id]
19
+ permit_attribute :product_title, Product.arel_table[:title]
20
+ end
21
+
22
+ @predicate_search.apply(query, left_outer_joins(:product).group(:id))
23
+ end
24
+
25
+ # Search for records matching the specified terms.
26
+ #
27
+ # @param query [String] The query with one or more terms.
28
+ # @return [ActiveRecord::Relation] The current relation filtered by the terms.
29
+ #
30
+ def term_search(query)
31
+ @term_search ||= Pursuit::TermSearch.new(default_table: arel_table) do
32
+ search_attribute :title
33
+ end
34
+
35
+ # Note that we're using `all` here, but this still works when used in a chain:
36
+ # => ProductVariation.where(stock_status: :in_stock).search('Green')
37
+ @term_search.apply(query, all)
38
+ end
39
+
40
+ # Search for records matching the specified query.
41
+ #
42
+ # @param query [String] The query.
43
+ # @return [ActiveRecord::Relation] The current relation filtered by the query.
44
+ #
45
+ def search(query)
46
+ return none if query.blank?
47
+
48
+ predicate_search(query)
49
+ rescue Parslet::ParseFailed
50
+ term_search(query)
51
+ end
52
+ end
53
+ end
@@ -2,27 +2,66 @@
2
2
 
3
3
  RSpec.describe Pursuit::PredicateSearch do
4
4
  subject(:predicate_search) do
5
- described_class.new(
6
- Product.left_outer_joins(:variations).group(:id),
7
- permit_aggregate_modifiers: true
8
- ) do
9
- permit_attribute :title
10
- permit_attribute :variation, ProductVariation.arel_table[:id]
11
- permit_attribute :variation_title, ProductVariation.arel_table[:title]
12
- end
5
+ described_class.new(default_table: Product.arel_table, permit_aggregate_modifiers: true)
13
6
  end
14
7
 
15
8
  describe '#initialize' do
16
- it 'is expected to set #relation eq `relation`' do
17
- expect(predicate_search).to have_attributes(relation: Product.left_outer_joins(:variations).group(:id))
9
+ it 'is expected to set #default_table eq `default_table`' do
10
+ expect(predicate_search).to have_attributes(default_table: Product.arel_table)
18
11
  end
19
12
 
20
13
  it 'is expected to set #permit_aggregate_modifiers eq `permit_aggregate_modifiers`' do
21
14
  expect(predicate_search).to have_attributes(permit_aggregate_modifiers: true)
22
15
  end
23
16
 
24
- it 'is expected to evaluate the passed block' do
25
- expect(predicate_search.permitted_attributes).to be_present
17
+ it 'is expected to invoke the passed block' do
18
+ expect { |block| described_class.new(&block) }.to yield_control
19
+ end
20
+ end
21
+
22
+ describe '#permit_attribute' do
23
+ subject(:permit_attribute) { predicate_search.permit_attribute(name, attribute) }
24
+
25
+ context 'when `attribute` is nil' do
26
+ let(:name) { :title }
27
+ let(:attribute) { nil }
28
+
29
+ it 'is expected to add the attribute from #default_table to #permitted_attributes' do
30
+ permit_attribute
31
+ expect(predicate_search.permitted_attributes).to match(
32
+ hash_including(
33
+ title: Product.arel_table[:title]
34
+ )
35
+ )
36
+ end
37
+ end
38
+
39
+ context 'when `attribute` is a Symbol' do
40
+ let(:name) { :name }
41
+ let(:attribute) { :title }
42
+
43
+ it 'is expected to add the attribute from #default_table to #permitted_attributes' do
44
+ permit_attribute
45
+ expect(predicate_search.permitted_attributes).to match(
46
+ hash_including(
47
+ name: Product.arel_table[:title]
48
+ )
49
+ )
50
+ end
51
+ end
52
+
53
+ context 'when `attribute` is an Arel::Attributes::Attribute' do
54
+ let(:name) { :variation_currency }
55
+ let(:attribute) { ProductVariation.arel_table[:currency] }
56
+
57
+ it 'is expected to add the attribute to #permitted_attributes' do
58
+ permit_attribute
59
+ expect(predicate_search.permitted_attributes).to match(
60
+ hash_including(
61
+ variation_currency: ProductVariation.arel_table[:currency]
62
+ )
63
+ )
64
+ end
26
65
  end
27
66
  end
28
67
 
@@ -38,24 +77,15 @@ RSpec.describe Pursuit::PredicateSearch do
38
77
  it { is_expected.to be_a(Pursuit::PredicateTransform) }
39
78
  end
40
79
 
41
- describe '#permit_attribute' do
42
- subject(:permit_attribute) do
43
- predicate_search.permit_attribute(:variation_currency, ProductVariation.arel_table[:currency])
44
- end
45
-
46
- it 'is expected to add the attribute to #permitted_attributes' do
47
- permit_attribute
48
- expect(predicate_search.permitted_attributes).to match(
49
- hash_including(
50
- variation_currency: ProductVariation.arel_table[:currency]
51
- )
52
- )
53
- end
54
- end
55
-
56
80
  describe '#parse' do
57
81
  subject(:parse) { predicate_search.parse('title ~ Shirt & #variation > 0') }
58
82
 
83
+ before do
84
+ predicate_search.permitted_attributes[:title] = Product.arel_table[:title]
85
+ predicate_search.permitted_attributes[:variation] = ProductVariation.arel_table[:id]
86
+ predicate_search.permitted_attributes[:variation_title] = ProductVariation.arel_table[:title]
87
+ end
88
+
59
89
  it 'is expected to equal a Hash containing the ARel nodes' do
60
90
  expect(parse).to eq(
61
91
  {
@@ -67,9 +97,15 @@ RSpec.describe Pursuit::PredicateSearch do
67
97
  end
68
98
 
69
99
  describe '#apply' do
70
- subject(:apply) { predicate_search.apply('title ~ Shirt') }
100
+ subject(:apply) { predicate_search.apply('title ~ Shirt', Product.left_outer_joins(:variations).group(:id)) }
101
+
102
+ before do
103
+ predicate_search.permitted_attributes[:title] = Product.arel_table[:title]
104
+ predicate_search.permitted_attributes[:variation] = ProductVariation.arel_table[:id]
105
+ predicate_search.permitted_attributes[:variation_title] = ProductVariation.arel_table[:title]
106
+ end
71
107
 
72
- it 'is expected to equal #relation with predicate clauses applied' do
108
+ it 'is expected to equal `relation` with predicate clauses applied' do
73
109
  expect(apply).to eq(
74
110
  Product.left_outer_joins(:variations).group(:id).where(
75
111
  Product.arel_table[:title].matches('%Shirt%')
@@ -20,7 +20,8 @@ RSpec.describe Pursuit::PredicateTransform do
20
20
  variations: ProductVariation.arel_table[Arel.star],
21
21
  variation_title: ProductVariation.arel_table[:title],
22
22
  variation_currency: ProductVariation.arel_table[:currency],
23
- variation_amount: ProductVariation.arel_table[:amount]
23
+ variation_amount: ProductVariation.arel_table[:amount],
24
+ variation_count: ProductVariation.arel_table[Arel.star].count
24
25
  }.with_indifferent_access
25
26
  end
26
27
 
@@ -340,6 +341,20 @@ RSpec.describe Pursuit::PredicateTransform do
340
341
  it { expect { apply }.to raise_exception(Pursuit::AggregateModifierRequired) }
341
342
  end
342
343
 
344
+ context 'when passed a comparison with an attribute representing a custom node' do
345
+ let(:tree) do
346
+ {
347
+ attribute: { string_no_quotes: 'variation_count' },
348
+ comparator: '=',
349
+ value: { integer: '123' }
350
+ }
351
+ end
352
+
353
+ it 'is expected to equal the correct ARel node' do
354
+ expect(apply).to eq(ProductVariation.arel_table[Arel.star].count.eq(123))
355
+ end
356
+ end
357
+
343
358
  context 'when passed an aggregate comparison with the "=" comparator' do
344
359
  let(:tree) do
345
360
  {
@@ -1,39 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Pursuit::SimpleSearch do
4
- subject(:simple_search) do
5
- described_class.new(
6
- Product.left_outer_joins(:variations).group(:id)
7
- ) do
8
- search_attribute :title
9
- search_attribute ProductVariation.arel_table[:title]
10
- end
11
- end
4
+ subject(:simple_search) { described_class.new(default_table: Product.arel_table) }
12
5
 
13
6
  describe '#initialize' do
14
- it 'is expected to set #relation eq `relation`' do
15
- expect(simple_search).to have_attributes(relation: Product.left_outer_joins(:variations).group(:id))
7
+ it 'is expected to set #default_table eq `default_table`' do
8
+ expect(simple_search).to have_attributes(default_table: Product.arel_table)
16
9
  end
17
10
 
18
- it 'is expected to evaluate the passed block' do
19
- expect(simple_search.attributes).to be_present
11
+ it 'is expected to invoke the passed block' do
12
+ expect { |block| described_class.new(&block) }.to yield_control
20
13
  end
21
14
  end
22
15
 
23
16
  describe '#search_attribute' do
24
- subject(:search_attribute) do
25
- simple_search.search_attribute(ProductVariation.arel_table[:currency])
17
+ subject(:search_attribute) { simple_search.search_attribute(attribute) }
18
+
19
+ context 'when `attribute` is a Symbol' do
20
+ let(:attribute) { :title }
21
+
22
+ it 'is expected to add the attribute from #default_table to #attributes' do
23
+ search_attribute
24
+ expect(simple_search.attributes).to include(Product.arel_table[:title])
25
+ end
26
26
  end
27
27
 
28
- it 'is expected to add the attribute to #attributes' do
29
- search_attribute
30
- expect(simple_search.attributes).to include(ProductVariation.arel_table[:currency])
28
+ context 'when `attribute` is an Arel::Attributes::Attribute' do
29
+ let(:attribute) { ProductVariation.arel_table[:currency] }
30
+
31
+ it 'is expected to add the attribute to #attributes' do
32
+ search_attribute
33
+ expect(simple_search.attributes).to include(ProductVariation.arel_table[:currency])
34
+ end
31
35
  end
32
36
  end
33
37
 
34
38
  describe '#parse' do
35
39
  subject(:parse) { simple_search.parse('Shirt') }
36
40
 
41
+ before do
42
+ simple_search.attributes << Product.arel_table[:title]
43
+ simple_search.attributes << ProductVariation.arel_table[:title]
44
+ end
45
+
37
46
  it 'is expected to equal the ARel node' do
38
47
  expect(parse).to eq(
39
48
  Product.arel_table[:title].matches('%Shirt%').or(
@@ -44,9 +53,14 @@ RSpec.describe Pursuit::SimpleSearch do
44
53
  end
45
54
 
46
55
  describe '#apply' do
47
- subject(:apply) { simple_search.apply('Shirt') }
56
+ subject(:apply) { simple_search.apply('Shirt', Product.left_outer_joins(:variations).group(:id)) }
57
+
58
+ before do
59
+ simple_search.attributes << Product.arel_table[:title]
60
+ simple_search.attributes << ProductVariation.arel_table[:title]
61
+ end
48
62
 
49
- it 'is expected to equal #relation with clauses applied' do
63
+ it 'is expected to equal `relation` with simple clauses applied' do
50
64
  expect(apply).to eq(
51
65
  Product.left_outer_joins(:variations).group(:id).where(
52
66
  Product.arel_table[:title].matches('%Shirt%').or(
@@ -1,22 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Pursuit::TermSearch do
4
- subject(:term_search) do
5
- described_class.new(
6
- Product.left_outer_joins(:variations).group(:id)
7
- ) do
8
- search_attribute :title
9
- search_attribute ProductVariation.arel_table[:title]
10
- end
11
- end
4
+ subject(:term_search) { described_class.new(default_table: Product.arel_table) }
12
5
 
13
6
  describe '#initialize' do
14
- it 'is expected to set #relation eq `relation`' do
15
- expect(term_search).to have_attributes(relation: Product.left_outer_joins(:variations).group(:id))
7
+ it 'is expected to set #default_table eq `default_table`' do
8
+ expect(term_search).to have_attributes(default_table: Product.arel_table)
16
9
  end
17
10
 
18
- it 'is expected to evaluate the passed block' do
19
- expect(term_search.attributes).to be_present
11
+ it 'is expected to invoke the passed block' do
12
+ expect { |block| described_class.new(&block) }.to yield_control
20
13
  end
21
14
  end
22
15
 
@@ -33,19 +26,35 @@ RSpec.describe Pursuit::TermSearch do
33
26
  end
34
27
 
35
28
  describe '#search_attribute' do
36
- subject(:search_attribute) do
37
- term_search.search_attribute(ProductVariation.arel_table[:currency])
29
+ subject(:search_attribute) { term_search.search_attribute(attribute) }
30
+
31
+ context 'when `attribute` is a Symbol' do
32
+ let(:attribute) { :title }
33
+
34
+ it 'is expected to add the attribute from #default_table to #attributes' do
35
+ search_attribute
36
+ expect(term_search.attributes).to include(Product.arel_table[:title])
37
+ end
38
38
  end
39
39
 
40
- it 'is expected to add the attribute to #attributes' do
41
- search_attribute
42
- expect(term_search.attributes).to include(ProductVariation.arel_table[:currency])
40
+ context 'when `attribute` is an Arel::Attributes::Attribute' do
41
+ let(:attribute) { ProductVariation.arel_table[:currency] }
42
+
43
+ it 'is expected to add the attribute to #attributes' do
44
+ search_attribute
45
+ expect(term_search.attributes).to include(ProductVariation.arel_table[:currency])
46
+ end
43
47
  end
44
48
  end
45
49
 
46
50
  describe '#parse' do
47
51
  subject(:parse) { term_search.parse('Shirt') }
48
52
 
53
+ before do
54
+ term_search.attributes << Product.arel_table[:title]
55
+ term_search.attributes << ProductVariation.arel_table[:title]
56
+ end
57
+
49
58
  it 'is expected to equal the ARel node' do
50
59
  expect(parse).to eq(
51
60
  Product.arel_table[:title].matches('%Shirt%').or(
@@ -56,9 +65,14 @@ RSpec.describe Pursuit::TermSearch do
56
65
  end
57
66
 
58
67
  describe '#apply' do
59
- subject(:apply) { term_search.apply('Shirt') }
68
+ subject(:apply) { term_search.apply('Shirt', Product.left_outer_joins(:variations).group(:id)) }
69
+
70
+ before do
71
+ term_search.attributes << Product.arel_table[:title]
72
+ term_search.attributes << ProductVariation.arel_table[:title]
73
+ end
60
74
 
61
- it 'is expected to equal #relation with term clauses applied' do
75
+ it 'is expected to equal `relation` with term clauses applied' do
62
76
  expect(apply).to eq(
63
77
  Product.left_outer_joins(:variations).group(:id).where(
64
78
  Product.arel_table[:title].matches('%Shirt%').or(
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pursuit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nialto Services
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-11-02 00:00:00.000000000 Z
10
+ date: 2025-03-08 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -64,14 +63,25 @@ dependencies:
64
63
  - - "~>"
65
64
  - !ruby/object:Gem::Version
66
65
  version: '2.0'
67
- description:
68
66
  email:
69
67
  - support@nialtoservices.co.uk
70
68
  executables: []
71
69
  extensions: []
72
70
  extra_rdoc_files: []
73
71
  files:
74
- - ".github/workflows/rubygem.yaml"
72
+ - ".github/gemfiles/ruby-2.6.gemfile"
73
+ - ".github/gemfiles/ruby-2.7.gemfile"
74
+ - ".github/gemfiles/ruby-3.0.gemfile"
75
+ - ".github/gemfiles/ruby-3.1.gemfile"
76
+ - ".github/gemfiles/ruby-3.2.gemfile"
77
+ - ".github/gemfiles/ruby-3.3.gemfile"
78
+ - ".github/gemfiles/ruby-3.4.gemfile"
79
+ - ".github/gemfiles/ruby-head.gemfile"
80
+ - ".github/gemfiles/ruby-jruby-head.gemfile"
81
+ - ".github/gemfiles/ruby-jruby.gemfile"
82
+ - ".github/gemfiles/ruby-truffleruby-head.gemfile"
83
+ - ".github/gemfiles/ruby-truffleruby.gemfile"
84
+ - ".github/workflows/continuous-integration.yaml"
75
85
  - ".gitignore"
76
86
  - ".rbenv-gemsets"
77
87
  - ".rspec"
@@ -106,6 +116,9 @@ files:
106
116
  - spec/internal/app/models/product.rb
107
117
  - spec/internal/app/models/product_category.rb
108
118
  - spec/internal/app/models/product_variation.rb
119
+ - spec/internal/app/searches/product_category_search.rb
120
+ - spec/internal/app/searches/product_search.rb
121
+ - spec/internal/app/searches/product_variation_search.rb
109
122
  - spec/internal/config/database.yml
110
123
  - spec/internal/db/schema.rb
111
124
  - spec/internal/log/.keep
@@ -118,14 +131,12 @@ files:
118
131
  - spec/lib/pursuit/term_transform_spec.rb
119
132
  - spec/lib/pursuit_spec.rb
120
133
  - spec/spec_helper.rb
121
- - travis/gemfiles/7.1.gemfile
122
134
  homepage: https://github.com/NialtoServices/pursuit
123
135
  licenses:
124
136
  - Apache-2.0
125
137
  metadata:
126
138
  rubygems_mfa_required: 'true'
127
139
  yard.run: yri
128
- post_install_message:
129
140
  rdoc_options: []
130
141
  require_paths:
131
142
  - lib
@@ -140,8 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
151
  - !ruby/object:Gem::Version
141
152
  version: '0'
142
153
  requirements: []
143
- rubygems_version: 3.4.10
144
- signing_key:
154
+ rubygems_version: 3.6.2
145
155
  specification_version: 4
146
156
  summary: Advanced key-based searching for ActiveRecord objects.
147
157
  test_files: []
@@ -1,46 +0,0 @@
1
- name: RubyGem
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
-
7
- jobs:
8
- rubocop:
9
- name: RuboCop
10
- runs-on: ubuntu-latest
11
- steps:
12
- - name: Checkout Source Code
13
- uses: actions/checkout@v3
14
- - name: Setup Ruby
15
- uses: ruby/setup-ruby@v1
16
- - name: Setup RubyGems
17
- run: |
18
- bundle install
19
- - name: RuboCop
20
- run: |
21
- bundle exec rubocop --parallel --format progress --format html --out rubocop-report.html
22
- - name: Upload Report
23
- uses: actions/upload-artifact@v3
24
- with:
25
- name: RuboCop Report
26
- path: rubocop-report.html
27
-
28
- rspec:
29
- name: RSpec
30
- runs-on: ubuntu-latest
31
- steps:
32
- - name: Checkout Source Code
33
- uses: actions/checkout@v3
34
- - name: Setup Ruby
35
- uses: ruby/setup-ruby@v1
36
- - name: Setup RubyGems
37
- run: |
38
- bundle install
39
- - name: RSpec
40
- run: |
41
- bundle exec rspec --format progress --format html --out rspec-report.html
42
- - name: Upload Report
43
- uses: actions/upload-artifact@v3
44
- with:
45
- name: RSpec Report
46
- path: rspec-report.html
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- gemspec path: '../../'
6
-
7
- gem 'activerecord', '~> 7.1.0'
8
- gem 'activesupport', '~> 7.1.0'