pursuit 0.4.5 → 1.1.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubygem.yaml +46 -0
  3. data/Gemfile +14 -13
  4. data/Gemfile.lock +14 -13
  5. data/README.md +210 -27
  6. data/bin/console +10 -0
  7. data/lib/pursuit/aggregate_modifier_not_found.rb +20 -0
  8. data/lib/pursuit/aggregate_modifier_required.rb +20 -0
  9. data/lib/pursuit/aggregate_modifiers_not_available.rb +13 -0
  10. data/lib/pursuit/attribute_not_found.rb +20 -0
  11. data/lib/pursuit/constants.rb +1 -1
  12. data/lib/pursuit/error.rb +7 -0
  13. data/lib/pursuit/predicate_parser.rb +181 -0
  14. data/lib/pursuit/predicate_search.rb +86 -0
  15. data/lib/pursuit/predicate_transform.rb +231 -0
  16. data/lib/pursuit/query_error.rb +7 -0
  17. data/lib/pursuit/simple_search.rb +66 -0
  18. data/lib/pursuit/term_parser.rb +44 -0
  19. data/lib/pursuit/term_search.rb +74 -0
  20. data/lib/pursuit/term_transform.rb +35 -0
  21. data/lib/pursuit.rb +18 -4
  22. data/pursuit.gemspec +4 -3
  23. data/spec/internal/app/models/application_record.rb +5 -0
  24. data/spec/internal/app/models/product.rb +3 -14
  25. data/spec/internal/app/models/product_category.rb +3 -1
  26. data/spec/internal/app/models/product_variation.rb +3 -1
  27. data/spec/internal/app/searches/product_category_search.rb +50 -0
  28. data/spec/internal/app/searches/product_search.rb +53 -0
  29. data/spec/internal/app/searches/product_variation_search.rb +53 -0
  30. data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
  31. data/spec/lib/pursuit/predicate_search_spec.rb +116 -0
  32. data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
  33. data/spec/lib/pursuit/simple_search_spec.rb +73 -0
  34. data/spec/lib/pursuit/term_parser_spec.rb +271 -0
  35. data/spec/lib/pursuit/term_search_spec.rb +85 -0
  36. data/spec/lib/pursuit/term_transform_spec.rb +105 -0
  37. metadata +50 -25
  38. data/.travis.yml +0 -26
  39. data/lib/pursuit/dsl.rb +0 -28
  40. data/lib/pursuit/railtie.rb +0 -13
  41. data/lib/pursuit/search.rb +0 -172
  42. data/lib/pursuit/search_options.rb +0 -86
  43. data/lib/pursuit/search_term_parser.rb +0 -46
  44. data/spec/lib/pursuit/dsl_spec.rb +0 -22
  45. data/spec/lib/pursuit/search_options_spec.rb +0 -146
  46. data/spec/lib/pursuit/search_spec.rb +0 -516
  47. data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
  48. data/travis/gemfiles/5.2.gemfile +0 -8
  49. data/travis/gemfiles/6.0.gemfile +0 -8
  50. data/travis/gemfiles/6.1.gemfile +0 -8
  51. data/travis/gemfiles/7.0.gemfile +0 -8
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Transform for a list of terms.
5
+ #
6
+ class TermTransform < Parslet::Transform
7
+ # String Types
8
+
9
+ rule(string_double_quotes: []) { '' }
10
+ rule(string_double_quotes: simple(:value)) { value.to_s.gsub(/\\(.)/, '\1') }
11
+
12
+ rule(string_single_quotes: []) { '' }
13
+ rule(string_single_quotes: simple(:value)) { value.to_s.gsub(/\\(.)/, '\1') }
14
+
15
+ rule(string_no_quotes: simple(:value)) { value.to_s }
16
+
17
+ # Terms
18
+
19
+ rule(term: simple(:term)) do |context|
20
+ value = ActiveRecord::Base.sanitize_sql_like(context[:term])
21
+ value = "%#{value}%"
22
+
23
+ context[:attributes].inject(nil) do |previous_node, attribute|
24
+ node = attribute.matches(value)
25
+ next node unless previous_node
26
+
27
+ previous_node.or(node)
28
+ end
29
+ end
30
+
31
+ # Joins
32
+
33
+ rule(left: simple(:left), right: simple(:right)) { left.and(right) }
34
+ end
35
+ end
data/lib/pursuit.rb CHANGED
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_record'
4
+ require 'active_support'
5
+ require 'bigdecimal'
6
+ require 'parslet'
7
+
3
8
  require_relative 'pursuit/constants'
4
- require_relative 'pursuit/search_options'
5
- require_relative 'pursuit/search_term_parser'
6
- require_relative 'pursuit/search'
7
- require_relative 'pursuit/railtie' if defined?(Rails::Railtie)
9
+ require_relative 'pursuit/error'
10
+ require_relative 'pursuit/query_error'
11
+ require_relative 'pursuit/aggregate_modifier_not_found'
12
+ require_relative 'pursuit/aggregate_modifier_required'
13
+ require_relative 'pursuit/aggregate_modifiers_not_available'
14
+ require_relative 'pursuit/attribute_not_found'
15
+ require_relative 'pursuit/predicate_parser'
16
+ require_relative 'pursuit/predicate_transform'
17
+ require_relative 'pursuit/predicate_search'
18
+ require_relative 'pursuit/term_parser'
19
+ require_relative 'pursuit/term_transform'
20
+ require_relative 'pursuit/term_search'
21
+ require_relative 'pursuit/simple_search'
data/pursuit.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.email = ['support@nialtoservices.co.uk']
13
13
 
14
14
  spec.summary = 'Advanced key-based searching for ActiveRecord objects.'
15
- spec.homepage = 'https://github.com/nialtoservices/pursuit'
15
+ spec.homepage = 'https://github.com/NialtoServices/pursuit'
16
16
  spec.license = 'Apache-2.0'
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0")
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.metadata['rubygems_mfa_required'] = 'true'
24
24
  spec.metadata['yard.run'] = 'yri'
25
25
 
26
- spec.add_runtime_dependency 'activerecord', '>= 5.2.0', '< 7.2.0'
27
- spec.add_runtime_dependency 'activesupport', '>= 5.2.0', '< 7.2.0'
26
+ spec.add_runtime_dependency 'activerecord', '>= 5.2.0', '<= 8.0.0'
27
+ spec.add_runtime_dependency 'activesupport', '>= 5.2.0', '<= 8.0.0'
28
+ spec.add_runtime_dependency 'parslet', '~> 2.0'
28
29
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
@@ -1,22 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Product < ActiveRecord::Base
3
+ class Product < ApplicationRecord
4
+ include ProductSearch
5
+
4
6
  belongs_to :category, class_name: 'ProductCategory', inverse_of: :products, optional: true
5
7
 
6
8
  has_many :variations, class_name: 'ProductVariation', inverse_of: :product
7
9
 
8
10
  validates :title, presence: true
9
-
10
- searchable do |o|
11
- o.relation :variations, :title, :stock_status
12
-
13
- o.attribute :title
14
- o.attribute :description
15
- o.attribute :rating, unkeyed: false
16
- o.attribute :title_length, unkeyed: false do
17
- Arel::Nodes::NamedFunction.new('LENGTH', [arel_table[:title]])
18
- end
19
-
20
- o.attribute :category, :category_id
21
- end
22
11
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ProductCategory < ActiveRecord::Base
3
+ class ProductCategory < ApplicationRecord
4
+ include ProductCategorySearch
5
+
4
6
  has_many :products, class_name: 'Product', foreign_key: :category_id, inverse_of: :category, dependent: :nullify
5
7
 
6
8
  validates :name, presence: true
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ProductVariation < ActiveRecord::Base
3
+ class ProductVariation < ApplicationRecord
4
+ include ProductVariationSearch
5
+
4
6
  belongs_to :product
5
7
 
6
8
  enum stock_status: { in_stock: 1, low_stock: 2, out_of_stock: 3 }
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductCategorySearch
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 :name
15
+ permit_attribute :product, Product.arel_table[:id]
16
+ permit_attribute :product_title, Product.arel_table[:title]
17
+ end
18
+
19
+ @predicate_search.apply(query, left_outer_joins(:products).group(:id))
20
+ end
21
+
22
+ # Search for records matching the specified terms.
23
+ #
24
+ # @param query [String] The query with one or more terms.
25
+ # @return [ActiveRecord::Relation] The current relation filtered by the terms.
26
+ #
27
+ def term_search(query)
28
+ @term_search ||= Pursuit::TermSearch.new(default_table: arel_table) do
29
+ search_attribute :name
30
+ end
31
+
32
+ # Note that we're using `all` here, but this still works when used in a chain:
33
+ # => ProductVariation.where(stock_status: :in_stock).search('Green')
34
+ @term_search.apply(query, all)
35
+ end
36
+
37
+ # Search for records matching the specified query.
38
+ #
39
+ # @param query [String] The query.
40
+ # @return [ActiveRecord::Relation] The current relation filtered by the query.
41
+ #
42
+ def search(query)
43
+ return none if query.blank?
44
+
45
+ predicate_search(query)
46
+ rescue Parslet::ParseFailed
47
+ term_search(query)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductSearch
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 :category, ProductCategory.arel_table[:id]
16
+ permit_attribute :category_name, ProductCategory.arel_table[:name]
17
+ permit_attribute :variation, ProductVariation.arel_table[:id]
18
+ permit_attribute :variation_title, ProductVariation.arel_table[:title]
19
+ permit_attribute :variation_currency, ProductVariation.arel_table[:currency]
20
+ permit_attribute :variation_amount, ProductVariation.arel_table[:amount]
21
+ end
22
+
23
+ @predicate_search.apply(query, left_outer_joins(:category, :variations).group(:id).order(:title))
24
+ end
25
+
26
+ # Search for records matching the specified terms.
27
+ #
28
+ # @param query [String] The query with one or more terms.
29
+ # @return [ActiveRecord::Relation] The current relation filtered by the terms.
30
+ #
31
+ def term_search(query)
32
+ @term_search ||= Pursuit::TermSearch.new(default_table: arel_table) do
33
+ search_attribute :title
34
+ search_attribute ProductCategory.arel_table[:name]
35
+ end
36
+
37
+ @term_search.apply(query, left_outer_joins(:category).group(:id).order(:title))
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
@@ -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