quo 0.6.0 → 1.0.0.beta1

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -1
  3. data/Appraisals +15 -0
  4. data/CHANGELOG.md +78 -0
  5. data/Gemfile +6 -4
  6. data/LICENSE.txt +1 -1
  7. data/README.md +222 -232
  8. data/Steepfile +0 -2
  9. data/gemfiles/rails_7.0.gemfile +15 -0
  10. data/gemfiles/rails_7.1.gemfile +15 -0
  11. data/gemfiles/rails_7.2.gemfile +15 -0
  12. data/gemfiles/rails_8.0.gemfile +15 -0
  13. data/lib/quo/collection_backed_query.rb +87 -0
  14. data/lib/quo/collection_results.rb +44 -0
  15. data/lib/quo/composed_query.rb +278 -0
  16. data/lib/quo/engine.rb +11 -0
  17. data/lib/quo/minitest/helpers.rb +41 -0
  18. data/lib/quo/preloadable.rb +46 -0
  19. data/lib/quo/query.rb +97 -215
  20. data/lib/quo/relation_backed_query.rb +150 -0
  21. data/lib/quo/relation_backed_query_specification.rb +154 -0
  22. data/lib/quo/relation_results.rb +58 -0
  23. data/lib/quo/results.rb +48 -44
  24. data/lib/quo/rspec/helpers.rb +31 -9
  25. data/lib/quo/testing/collection_backed_fake.rb +29 -0
  26. data/lib/quo/testing/relation_backed_fake.rb +52 -0
  27. data/lib/quo/version.rb +3 -1
  28. data/lib/quo.rb +23 -30
  29. data/rbs_collection.yaml +0 -2
  30. data/sig/generated/quo/collection_backed_query.rbs +39 -0
  31. data/sig/generated/quo/collection_results.rbs +30 -0
  32. data/sig/generated/quo/composed_query.rbs +112 -0
  33. data/sig/generated/quo/engine.rbs +6 -0
  34. data/sig/generated/quo/preloadable.rbs +29 -0
  35. data/sig/generated/quo/query.rbs +98 -0
  36. data/sig/generated/quo/relation_backed_query.rbs +67 -0
  37. data/sig/generated/quo/relation_backed_query_specification.rbs +94 -0
  38. data/sig/generated/quo/relation_results.rbs +38 -0
  39. data/sig/generated/quo/results.rbs +39 -0
  40. data/sig/generated/quo/testing/collection_backed_fake.rbs +13 -0
  41. data/sig/generated/quo/testing/relation_backed_fake.rbs +23 -0
  42. data/sig/generated/quo/version.rbs +5 -0
  43. data/sig/generated/quo.rbs +9 -0
  44. data/sig/literal.rbs +7 -0
  45. metadata +77 -37
  46. data/lib/quo/eager_query.rb +0 -51
  47. data/lib/quo/loaded_query.rb +0 -18
  48. data/lib/quo/merged_query.rb +0 -36
  49. data/lib/quo/query_composer.rb +0 -78
  50. data/lib/quo/railtie.rb +0 -7
  51. data/lib/quo/utilities/callstack.rb +0 -21
  52. data/lib/quo/utilities/compose.rb +0 -18
  53. data/lib/quo/utilities/sanitize.rb +0 -19
  54. data/lib/quo/utilities/wrap.rb +0 -23
  55. data/lib/quo/wrapped_query.rb +0 -18
  56. data/sig/quo/eager_query.rbs +0 -15
  57. data/sig/quo/loaded_query.rbs +0 -7
  58. data/sig/quo/merged_query.rbs +0 -19
  59. data/sig/quo/query.rbs +0 -83
  60. data/sig/quo/query_composer.rbs +0 -32
  61. data/sig/quo/results.rbs +0 -22
  62. data/sig/quo/utilities/callstack.rbs +0 -7
  63. data/sig/quo/utilities/compose.rbs +0 -8
  64. data/sig/quo/utilities/sanitize.rbs +0 -9
  65. data/sig/quo/utilities/wrap.rbs +0 -11
  66. data/sig/quo/wrapped_query.rbs +0 -11
  67. data/sig/quo.rbs +0 -41
data/README.md CHANGED
@@ -1,330 +1,324 @@
1
- # Quo
1
+ # Quo: Query Objects for ActiveRecord
2
2
 
3
- Quo query objects can help you abstract ActiveRecord DB queries into reusable and composable objects with a chainable
4
- interface.
3
+ Quo helps you organize database queries into reusable, composable, and testable objects.
5
4
 
6
- The query object can also abstract over any array-like collection meaning that it is possible for example to cache the
7
- data from a query and reuse it.
5
+ ## Core Features
8
6
 
9
- The core implementation provides the following functionality:
7
+ * Wrap around an underlying ActiveRecord relation or array-like collection
8
+ * Supports pagination for ActiveRecord-based queries and collections that respond to `[]`
9
+ * Support composition with the `+` (`compose`) method to merge multiple query objects
10
+ * Allow transforming results with the `transform` method
11
+ * Offer utility methods that operate on the underlying collection (eg `exists?`)
12
+ * Act as a callable with chainable methods like ActiveRecord
13
+ * Provide a clear separation between query definition and execution with enumerable `Results` objects
14
+ * Type-safe properties with optional default values
10
15
 
11
- * wrap around an underlying ActiveRecord or array-like collection
12
- * optionally provides paging behaviour to ActiveRecord based queries
13
- * provides a number of utility methods that operate on the underlying collection (eg `exists?`)
14
- * provides a `+` (`compose`) method which merges two query object instances (see section below for details!)
15
- * can specify a mapping or transform method to `transform` to perform on results
16
- * in development outputs the callstack that led to the execution of the query
17
- * acts as a callable which executes the underlying query with `.first`
18
- * can return an `Enumerable` of results
16
+ ## Core Concepts
19
17
 
20
- ## Creating a Quo query object
18
+ Query objects encapsulate query logic in dedicated classes, making complex queries more manageable and reusable.
21
19
 
22
- The query object must inherit from `Quo::Query` and provide an implementation for the `query` method.
20
+ Quo provides two main components:
21
+ 1. **Query Objects** - Define and configure queries
22
+ 2. **Results Objects** - Execute queries and provide access to the results
23
23
 
24
- The `query` method must return either:
24
+ ## Creating Query Objects
25
25
 
26
- - an `ActiveRecord::Relation`
27
- - an Array (an 'eager loaded' query)
28
- - or another `Quo::Query` instance.
26
+ ### Relation-Backed Queries
29
27
 
30
- Remember that the query object should be useful in composition with other query objects. Thus it should not directly
31
- specify things that are not directly needed to fetch the right data for the given context.
28
+ For queries based on ActiveRecord relations:
32
29
 
33
- For example the ordering of the results is mostly something that is specified when the query object is used, not as
34
- part of the query itself (as then it would always enforce the ordering on other queries it was composed with).
30
+ ```ruby
31
+ class RecentActiveUsers < Quo::RelationBackedQuery
32
+ # Define typed properties
33
+ prop :days_ago, Integer, default: -> { 30 }
34
+
35
+ def query
36
+ User
37
+ .where(active: true)
38
+ .where("created_at > ?", days_ago.days.ago)
39
+ end
40
+ end
35
41
 
36
- ## Passing options to queries
42
+ # Create and use the query
43
+ query = RecentActiveUsers.new(days_ago: 7)
44
+ results = query.results
37
45
 
38
- If any parameters are need in `query`, these are provided when instantiating the query object using the `options` hash.
46
+ # Work with results
47
+ results.each { |user| puts user.email }
48
+ puts "Found #{results.count} users"
49
+ ```
39
50
 
40
- It is also possible to pass special configuration options to the constructor options hash.
51
+ ### Collection-Backed Queries
41
52
 
42
- Specifically when the underlying collection is a ActiveRecord relation then:
53
+ For queries based on any Enumerable collection:
43
54
 
44
- * `order`: the `order` condition for the relation (eg `:desc`)
45
- * `includes`: the `includes` condition for the relation (eg `account: {user: :profile}`)
46
- * `group`: the `group` condition for the relation
47
- * `page`: the current page number to fetch
48
- * `page_size`: the number of elements to fetch in the page
55
+ ```ruby
56
+ class CachedUsers < Quo::CollectionBackedQuery
57
+ prop :role, String
58
+
59
+ def collection
60
+ @cached_users ||= Rails.cache.fetch("all_users", expires_in: 1.hour) do
61
+ User.all.to_a
62
+ end.select { |user| user.role == role }
63
+ end
64
+ end
49
65
 
50
- Note that the above options have no bearing on the query if it is backed by an array-like collection and that some
51
- options can be configured using the following methods.
66
+ # Use the query
67
+ admins = CachedUsers.new(role: "admin").results
68
+ ```
52
69
 
53
- ## Configuring queries
70
+ ## Quick Queries with Wrap
54
71
 
55
- Note that it is possible to configure a query using chainable methods similar to ActiveRecord:
72
+ Create query objects without subclassing:
56
73
 
57
- * limit
58
- * order
59
- * group
60
- * includes
61
- * left_outer_joins
62
- * preload
63
- * joins
74
+ ```ruby
75
+ # Relation-backed
76
+ users_query = Quo::RelationBackedQuery.wrap(User.active).new
77
+ active_users = users_query.results
64
78
 
65
- Note that these return a new Quo Query and do not mutate the original instance.
79
+ # Collection-backed
80
+ items_query = Quo::CollectionBackedQuery.wrap([1, 2, 3]).new
81
+ items = items_query.results
82
+ ```
66
83
 
67
- ## Composition of queries (merging or combining them)
84
+ ## Type-Safe Properties
68
85
 
69
- Quo query objects are composeability. In `ActiveRecord::Relation` this is acheived using `merge`
70
- and so under the hood `Quo::Query` uses that when composing relations. However since Queries can also abstract over
71
- array-like collections (ie enumerable and define a `+` method) compose also handles concating them together.
86
+ Quo uses the `Literal` gem for typed properties:
72
87
 
73
- Composing can be done with either
88
+ ```ruby
89
+ class UsersByState < Quo::RelationBackedQuery
90
+ prop :state, String
91
+ prop :minimum_age, Integer, default: -> { 18 }
92
+ prop :active_only, Boolean, default: -> { true }
74
93
 
75
- - `Quo::Query.compose(left, right)`
76
- - or `left.compose(right)`
77
- - or more simply with `left + right`
94
+ def query
95
+ scope = User.where(state: state)
96
+ scope = scope.where("age >= ?", minimum_age) if minimum_age.present?
97
+ scope = scope.where(active: true) if active_only
98
+ scope
99
+ end
100
+ end
78
101
 
79
- The composition methods also accept an optional parameter to pass to ActiveRecord relation merges for the `joins`.
80
- This allows you to compose together Query objects which return relations which are of different models but still merge
81
- them correctly with the appropriate joins. Note with the alias you cant neatly specify optional parameters for joins
82
- on relations.
102
+ query = UsersByState.new(state: "California", minimum_age: 21)
103
+ ```
83
104
 
84
- Note that the compose process creates a new query object instance, which is a instance of a `Quo::MergedQuery`.
105
+ ## Fluent API for Building Queries
85
106
 
86
- Consider the following cases:
107
+ ```ruby
108
+ query = UsersByState.new(state: "California")
109
+ .order(created_at: :desc)
110
+ .includes(:profile)
111
+ .limit(10)
112
+ .where(verified: true)
87
113
 
88
- 1. compose two query objects which return `ActiveRecord::Relation`s
89
- 2. compose two query objects, one of which returns a `ActiveRecord::Relation`, and the other an array-like
90
- 3. compose two query objects which return array-likes
114
+ users = query.results
115
+ ```
91
116
 
92
- In case (1) the compose process uses `ActiveRecords::Relation`'s `merge` method to create another query object
93
- wrapped around a new 'composed' `ActiveRecords::Relation`.
117
+ Available methods include:
118
+ * `where`
119
+ * `order`
120
+ * `limit`
121
+ * `includes`
122
+ * `preload`
123
+ * `left_outer_joins`
124
+ * `joins`
125
+ * `group`
94
126
 
95
- In case (2) the query object with a `ActiveRecords::Relation` inside is executed, and the result is then concatenated
96
- to the array-like with `+`
127
+ Each method returns a new query instance without modifying the original.
97
128
 
98
- In case (3) the values contained with each 'eager' query object are concatenated with `+`
129
+ ## Pagination
99
130
 
100
- *Note that*
131
+ ```ruby
132
+ query = UsersByState.new(
133
+ state: "California",
134
+ page: 2,
135
+ page_size: 20
136
+ )
137
+
138
+ # Get paginated results
139
+ users = query.results
140
+
141
+ # Navigation
142
+ next_page = query.next_page_query
143
+ prev_page = query.previous_page_query
144
+ ```
101
145
 
102
- with `left.compose(right)`, `left` must obviously be an instance of a `Quo::Query`, and `right` can be either a
103
- query object or and `ActiveRecord::Relation`. However `Quo::Query.compose(left, right)` also accepts
104
- `ActiveRecord::Relation`s for left.
146
+ ## Composing Queries
105
147
 
106
- ### Examples
148
+ Combine multiple queries:
107
149
 
108
150
  ```ruby
109
- class CompanyToBeApproved < Quo::Query
151
+ class ActiveUsers < Quo::RelationBackedQuery
110
152
  def query
111
- Registration
112
- .left_joins(:approval)
113
- .where(approvals: {completed_at: nil})
153
+ User.where(active: true)
114
154
  end
115
155
  end
116
156
 
117
- class CompanyInUsState < Quo::Query
157
+ class PremiumUsers < Quo::RelationBackedQuery
118
158
  def query
119
- Registration
120
- .joins(company: :address)
121
- .where(addresses: {state: options[:state]})
159
+ User.where(subscription_tier: "premium")
122
160
  end
123
161
  end
124
162
 
125
- query1 = CompanyToBeApproved.new
126
- query2 = CompanyInUsState.new(state: "California")
127
-
128
- # Compose
129
- composed = query1 + query2 # or Quo::Query.compose(query1, query2) or query1.compose(query2)
130
- composed.first
163
+ # Compose queries
164
+ active_premium = ActiveUsers.new + PremiumUsers.new
165
+ users = active_premium.results
131
166
  ```
132
167
 
133
- This effectively executes:
134
-
135
- ```ruby
136
- Registration
137
- .left_joins(:approval)
138
- .joins(company: :address)
139
- .where(approvals: {completed_at: nil})
140
- .where(addresses: {state: options[:state]})
141
- ```
142
-
143
- It is also possible to compose with an `ActiveRecord::Relation`. This can be useful in a Query object itself to help
144
- build up the `query` relation. For example:
145
-
146
- ```ruby
147
- class RegistrationToBeApproved < Quo::Query
148
- def query
149
- done = Registration.where(step: "complete")
150
- approved = CompanyToBeApproved.new
151
- # Here we use `.compose` utility method to wrap our Relation in a Query and
152
- # then compose with the other Query
153
- Quo::Query.compose(done, approved)
154
- end
155
- end
156
-
157
- # A Relation can be composed directly to a Quo::Query
158
- query = RegistrationToBeApproved.new + Registration.where(blocked: false)
159
- ```
168
+ You can compose queries using:
169
+ * `Quo::Query.compose(left, right)`
170
+ * `left.compose(right)`
171
+ * `left + right`
160
172
 
161
- Also you can use joins:
173
+ ### Composing with Joins
162
174
 
163
175
  ```ruby
164
- class TagByName < Quo::Query
176
+ class ProductsQuery < Quo::RelationBackedQuery
165
177
  def query
166
- Tag.where(name: options[:name])
178
+ Product.where(active: true)
167
179
  end
168
180
  end
169
181
 
170
- class CategoryByName < Quo::Query
182
+ class CategoriesQuery < Quo::RelationBackedQuery
171
183
  def query
172
- Category.where(name: options[:name])
184
+ Category.where(featured: true)
173
185
  end
174
186
  end
175
187
 
176
- tags = TagByName.new(name: "Intel")
177
- for_category = CategoryByName.new(name: "CPUs")
178
- tags.compose(for_category, :category) # perform join on tag association `category`
188
+ # Compose with a join
189
+ products = ProductsQuery.new.compose(CategoriesQuery.new, joins: :category)
179
190
 
180
- # equivalent to Tag.joins(:category).where(name: "Intel").where(categories: {name: "CPUs"})
191
+ # Equivalent to:
192
+ # Product.joins(:category)
193
+ # .where(products: { active: true })
194
+ # .where(categories: { featured: true })
181
195
  ```
182
196
 
183
- Eager loaded queries can also be composed (see below sections for more details).
184
-
185
- ### Quo::MergedQuery
186
-
187
- The new instance of `Quo::MergedQuery` from a compose process, retains references to the original entities that were
188
- composed. These are then used to create a more useful output from `to_s`, so that it is easier to understand what the
189
- merged query is actually made up of:
197
+ ## Transforming Results
190
198
 
191
199
  ```ruby
192
- q = FooQuery.new + BarQuery.new
193
- puts q
194
- # > "Quo::MergedQuery[FooQuery, BarQuery]"
200
+ query = UsersByState.new(state: "California")
201
+ .transform { |user| UserPresenter.new(user) }
202
+
203
+ # Results are automatically transformed
204
+ presenters = query.results.to_a # Array of UserPresenter objects
195
205
  ```
196
206
 
197
- ## Query Objects & Pagination
207
+ ## Custom Association Preloading
198
208
 
199
- Specify extra options to enable pagination:
209
+ ```ruby
210
+ class UsersWithOrders < Quo::RelationBackedQuery
211
+ include Quo::Preloadable
212
+
213
+ def query
214
+ User.all
215
+ end
200
216
 
201
- * `page`: the current page number to fetch
202
- * `page_size`: the number of elements to fetch in the page
217
+ def preload_associations(collection)
218
+ # Custom preloading logic
219
+ ActiveRecord::Associations::Preloader.new(
220
+ records: collection,
221
+ associations: [:profile, :orders]
222
+ ).call
223
+
224
+ collection
225
+ end
226
+ end
227
+ ```
203
228
 
204
- ### `Quo::EagerQuery` & `Quo::LoadedQuery` objects
229
+ ## Testing Helpers
205
230
 
206
- `Quo::EagerQuery` is a subclass of `Quo::Query` which can be used to create query objects which are 'eager loaded' by
207
- default. This is useful for encapsulating data that doesn't come from an ActiveRecord query or queries that
208
- execute immediately. Subclass EasyQuery and override `collection` to return the data you want to encapsulate.
231
+ ### Minitest
209
232
 
210
233
  ```ruby
211
- class MyEagerQuery < Quo::EagerQuery
212
- def collection
213
- [1, 2, 3]
234
+ class UserQueryTest < ActiveSupport::TestCase
235
+ include Quo::Minitest::Helpers
236
+
237
+ test "filters users by state" do
238
+ users = [User.new(name: "Alice"), User.new(name: "Bob")]
239
+
240
+ fake_query(UsersByState, results: users) do
241
+ result = UsersByState.new(state: "California").results.to_a
242
+ assert_equal users, result
243
+ end
214
244
  end
215
245
  end
216
- q = MyEagerQuery.new
217
- q.eager? # is it 'eager'? Yes it is!
218
- q.count # '3'
219
246
  ```
220
247
 
221
- Sometimes it is useful to create similar Queries without needing to create a explicit subclass of your own. For this
222
- use `Quo::LoadedQuery`:
248
+ ### RSpec
223
249
 
224
250
  ```ruby
225
- q = Quo::LoadedQuery.new([1, 2, 3])
226
- q.eager? # is it 'eager'? Yes it is!
227
- q.count # '3'
251
+ RSpec.describe UsersByState do
252
+ include Quo::RSpec::Helpers
253
+
254
+ it "filters users by state" do
255
+ users = [User.new(name: "Alice"), User.new(name: "Bob")]
256
+
257
+ fake_query(UsersByState, results: users) do
258
+ result = UsersByState.new(state: "California").results.to_a
259
+ expect(result).to eq(users)
260
+ end
261
+ end
262
+ end
228
263
  ```
229
264
 
230
- `Quo::EagerQuery` also uses `total_count` option value as the specified 'total count', useful when the data is
231
- actually just a page of the data and not the total count.
265
+ ## Project Organization
232
266
 
233
- Example of an EagerQuery used to wrap a page of enumerable data:
267
+ Suggested directory structure:
234
268
 
235
- ```ruby
236
- Quo::LoadedQuery.new(my_data, total_count: 100, page: current_page)
269
+ ```
270
+ app/
271
+ queries/
272
+ application_query.rb
273
+ users/
274
+ active_users_query.rb
275
+ by_state_query.rb
276
+ products/
277
+ featured_products_query.rb
237
278
  ```
238
279
 
239
- If a loaded query is `compose`d with other Query objects then it will be seen as an array-like, and concatenated to whatever
240
- results are returned from the other queries. An loaded or eager query will force all other queries to be eager loaded.
241
-
242
- ### Composition
243
-
244
- Examples of composition of eager loaded queries
280
+ Base classes:
245
281
 
246
282
  ```ruby
247
- class CachedTags < Quo::Query
248
- def query
249
- @tags ||= Tag.where(active: true).to_a
250
- end
283
+ # app/queries/application_query.rb
284
+ class ApplicationQuery < Quo::RelationBackedQuery
285
+ # Common functionality
251
286
  end
252
287
 
253
- composed = CachedTags.new(active: false) + [1, 2]
254
- composed.last
255
- # => 2
256
- composed.first
257
- # => #<Tag id: ...>
258
-
259
- Quo::LoadedQuery.new([3, 4]).compose(Quo::LoadedQuery.new([1, 2])).last
260
- # => 2
261
- Quo::Query.compose([1, 2], [3, 4]).last
262
- # => 4
288
+ # app/queries/application_collection_query.rb
289
+ class ApplicationCollectionQuery < Quo::CollectionBackedQuery
290
+ # Common functionality
291
+ end
263
292
  ```
264
293
 
265
- ## Transforming results
266
-
267
- Sometimes you want to specify a block to execute on each result for any method that returns results, such as `first`,
268
- `last` and `each`.
294
+ ## Installation
269
295
 
270
- This can be specified using the `transform(&block)` instance method. For example:
296
+ Add to your Gemfile:
271
297
 
272
298
  ```ruby
273
- TagsQuery.new(
274
- active: [true, false],
275
- page: 1,
276
- page_size: 30,
277
- ).transform { |tag| TagPresenter.new(tag) }
278
- .first
279
- # => #<TagPresenter ...>
299
+ gem "quo"
280
300
  ```
281
301
 
282
- ## Tests & stubbing
302
+ Then execute:
283
303
 
284
- Tests for Query objects themselves should exercise the actual underlying query. But in other code stubbing the query
285
- maybe desirable.
286
-
287
- The spec helper method `stub_query(query_class, {results: ..., with: ...})` can do this for you.
288
-
289
- It stubs `.new` on the Query object and returns instances of `LoadedQuery` instead with the given `results`.
290
- The `with` option is passed to the Query object on initialisation and used when setting up the method stub on the
291
- query class.
304
+ ```
305
+ $ bundle install
306
+ ```
292
307
 
293
- For example:
308
+ ## Configuration
294
309
 
295
310
  ```ruby
296
- stub_query(TagQuery, with: {name: "Something"}, results: [t1, t2])
297
- expect(TagQuery.new(name: "Something").first).to eql t1
311
+ # config/initializers/quo.rb
312
+ Quo.default_page_size = 25
313
+ Quo.max_page_size = 100
314
+ Quo.relation_backed_query_base_class = "ApplicationQuery"
315
+ Quo.collection_backed_query_base_class = "ApplicationCollectionQuery"
298
316
  ```
299
317
 
300
- *Note that*
301
-
302
- This returns an instance of EagerQuery, so will not work for cases were the actual type of the query instance is
303
- important or where you are doing a composition of queries backed by relations!
304
-
305
- If `compose` will be used then `Quo::Query.compose` needs to be stubbed. Something might be possible to make this
306
- nicer in future.
307
-
308
- ## Other reading
309
-
310
- See:
311
- * [Includes vs preload vs eager_load](http://blog.scoutapp.com/articles/2017/01/24/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where)
312
- * [Objects on Rails](http://objectsonrails.com/#sec-14)
313
-
318
+ ## Requirements
314
319
 
315
- ## Installation
316
-
317
- Install the gem and add to the application's Gemfile by executing:
318
-
319
- $ bundle add quo
320
-
321
- If bundler is not being used to manage dependencies, install the gem by executing:
322
-
323
- $ gem install quo
324
-
325
- ## Usage
326
-
327
- TODO: Write usage instructions here
320
+ - Ruby 3.1+
321
+ - Rails 7.0+
328
322
 
329
323
  ## Development
330
324
 
@@ -338,11 +332,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/steveg
338
332
 
339
333
  ## Inspired by `rectify`
340
334
 
341
- Note this implementation is loosely based on that in the `Rectify` gem; https://github.com/andypike/rectify.
342
-
343
- See https://github.com/andypike/rectify#query-objects for more information.
344
-
345
- Thanks to Andy Pike for the inspiration.
335
+ This implementation is inspired by the `Rectify` gem: https://github.com/andypike/rectify. Thanks to Andy Pike for the inspiration.
346
336
 
347
337
  ## License
348
338
 
data/Steepfile CHANGED
@@ -32,6 +32,4 @@ target :lib do
32
32
  ignore "lib/quo/rspec/*.rb"
33
33
  ignore "lib/tasks/*"
34
34
  ignore "lib/quo/railtie.rb"
35
-
36
- library "forwardable"
37
35
  end
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.0"
6
+
7
+ group :development, :test do
8
+ gem "sqlite3"
9
+ gem "rake", "~> 13.0"
10
+ gem "minitest", "~> 5.0"
11
+ gem "standard"
12
+ gem "steep"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.1"
6
+
7
+ group :development, :test do
8
+ gem "sqlite3"
9
+ gem "rake", "~> 13.0"
10
+ gem "minitest", "~> 5.0"
11
+ gem "standard"
12
+ gem "steep"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.2"
6
+
7
+ group :development, :test do
8
+ gem "sqlite3"
9
+ gem "rake", "~> 13.0"
10
+ gem "minitest", "~> 5.0"
11
+ gem "standard"
12
+ gem "steep"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 8.0"
6
+
7
+ group :development, :test do
8
+ gem "sqlite3"
9
+ gem "rake"
10
+ gem "minitest"
11
+ gem "standard"
12
+ gem "steep"
13
+ end
14
+
15
+ gemspec path: "../"