pursuit 0.4.5 → 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 (48) 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 +83 -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 +64 -0
  18. data/lib/pursuit/term_parser.rb +44 -0
  19. data/lib/pursuit/term_search.rb +69 -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 +25 -9
  25. data/spec/internal/app/models/product_category.rb +23 -1
  26. data/spec/internal/app/models/product_variation.rb +26 -1
  27. data/spec/lib/pursuit/predicate_parser_spec.rb +1604 -0
  28. data/spec/lib/pursuit/predicate_search_spec.rb +80 -0
  29. data/spec/lib/pursuit/predicate_transform_spec.rb +624 -0
  30. data/spec/lib/pursuit/simple_search_spec.rb +59 -0
  31. data/spec/lib/pursuit/term_parser_spec.rb +271 -0
  32. data/spec/lib/pursuit/term_search_spec.rb +71 -0
  33. data/spec/lib/pursuit/term_transform_spec.rb +105 -0
  34. metadata +47 -25
  35. data/.travis.yml +0 -26
  36. data/lib/pursuit/dsl.rb +0 -28
  37. data/lib/pursuit/railtie.rb +0 -13
  38. data/lib/pursuit/search.rb +0 -172
  39. data/lib/pursuit/search_options.rb +0 -86
  40. data/lib/pursuit/search_term_parser.rb +0 -46
  41. data/spec/lib/pursuit/dsl_spec.rb +0 -22
  42. data/spec/lib/pursuit/search_options_spec.rb +0 -146
  43. data/spec/lib/pursuit/search_spec.rb +0 -516
  44. data/spec/lib/pursuit/search_term_parser_spec.rb +0 -34
  45. data/travis/gemfiles/5.2.gemfile +0 -8
  46. data/travis/gemfiles/6.0.gemfile +0 -8
  47. data/travis/gemfiles/6.1.gemfile +0 -8
  48. data/travis/gemfiles/7.0.gemfile +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3de94df3d5b52d4c91443ae748a3abb79e64a46aee742af05d81f38dc3c050b6
4
- data.tar.gz: c41d2d0a0498b08fa938046973bd459856253e78159e7f71256f0d33ce6b0c61
3
+ metadata.gz: 48b4becb7e8e0e0d39335cd5172fd4870989a9e26033a882e082480431330285
4
+ data.tar.gz: 33265eafe36113d7e4f263a6f9a2dc9fd7d12cfda95073c29ea0866f627e4566
5
5
  SHA512:
6
- metadata.gz: 488e5ac1cf2b9d4368608d179134f16945450de39560c07671909c3dbdf2c736afe528ec0b319d9b673ff4f990272b3d9678c22a1748f64f8f526b35a7ff554c
7
- data.tar.gz: 78178098bb90e01510d2bb741a7bc5994ea307d5e79fb47e45cf1e476a91a2cd1dc6df2b70423530a4a7aca93e29f1c3e16b6c80a03c3538158043b5330ff32d
6
+ metadata.gz: 01fba1c0d143f914d3a040620f618538f8e77396e7e9fa17f503a06ef6b419c2b9eb0a55950e60b16608d944535eb9a3bf70888615709d08f525938d25850bf6
7
+ data.tar.gz: b2d0106bc8ceea194d87f62dd8831c3cb4477c831c951f638038d6d94384ee91c64039891203783ee50a3bedf76672245641ba5206191b5350f72d46e1e462f3
@@ -0,0 +1,46 @@
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
data/Gemfile CHANGED
@@ -4,16 +4,17 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- gem 'bundler', '~> 2.4'
8
- gem 'combustion', '~> 1.3'
9
- gem 'guard', '~> 2.18'
10
- gem 'guard-rspec', '~> 4.7'
11
- gem 'pry', '~> 0.14'
12
- gem 'rake', '~> 13.0'
13
- gem 'rspec', '~> 3.12'
14
- gem 'rspec-rails', '~> 6.0'
15
- gem 'rubocop', '~> 1.57'
16
- gem 'rubocop-rake', '~> 0.6'
17
- gem 'rubocop-rspec', '~> 2.25'
18
- gem 'sqlite3', '~> 1.6'
19
- gem 'yard', '~> 0.9'
7
+ group :development do
8
+ gem 'combustion', '~> 1.3'
9
+ gem 'guard', '~> 2.18'
10
+ gem 'guard-rspec', '~> 4.7'
11
+ gem 'pry', '~> 0.14'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rspec', '~> 3.12'
14
+ gem 'rspec-rails', '~> 6.0'
15
+ gem 'rubocop', '~> 1.57'
16
+ gem 'rubocop-rake', '~> 0.6'
17
+ gem 'rubocop-rspec', '~> 2.25'
18
+ gem 'sqlite3', '~> 1.6'
19
+ gem 'yard', '~> 0.9'
20
+ end
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pursuit (0.4.5)
5
- activerecord (>= 5.2.0, < 7.2.0)
6
- activesupport (>= 5.2.0, < 7.2.0)
4
+ pursuit (1.0.1)
5
+ activerecord (>= 5.2.0, <= 8.0.0)
6
+ activesupport (>= 5.2.0, <= 8.0.0)
7
+ parslet (~> 2.0)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
@@ -90,7 +91,7 @@ GEM
90
91
  minitest (5.20.0)
91
92
  mutex_m (0.1.2)
92
93
  nenv (0.3.0)
93
- nokogiri (1.15.4-aarch64-linux)
94
+ nokogiri (1.15.4-arm-linux)
94
95
  racc (~> 1.4)
95
96
  nokogiri (1.15.4-arm64-darwin)
96
97
  racc (~> 1.4)
@@ -105,12 +106,13 @@ GEM
105
106
  parser (3.2.2.4)
106
107
  ast (~> 2.4.1)
107
108
  racc
109
+ parslet (2.0.0)
108
110
  pry (0.14.2)
109
111
  coderay (~> 1.1)
110
112
  method_source (~> 1.0)
111
113
  psych (5.1.1.1)
112
114
  stringio
113
- racc (1.7.1)
115
+ racc (1.7.2)
114
116
  rack (3.0.8)
115
117
  rack-session (2.0.0)
116
118
  rack (>= 3.0.0)
@@ -135,7 +137,7 @@ GEM
135
137
  thor (~> 1.0, >= 1.2.2)
136
138
  zeitwerk (~> 2.6)
137
139
  rainbow (3.1.1)
138
- rake (13.0.6)
140
+ rake (13.1.0)
139
141
  rb-fsevent (0.11.2)
140
142
  rb-inotify (0.10.1)
141
143
  ffi (~> 1.0)
@@ -192,10 +194,10 @@ GEM
192
194
  ruby-progressbar (1.13.0)
193
195
  ruby2_keywords (0.0.5)
194
196
  shellany (0.0.1)
195
- sqlite3 (1.6.7-aarch64-linux)
196
- sqlite3 (1.6.7-arm64-darwin)
197
- sqlite3 (1.6.7-x86_64-darwin)
198
- sqlite3 (1.6.7-x86_64-linux)
197
+ sqlite3 (1.6.8-arm-linux)
198
+ sqlite3 (1.6.8-arm64-darwin)
199
+ sqlite3 (1.6.8-x86_64-darwin)
200
+ sqlite3 (1.6.8-x86_64-linux)
199
201
  stringio (3.0.8)
200
202
  thor (1.3.0)
201
203
  timeout (0.4.0)
@@ -207,13 +209,12 @@ GEM
207
209
  zeitwerk (2.6.12)
208
210
 
209
211
  PLATFORMS
210
- aarch64-linux
211
212
  arm64-darwin-22
212
- x86_64-darwin-21
213
+ arm64-linux
214
+ x86_64-darwin-22
213
215
  x86_64-linux
214
216
 
215
217
  DEPENDENCIES
216
- bundler (~> 2.4)
217
218
  combustion (~> 1.3)
218
219
  guard (~> 2.18)
219
220
  guard-rspec (~> 4.7)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pursuit
2
2
 
3
- Advanced key-based searching for ActiveRecord objects.
3
+ Search your ActiveRecord objects with ease!
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,41 +16,224 @@ gem 'pursuit'
16
16
 
17
17
  ### Usage
18
18
 
19
- You can use the convenient DSL syntax to declare which attributes and relationships are searchable:
19
+ Pursuit comes with three different strategies for interpreting queries:
20
+
21
+ - Simple
22
+ - Term
23
+ - Predicate
24
+
25
+ ### Simple Search
26
+
27
+ Simple takes the entire query and generates a SQL `LIKE` (or `ILIKE` for *PostgreSQL*) statement for each attribute
28
+ added to the search instance. Here's an example of how you might use simple to search a hypothetical `Product` record:
29
+
30
+ ```ruby
31
+ search = Pursuit::SimpleSearch.new(Product.all)
32
+ search.search_attribute(:title)
33
+ search.search_attribute(:subtitle)
34
+ search.apply('Green Shirt')
35
+ ```
36
+
37
+ Which results in the following SQL query:
38
+
39
+ ```sql
40
+ SELECT
41
+ "products".*
42
+ FROM
43
+ "products"
44
+ WHERE
45
+ "products"."title" LIKE '%Green Shirt%'
46
+ OR "products"."subtitle" LIKE '%Green Shirt%'
47
+ ```
48
+
49
+ The initializer method also accepts a block, which is evaluated within the instance's context. This can make it cleaner
50
+ when declaring the searchable attributes:
51
+
52
+ ```ruby
53
+ search = Pursuit::SimpleSearch.new(Product.all) do
54
+ search_attribute :title
55
+ search_attribute :subtitle
56
+ end
57
+
58
+ search.apply('Green Shirt')
59
+ ```
60
+
61
+ You can also pass custom `Arel::Attribute::Attribute` objects, which are especially useful when using joins:
62
+
63
+ ```ruby
64
+ search = Pursuit::SimpleSearch.new(
65
+ Product.left_outer_joins(:variations).group(:id)
66
+ ) do
67
+ search_attribute :title
68
+ search_attribute ProductVariation.arel_table[:title]
69
+ end
70
+
71
+ search.apply('Green Shirt')
72
+ ```
73
+
74
+ Which results in the following SQL query:
75
+
76
+ ```sql
77
+ SELECT
78
+ "products".*
79
+ FROM
80
+ "products"
81
+ LEFT OUTER JOIN "product_variations" ON "product_variations"."product_id" = "products"."id"
82
+ WHERE
83
+ "products"."title" LIKE '%Green Shirt%'
84
+ OR "product_variations"."title" LIKE '%Green Shirt%'
85
+ GROUP BY
86
+ "products"."id"
87
+ ```
88
+
89
+ ### Term Search
90
+
91
+ Term searches break a query into individual terms on spaces, while providing double and single quoted strings as a
92
+ means to include spaces. Here's an example of using term searches on the same `Product` record from earlier:
20
93
 
21
94
  ```ruby
22
- class Product < ActiveRecord::Base
23
- searchable do |o|
24
- o.relation :variations, :title, :stock_status
25
-
26
- # Attributes can be used for both keyed and unkeyed searching by default, but you can pass either `keyed: false` or
27
- # `unkeyed: false` to restrict when the attribute is searched.
28
- o.attribute :title
29
- o.attribute :description
30
- o.attribute :rating, unkeyed: false
31
-
32
- # You can shorten the search keyword by passing the desired search term first, and then the real attribute name
33
- # as the second argument.
34
- # => "category*=shirts"
35
- o.attribute :category, :category_id
36
-
37
- # It's also possible to query entirely custom Arel nodes by passing a block which returns the Arel node to query.
38
- # You could use this to query a person's full name by concatenating their first and last name columns, for example.
39
- o.attribute :title_length, unkeyed: false do
40
- Arel::Nodes::NamedFunction.new('LENGTH', [
41
- arel_table[:title]
42
- ])
43
- end
44
- end
95
+ search = Pursuit::TermSearch.new(Product.all) do
96
+ search_attribute :title
97
+ search_attribute :subtitle
45
98
  end
99
+
100
+ search.apply('Green "Luxury Shirt"')
101
+ ```
102
+
103
+ Which results in a SQL query similar to the following:
104
+
105
+ ```sql
106
+ SELECT
107
+ "products".*
108
+ FROM
109
+ "products"
110
+ WHERE
111
+ (
112
+ "products"."title" LIKE '%Green%'
113
+ OR "products"."subtitle" LIKE '%Green%'
114
+ ) AND (
115
+ "products"."title" LIKE '%Luxury Shirt%'
116
+ OR "products"."subtitle" LIKE '%Luxury Shirt%'
117
+ )
46
118
  ```
47
119
 
48
- This creates a ```.search``` method on your record class which accepts a single query argument:
120
+ ### Predicate Search
121
+
122
+ Predicate searches use a parser (implemented with the `parslet` gem) to provide a minimal query language.
123
+ This syntax is similar to the `WHERE` and `HAVING` clauses in SQL, but uses only symbols for operators and joins.
124
+
125
+ Attributes can only be used in predicate searches when they have been added to the list of permitted attributes.
126
+ You can also rename attributes, and add attributes for joined records.
127
+
128
+ Here's a more complex example of using predicate-based searches with joins on the `Product` record from earlier:
49
129
 
50
130
  ```ruby
51
- Product.search('plain shirt rating>=3')
131
+ search = Pursuit::PredicateSearch.new(
132
+ Product.left_outer_join(:category, :variations).group(:id)
133
+ ) do
134
+ # Product Attributes
135
+ permit_attribute :title
136
+
137
+ # Product Category Attributes
138
+ permit_attribute :category_name, ProductCategory.arel_table[:name]
139
+
140
+ # Product Variation Attributes
141
+ permit_attribute :variation_title, ProductVariation.arel_table[:title]
142
+ permit_attribute :variation_currency, ProductVariation.arel_table[:currency]
143
+ permit_attribute :variation_amount, ProductVariation.arel_table[:amount]
144
+ end
145
+
146
+ search.apply('title = "Luxury Shirt" & (variation_amount = 0 | variation_amount > 1000)')
147
+ ```
148
+
149
+ This translates to "a product whose title is 'Luxury Shirt' and has at least one variation with either an amount of 0,
150
+ or an amount greater than 1000", which could be expressed in SQL as:
151
+
152
+ ```sql
153
+ SELECT
154
+ "products".*
155
+ FROM
156
+ "products"
157
+ LEFT OUTER JOIN "product_categories" ON "product_categories"."id" = "products"."category_id"
158
+ LEFT OUTER JOIN "product_variations" ON "product_variations"."product_id" = "products"."id"
159
+ WHERE
160
+ "products"."title" = 'Luxury Shirt'
161
+ AND (
162
+ "product_variations"."amount" = 0
163
+ OR "product_variations"."amount" > 1000
164
+ )
165
+ GROUP BY
166
+ "products"."id"
52
167
  ```
53
168
 
169
+ You can use any of the following operators in comparisons:
170
+
171
+ - `=` checks if the attribute is equal to the value.
172
+ - `!=` checks if the attributes is not equal to the value.
173
+ - `>` checks if the attribute is greater than the value.
174
+ - `<` checks if the attribute is less than the value.
175
+ - `>=` checks if the attribute is greater than or equal to the value.
176
+ - `<=` checks if the attribute is less than or equal to the value.
177
+ - `~` checks if the attribute matches the value (using `LIKE` or `ILIKE`).
178
+ - `!~` checks if the attribute does not match the value (using `LIKE` or `ILIKE`).
179
+
180
+ Predicate searches also support "aggregate modifiers" which enable the use of aggregate functions, however this feature
181
+ must be explicitly enabled and requires you to use a `GROUP BY` clause:
182
+
183
+ ```ruby
184
+ search = Pursuit::PredicateSearch.new(
185
+ Product.left_outer_join(:category, :variations).group(:id)
186
+ ) do
187
+ # Product Attributes
188
+ permit_attribute :title
189
+
190
+ # Product Category Attributes
191
+ permit_attribute :category, ProductCategory.arel_table[:id]
192
+ permit_attribute :category_name, ProductCategory.arel_table[:name]
193
+
194
+ # Product Variation Attributes
195
+ permit_attribute :variation, ProductVariation.arel_table[:id]
196
+ permit_attribute :variation_title, ProductVariation.arel_table[:title]
197
+ permit_attribute :variation_currency, ProductVariation.arel_table[:currency]
198
+ permit_attribute :variation_amount, ProductVariation.arel_table[:amount]
199
+ end
200
+
201
+ search.apply('title = "Luxury Shirt" & #variation > 5')
202
+ ```
203
+
204
+ And the resulting SQL from this query:
205
+
206
+ ```sql
207
+ SELECT
208
+ "products".*
209
+ FROM
210
+ "products"
211
+ LEFT OUTER JOIN "product_categories" ON "product_categories"."id" = "products"."category_id"
212
+ LEFT OUTER JOIN "product_variations" ON "product_variations"."product_id" = "products"."id"
213
+ WHERE
214
+ "products"."title" = 'Luxury Shirt'
215
+ GROUP BY
216
+ "products"."id"
217
+ HAVING
218
+ COUNT("product_variations"."id") > 5
219
+ ```
220
+
221
+ There's no distinction between the `WHERE` and `HAVING` clause in the predicate syntax, as it's intended to be easy to
222
+ use, but this does come with a caveat.
223
+
224
+ The query must have all aggregate-modified comparisons before or after non-aggregate-modified comparisons, you can't
225
+ mix both.
226
+
227
+ For example, this query would result in a parsing error: `title ~ Shirt & #variation > 5 & category_name = Shirts`
228
+
229
+ You can preceed any attribute with one of these aggregate modifier symbols:
230
+
231
+ - `#` uses the `COUNT` aggregate function
232
+ - `+` uses the `MAX` aggregate function
233
+ - `-` uses the `MIN` aggregate function
234
+ - `*` uses the `SUM` aggregate function
235
+ - `~` uses the `AVG` aggregate function
236
+
54
237
  ## Development
55
238
 
56
239
  After checking out the repo, run `bundle exec rake spec` to run the tests.
data/bin/console CHANGED
@@ -1,7 +1,17 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # frozen_string_literal: true
4
+
3
5
  require 'bundler/setup'
4
6
  require 'pursuit'
5
7
  require 'pry'
6
8
 
9
+ if ENV['AR'] == 'true'
10
+ require 'combustion'
11
+ Combustion.initialize!(:active_record)
12
+
13
+ require 'bundler'
14
+ Bundler.require(:default, :development)
15
+ end
16
+
7
17
  Pry.start
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Raised when an aggregate modifier cannot be found.
5
+ #
6
+ class AggregateModifierNotFound < QueryError
7
+ # @return [String] The aggregate modifier which does not map to an aggregate function.
8
+ #
9
+ attr_reader :aggregate_modifier
10
+
11
+ # Creates a new error instance.
12
+ #
13
+ # @param aggregate_modifier [Symbol] The aggregate modifier which does not map to an aggregate function.
14
+ #
15
+ def initialize(aggregate_modifier)
16
+ @aggregate_modifier = aggregate_modifier
17
+ super("#{aggregate_modifier} is not a valid aggregate modifier")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Raised when an attribute that must be used with an aggregate modifier is used without one.
5
+ #
6
+ class AggregateModifierRequired < QueryError
7
+ # @return [Symbol] The name of the attribute which must be used with an aggregate modifier.
8
+ #
9
+ attr_reader :attribute
10
+
11
+ # Creates a new error instance.
12
+ #
13
+ # @param attribute [Symbol] The name of the attribute which must be used with an aggregate modifier.
14
+ #
15
+ def initialize(attribute)
16
+ @attribute = attribute
17
+ super("'#{attribute}' must be used with an aggregate modifier")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Raised when an aggregate modifier is used in a query, but aggregate modifiers are not available.
5
+ #
6
+ class AggregateModifiersNotAvailable < QueryError
7
+ # Creates a new error instance.
8
+ #
9
+ def initialize
10
+ super('Aggregate modifiers cannot be used in this query')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Raised when an attribute cannot be found.
5
+ #
6
+ class AttributeNotFound < QueryError
7
+ # @return [Symbol] The name of the attribute which could not be found.
8
+ #
9
+ attr_reader :attribute
10
+
11
+ # Creates a new error instance.
12
+ #
13
+ # @param attribute [Symbol] The name of the attribute which could not be found.
14
+ #
15
+ def initialize(attribute)
16
+ @attribute = attribute
17
+ super("'#{attribute}' is not a valid attribute")
18
+ end
19
+ end
20
+ end
@@ -3,5 +3,5 @@
3
3
  module Pursuit
4
4
  # @return [String] The gem's semantic version number.
5
5
  #
6
- VERSION = '0.4.5'
6
+ VERSION = '1.0.1'
7
7
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pursuit
4
+ # Base error class for all pursuit errors.
5
+ #
6
+ class Error < StandardError; end
7
+ end