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.
- checksums.yaml +4 -4
- data/.standard.yml +4 -1
- data/Appraisals +15 -0
- data/CHANGELOG.md +78 -0
- data/Gemfile +6 -4
- data/LICENSE.txt +1 -1
- data/README.md +222 -232
- data/Steepfile +0 -2
- data/gemfiles/rails_7.0.gemfile +15 -0
- data/gemfiles/rails_7.1.gemfile +15 -0
- data/gemfiles/rails_7.2.gemfile +15 -0
- data/gemfiles/rails_8.0.gemfile +15 -0
- data/lib/quo/collection_backed_query.rb +87 -0
- data/lib/quo/collection_results.rb +44 -0
- data/lib/quo/composed_query.rb +278 -0
- data/lib/quo/engine.rb +11 -0
- data/lib/quo/minitest/helpers.rb +41 -0
- data/lib/quo/preloadable.rb +46 -0
- data/lib/quo/query.rb +97 -215
- data/lib/quo/relation_backed_query.rb +150 -0
- data/lib/quo/relation_backed_query_specification.rb +154 -0
- data/lib/quo/relation_results.rb +58 -0
- data/lib/quo/results.rb +48 -44
- data/lib/quo/rspec/helpers.rb +31 -9
- data/lib/quo/testing/collection_backed_fake.rb +29 -0
- data/lib/quo/testing/relation_backed_fake.rb +52 -0
- data/lib/quo/version.rb +3 -1
- data/lib/quo.rb +23 -30
- data/rbs_collection.yaml +0 -2
- data/sig/generated/quo/collection_backed_query.rbs +39 -0
- data/sig/generated/quo/collection_results.rbs +30 -0
- data/sig/generated/quo/composed_query.rbs +112 -0
- data/sig/generated/quo/engine.rbs +6 -0
- data/sig/generated/quo/preloadable.rbs +29 -0
- data/sig/generated/quo/query.rbs +98 -0
- data/sig/generated/quo/relation_backed_query.rbs +67 -0
- data/sig/generated/quo/relation_backed_query_specification.rbs +94 -0
- data/sig/generated/quo/relation_results.rbs +38 -0
- data/sig/generated/quo/results.rbs +39 -0
- data/sig/generated/quo/testing/collection_backed_fake.rbs +13 -0
- data/sig/generated/quo/testing/relation_backed_fake.rbs +23 -0
- data/sig/generated/quo/version.rbs +5 -0
- data/sig/generated/quo.rbs +9 -0
- data/sig/literal.rbs +7 -0
- metadata +77 -37
- data/lib/quo/eager_query.rb +0 -51
- data/lib/quo/loaded_query.rb +0 -18
- data/lib/quo/merged_query.rb +0 -36
- data/lib/quo/query_composer.rb +0 -78
- data/lib/quo/railtie.rb +0 -7
- data/lib/quo/utilities/callstack.rb +0 -21
- data/lib/quo/utilities/compose.rb +0 -18
- data/lib/quo/utilities/sanitize.rb +0 -19
- data/lib/quo/utilities/wrap.rb +0 -23
- data/lib/quo/wrapped_query.rb +0 -18
- data/sig/quo/eager_query.rbs +0 -15
- data/sig/quo/loaded_query.rbs +0 -7
- data/sig/quo/merged_query.rbs +0 -19
- data/sig/quo/query.rbs +0 -83
- data/sig/quo/query_composer.rbs +0 -32
- data/sig/quo/results.rbs +0 -22
- data/sig/quo/utilities/callstack.rbs +0 -7
- data/sig/quo/utilities/compose.rbs +0 -8
- data/sig/quo/utilities/sanitize.rbs +0 -9
- data/sig/quo/utilities/wrap.rbs +0 -11
- data/sig/quo/wrapped_query.rbs +0 -11
- 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
|
4
|
-
interface.
|
3
|
+
Quo helps you organize database queries into reusable, composable, and testable objects.
|
5
4
|
|
6
|
-
|
7
|
-
data from a query and reuse it.
|
5
|
+
## Core Features
|
8
6
|
|
9
|
-
|
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
|
-
|
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
|
-
|
18
|
+
Query objects encapsulate query logic in dedicated classes, making complex queries more manageable and reusable.
|
21
19
|
|
22
|
-
|
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
|
-
|
24
|
+
## Creating Query Objects
|
25
25
|
|
26
|
-
-
|
27
|
-
- an Array (an 'eager loaded' query)
|
28
|
-
- or another `Quo::Query` instance.
|
26
|
+
### Relation-Backed Queries
|
29
27
|
|
30
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
42
|
+
# Create and use the query
|
43
|
+
query = RecentActiveUsers.new(days_ago: 7)
|
44
|
+
results = query.results
|
37
45
|
|
38
|
-
|
46
|
+
# Work with results
|
47
|
+
results.each { |user| puts user.email }
|
48
|
+
puts "Found #{results.count} users"
|
49
|
+
```
|
39
50
|
|
40
|
-
|
51
|
+
### Collection-Backed Queries
|
41
52
|
|
42
|
-
|
53
|
+
For queries based on any Enumerable collection:
|
43
54
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
66
|
+
# Use the query
|
67
|
+
admins = CachedUsers.new(role: "admin").results
|
68
|
+
```
|
52
69
|
|
53
|
-
##
|
70
|
+
## Quick Queries with Wrap
|
54
71
|
|
55
|
-
|
72
|
+
Create query objects without subclassing:
|
56
73
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
79
|
+
# Collection-backed
|
80
|
+
items_query = Quo::CollectionBackedQuery.wrap([1, 2, 3]).new
|
81
|
+
items = items_query.results
|
82
|
+
```
|
66
83
|
|
67
|
-
##
|
84
|
+
## Type-Safe Properties
|
68
85
|
|
69
|
-
Quo
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
105
|
+
## Fluent API for Building Queries
|
85
106
|
|
86
|
-
|
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
|
-
|
89
|
-
|
90
|
-
3. compose two query objects which return array-likes
|
114
|
+
users = query.results
|
115
|
+
```
|
91
116
|
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
to the array-like with `+`
|
127
|
+
Each method returns a new query instance without modifying the original.
|
97
128
|
|
98
|
-
|
129
|
+
## Pagination
|
99
130
|
|
100
|
-
|
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
|
-
|
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
|
-
|
148
|
+
Combine multiple queries:
|
107
149
|
|
108
150
|
```ruby
|
109
|
-
class
|
151
|
+
class ActiveUsers < Quo::RelationBackedQuery
|
110
152
|
def query
|
111
|
-
|
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
|
157
|
+
class PremiumUsers < Quo::RelationBackedQuery
|
118
158
|
def query
|
119
|
-
|
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
|
-
|
126
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
173
|
+
### Composing with Joins
|
162
174
|
|
163
175
|
```ruby
|
164
|
-
class
|
176
|
+
class ProductsQuery < Quo::RelationBackedQuery
|
165
177
|
def query
|
166
|
-
|
178
|
+
Product.where(active: true)
|
167
179
|
end
|
168
180
|
end
|
169
181
|
|
170
|
-
class
|
182
|
+
class CategoriesQuery < Quo::RelationBackedQuery
|
171
183
|
def query
|
172
|
-
Category.where(
|
184
|
+
Category.where(featured: true)
|
173
185
|
end
|
174
186
|
end
|
175
187
|
|
176
|
-
|
177
|
-
|
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
|
-
#
|
191
|
+
# Equivalent to:
|
192
|
+
# Product.joins(:category)
|
193
|
+
# .where(products: { active: true })
|
194
|
+
# .where(categories: { featured: true })
|
181
195
|
```
|
182
196
|
|
183
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|
-
##
|
207
|
+
## Custom Association Preloading
|
198
208
|
|
199
|
-
|
209
|
+
```ruby
|
210
|
+
class UsersWithOrders < Quo::RelationBackedQuery
|
211
|
+
include Quo::Preloadable
|
212
|
+
|
213
|
+
def query
|
214
|
+
User.all
|
215
|
+
end
|
200
216
|
|
201
|
-
|
202
|
-
|
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
|
-
|
229
|
+
## Testing Helpers
|
205
230
|
|
206
|
-
|
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
|
212
|
-
|
213
|
-
|
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
|
-
|
222
|
-
use `Quo::LoadedQuery`:
|
248
|
+
### RSpec
|
223
249
|
|
224
250
|
```ruby
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
231
|
-
actually just a page of the data and not the total count.
|
265
|
+
## Project Organization
|
232
266
|
|
233
|
-
|
267
|
+
Suggested directory structure:
|
234
268
|
|
235
|
-
```
|
236
|
-
|
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
|
-
|
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
|
-
|
248
|
-
|
249
|
-
|
250
|
-
end
|
283
|
+
# app/queries/application_query.rb
|
284
|
+
class ApplicationQuery < Quo::RelationBackedQuery
|
285
|
+
# Common functionality
|
251
286
|
end
|
252
287
|
|
253
|
-
|
254
|
-
|
255
|
-
#
|
256
|
-
|
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
|
-
##
|
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
|
-
|
296
|
+
Add to your Gemfile:
|
271
297
|
|
272
298
|
```ruby
|
273
|
-
|
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
|
-
|
302
|
+
Then execute:
|
283
303
|
|
284
|
-
|
285
|
-
|
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
|
-
|
308
|
+
## Configuration
|
294
309
|
|
295
310
|
```ruby
|
296
|
-
|
297
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
@@ -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: "../"
|