pursuit 0.4.5 → 1.1.0

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