quo 1.0.0.beta2 → 2.0.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.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +17 -0
- data/.devcontainer/compose.yml +10 -0
- data/.devcontainer/devcontainer.json +12 -0
- data/Appraisals +4 -12
- data/CHANGELOG.md +112 -1
- data/CLAUDE.md +19 -0
- data/Gemfile +7 -1
- data/LICENSE.txt +1 -1
- data/README.md +496 -203
- data/Rakefile +66 -6
- data/UPGRADING.md +216 -0
- data/badges/coverage_badge_total.svg +35 -0
- data/badges/rubycritic_badge_score.svg +35 -0
- data/claude-skill/README.md +100 -0
- data/claude-skill/SKILL.md +442 -0
- data/claude-skill/references/API_REFERENCE.md +462 -0
- data/claude-skill/references/COMPOSITION.md +396 -0
- data/claude-skill/references/PAGINATION.md +396 -0
- data/claude-skill/references/QUERY_TYPES.md +297 -0
- data/claude-skill/references/TRANSFORMERS.md +282 -0
- data/context/01-core-architecture.md +247 -0
- data/context/02-query-types-implementation.md +355 -0
- data/context/03-composition-transformation.md +441 -0
- data/context/04-pagination-results.md +485 -0
- data/context/05-testing-configuration.md +491 -0
- data/context/06-advanced-patterns-examples.md +153 -0
- data/gemfiles/rails_8.0.gemfile +10 -5
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/lib/generators/quo/install/USAGE +21 -0
- data/lib/generators/quo/install/install_generator.rb +63 -0
- data/lib/quo/collection_backed_query.rb +21 -15
- data/lib/quo/collection_results.rb +1 -0
- data/lib/quo/composed_collection_backed_query.rb +42 -0
- data/lib/quo/composed_instance.rb +144 -0
- data/lib/quo/composed_query.rb +43 -178
- data/lib/quo/composed_relation_backed_query.rb +42 -0
- data/lib/quo/composing/base_strategy.rb +22 -0
- data/lib/quo/composing/class_strategy.rb +86 -0
- data/lib/quo/composing/class_strategy_registry.rb +31 -0
- data/lib/quo/composing/query_classes_strategy.rb +38 -0
- data/lib/quo/composing.rb +81 -0
- data/lib/quo/engine.rb +1 -0
- data/lib/quo/minitest/helpers.rb +14 -24
- data/lib/quo/preloadable.rb +1 -0
- data/lib/quo/query.rb +22 -5
- data/lib/quo/relation_backed_query.rb +24 -18
- data/lib/quo/relation_backed_query_specification.rb +44 -25
- data/lib/quo/relation_results.rb +1 -0
- data/lib/quo/results.rb +31 -2
- data/lib/quo/rspec/helpers.rb +15 -26
- data/lib/quo/testing/collection_backed_fake.rb +1 -0
- data/lib/quo/testing/fake_helpers.rb +30 -0
- data/lib/quo/testing/relation_backed_fake.rb +1 -0
- data/lib/quo/version.rb +1 -1
- data/lib/quo/wrapped_collection_backed_query.rb +21 -0
- data/lib/quo/wrapped_relation_backed_query.rb +21 -0
- data/lib/quo.rb +8 -0
- data/quo.png +0 -0
- data/sig/generated/quo/collection_backed_query.rbs +10 -4
- data/sig/generated/quo/collection_results.rbs +1 -0
- data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
- data/sig/generated/quo/composed_instance.rbs +61 -0
- data/sig/generated/quo/composed_query.rbs +23 -56
- data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
- data/sig/generated/quo/composing/base_strategy.rbs +16 -0
- data/sig/generated/quo/composing/class_strategy.rbs +38 -0
- data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
- data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
- data/sig/generated/quo/composing.rbs +40 -0
- data/sig/generated/quo/engine.rbs +1 -0
- data/sig/generated/quo/minitest/helpers.rbs +12 -0
- data/sig/generated/quo/preloadable.rbs +1 -0
- data/sig/generated/quo/query.rbs +15 -4
- data/sig/generated/quo/relation_backed_query.rbs +15 -5
- data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
- data/sig/generated/quo/relation_results.rbs +1 -0
- data/sig/generated/quo/results.rbs +11 -0
- data/sig/generated/quo/rspec/helpers.rbs +12 -0
- data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
- data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
- data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
- data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
- data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
- data/sig/generated/quo.rbs +1 -0
- data/website/.gitignore +6 -0
- data/website/.nojekyll +0 -0
- data/website/404.html +26 -0
- data/website/Gemfile +24 -0
- data/website/_config.yml +50 -0
- data/website/_data/navigation.yml +8 -0
- data/website/_data/sidebar.yml +2 -0
- data/website/_data/social_links.yml +3 -0
- data/website/_docs/api.md +261 -0
- data/website/_docs/get-started.md +289 -0
- data/website/assets/quo.png +0 -0
- data/website/index.md +141 -0
- metadata +70 -13
- data/gemfiles/rails_7.0.gemfile +0 -15
- data/gemfiles/rails_7.1.gemfile +0 -15
- data/gemfiles/rails_7.2.gemfile +0 -15
data/Rakefile
CHANGED
|
@@ -1,14 +1,74 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
t.libs << "test"
|
|
8
|
-
t.libs << "lib"
|
|
9
|
-
t.test_files = FileList["test/**/*_test.rb"]
|
|
4
|
+
desc "Run tests"
|
|
5
|
+
task :test do
|
|
6
|
+
sh "bin/test"
|
|
10
7
|
end
|
|
11
8
|
|
|
12
9
|
require "standard/rake"
|
|
13
10
|
|
|
14
11
|
task default: %i[test standard]
|
|
12
|
+
|
|
13
|
+
# Add RubyCritic task with badge generation
|
|
14
|
+
begin
|
|
15
|
+
require "rubycritic_small_badge"
|
|
16
|
+
require "rubycritic/rake_task"
|
|
17
|
+
|
|
18
|
+
RubyCriticSmallBadge.configure do |config|
|
|
19
|
+
config.minimum_score = 90
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
RubyCritic::RakeTask.new do |task|
|
|
23
|
+
task.paths = FileList["lib/**/*.rb"]
|
|
24
|
+
|
|
25
|
+
task.options = %(--custom-format RubyCriticSmallBadge::Report
|
|
26
|
+
--minimum-score #{RubyCriticSmallBadge.config.minimum_score}
|
|
27
|
+
--coverage-path coverage/.resultset.json
|
|
28
|
+
--no-browser)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Run tests with coverage and then RubyCritic"
|
|
32
|
+
task rubycritic_with_coverage: [:coverage, :rubycritic]
|
|
33
|
+
rescue LoadError
|
|
34
|
+
desc "Run RubyCritic (not available)"
|
|
35
|
+
task :rubycritic do
|
|
36
|
+
puts "RubyCritic is not available"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "Run code coverage"
|
|
41
|
+
task :coverage do
|
|
42
|
+
ENV["COVERAGE"] = "1"
|
|
43
|
+
Rake::Task["test"].invoke
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
namespace :website do
|
|
47
|
+
desc "Build the documentation website"
|
|
48
|
+
task :build do
|
|
49
|
+
Dir.chdir("website") do
|
|
50
|
+
puts "Building documentation website..."
|
|
51
|
+
system "bundle install"
|
|
52
|
+
system "bundle exec jekyll build"
|
|
53
|
+
puts "Website built in website/_site/"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "Serve the documentation website locally"
|
|
58
|
+
task :serve do
|
|
59
|
+
Dir.chdir("website") do
|
|
60
|
+
puts "Starting local documentation server..."
|
|
61
|
+
puts "View the website at http://localhost:4000/"
|
|
62
|
+
system "bundle install"
|
|
63
|
+
system "bundle exec jekyll serve"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "Clean the documentation website build"
|
|
68
|
+
task :clean do
|
|
69
|
+
Dir.chdir("website") do
|
|
70
|
+
puts "Cleaning website build..."
|
|
71
|
+
system "bundle exec jekyll clean"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Upgrading Quo
|
|
2
|
+
|
|
3
|
+
## Upgrading from 1.x to 2.0
|
|
4
|
+
|
|
5
|
+
Quo 2.0 is a structural rework of query composition. Most code continues
|
|
6
|
+
to work unchanged. There are two intentional behavioural changes worth
|
|
7
|
+
knowing about before you bump, plus a new value-form API you'll want to
|
|
8
|
+
adopt for hot paths.
|
|
9
|
+
|
|
10
|
+
### TL;DR
|
|
11
|
+
|
|
12
|
+
- Composition still uses `+` / `compose` / `merge`. Same API.
|
|
13
|
+
- **Class composition** (`SomeClass + OtherClass`) — unchanged. Returns a Class.
|
|
14
|
+
- **Instance composition** (`some_instance + other_instance`) — now returns a
|
|
15
|
+
concrete value (`Quo::ComposedRelationBackedQuery` or
|
|
16
|
+
`Quo::ComposedCollectionBackedQuery`), not an anonymous class. Allocates
|
|
17
|
+
no new classes per call.
|
|
18
|
+
- New: `Quo::RelationBackedQuery.from(relation)` and
|
|
19
|
+
`Quo::CollectionBackedQuery.from(enumerable)` — value-form constructors,
|
|
20
|
+
for use instead of `wrap(rel).new` on hot paths.
|
|
21
|
+
- 1.x's "prop fan-out from a synthesised parent class" is gone. Each
|
|
22
|
+
operand keeps its own constructor-time props. Two subtle consequences
|
|
23
|
+
documented below.
|
|
24
|
+
- ~2× faster instance composition, with **zero anonymous classes per call**.
|
|
25
|
+
|
|
26
|
+
### Why
|
|
27
|
+
|
|
28
|
+
1.x had a single composition path. Both `Q1 + Q2` (between classes) and
|
|
29
|
+
`q1 + q2` (between instances) went through the same machinery, which
|
|
30
|
+
allocated a fresh anonymous class on every call. For instances, the
|
|
31
|
+
class was then immediately instantiated once and discarded — pure
|
|
32
|
+
waste, paid per request.
|
|
33
|
+
|
|
34
|
+
2.0 splits the two cases. Class composition keeps the class-allocation
|
|
35
|
+
machinery (it's the right shape for *defining a new type* once at
|
|
36
|
+
code-load time). Instance composition produces a value instead — a
|
|
37
|
+
small struct that holds the two operands and walks them at unwrap
|
|
38
|
+
time. No anonymous classes, no `prop` re-registration.
|
|
39
|
+
|
|
40
|
+
### What's the same
|
|
41
|
+
|
|
42
|
+
- The `+` / `compose` / `merge` API at both class and instance levels.
|
|
43
|
+
- The leaf query class API: `prop`, `query`, `collection`, `transform`,
|
|
44
|
+
`wrap`, `with_specification`, etc.
|
|
45
|
+
- Pagination and transformer surfaces on every Quo::Query.
|
|
46
|
+
- Specifications (`order(...)`, `joins(...)`, `where(...)`, `distinct`,
|
|
47
|
+
etc.) on relation-backed queries, including when applied to a composed
|
|
48
|
+
instance.
|
|
49
|
+
- `Quo::ComposedQuery` is still the marker module used by class
|
|
50
|
+
composition. Existing `kind_of?(Quo::ComposedQuery)` checks on
|
|
51
|
+
class-composed results keep working.
|
|
52
|
+
- Composed and wrapped value-form instances are subclasses of the
|
|
53
|
+
configured base classes (`Quo.relation_backed_query_base_class`,
|
|
54
|
+
`Quo.collection_backed_query_base_class` — typically
|
|
55
|
+
`ApplicationRelationQuery` / `ApplicationCollectionQuery`). Anything
|
|
56
|
+
defined on those base classes is available on `.from`-constructed and
|
|
57
|
+
instance-composed values. The base classes are resolved at autoload
|
|
58
|
+
time, so configure `Quo.relation_backed_query_base_class = ...`
|
|
59
|
+
in an initializer (it runs before eager load / first reference in
|
|
60
|
+
Rails apps).
|
|
61
|
+
|
|
62
|
+
### What's intentionally different
|
|
63
|
+
|
|
64
|
+
#### 1. Prop fan-out at instance composition is gone
|
|
65
|
+
|
|
66
|
+
In 1.x, `(Q1.new(score: 0.5) + Q2.new(score: 0.7))` produced a merged
|
|
67
|
+
class whose effective `score` was 0.7 (right won) and that value was
|
|
68
|
+
fanned to every child at unwrap time.
|
|
69
|
+
|
|
70
|
+
In 2.x, each operand keeps its own constructor-time props. The merged
|
|
71
|
+
relation is the AND of both operands' filters, evaluated independently:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# 1.x: filter = (score < 0.7 AND score < 0.7) -> score < 0.7
|
|
75
|
+
# 2.x: filter = (score < 0.5 AND score < 0.7) -> score < 0.5
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If you relied on 1.x's "right wins everywhere" behaviour, you now need
|
|
79
|
+
to construct the leaf with the value you actually want:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# Equivalent to 1.x's behaviour
|
|
83
|
+
Q1.new(score: 0.7) + Q2.new(score: 0.7)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### 2. Pagination inherits as a coupled (page, page_size) pair
|
|
87
|
+
|
|
88
|
+
In 1.x, `page` and `page_size` were fanned independently, so a composed
|
|
89
|
+
query could end up with `page: 2` from the left operand and
|
|
90
|
+
`page_size: 50` from the right, even though neither operand was
|
|
91
|
+
constructed with that pair.
|
|
92
|
+
|
|
93
|
+
In 2.x, pagination inherits *as a unit* from whichever operand is
|
|
94
|
+
paginated (i.e. has a non-nil `page`). Right wins if both are.
|
|
95
|
+
`page_size` alone doesn't make a query paginated, because every
|
|
96
|
+
Quo::Query has a default `page_size`.
|
|
97
|
+
|
|
98
|
+
If you previously relied on the cross-pollination behaviour, set
|
|
99
|
+
pagination explicitly on the composed instance:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
(q1 + q2).copy(page: 1, page_size: 50).results
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### 3. `wrap` is type-strict at definition time
|
|
106
|
+
|
|
107
|
+
`Quo::RelationBackedQuery.wrap(x)` now raises `ArgumentError` at call
|
|
108
|
+
time if `x` is not an `ActiveRecord::Relation` or a
|
|
109
|
+
`Quo::RelationBackedQuery` instance. Same for
|
|
110
|
+
`Quo::CollectionBackedQuery.wrap(x)` — `x` must be an `Enumerable` or a
|
|
111
|
+
`Quo::CollectionBackedQuery` instance. Previously these would silently
|
|
112
|
+
accept the wrong type and fail later at `.unwrap` / `.results` time,
|
|
113
|
+
often with a confusing error.
|
|
114
|
+
|
|
115
|
+
Block forms still defer the check to first call (the block body can
|
|
116
|
+
return anything until it's evaluated).
|
|
117
|
+
|
|
118
|
+
If you were relying on cross-type wrapping (e.g. handing an array to
|
|
119
|
+
`RelationBackedQuery.wrap`), use the matching constructor:
|
|
120
|
+
|
|
121
|
+
- in-memory collection → `Quo::CollectionBackedQuery.wrap(arr)` or
|
|
122
|
+
`Quo::CollectionBackedQuery.from(arr)`
|
|
123
|
+
- AR relation → `Quo::RelationBackedQuery.wrap(rel)` or
|
|
124
|
+
`Quo::RelationBackedQuery.from(rel)`
|
|
125
|
+
|
|
126
|
+
#### 4. Minimum Rails 8
|
|
127
|
+
|
|
128
|
+
Quo 2.x requires `activerecord >= 8.0` and `activesupport >= 8.0`. For
|
|
129
|
+
Rails 7.x, stay on Quo 1.x.
|
|
130
|
+
|
|
131
|
+
### What's new
|
|
132
|
+
|
|
133
|
+
#### `.from` — value-form constructors
|
|
134
|
+
|
|
135
|
+
For wrapping a bare relation or enumerable as a Quo::Query at a call
|
|
136
|
+
site, use `.from` instead of `wrap(...).new`:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# 1.x — still works, but allocates a new class per call.
|
|
140
|
+
Quo::RelationBackedQuery.wrap(Comment.where(read: false)).new.results
|
|
141
|
+
|
|
142
|
+
# 2.x — value form, no Class.new per call.
|
|
143
|
+
Quo::RelationBackedQuery.from(Comment.where(read: false)).results
|
|
144
|
+
|
|
145
|
+
# Same for collections.
|
|
146
|
+
Quo::CollectionBackedQuery.from([1, 2, 3]).results
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`.from` returns an instance of `Quo::WrappedRelationBackedQuery` /
|
|
150
|
+
`Quo::WrappedCollectionBackedQuery`. They behave like any other
|
|
151
|
+
Quo::Query — composable, paginatable, transformable.
|
|
152
|
+
|
|
153
|
+
Keep using `wrap` when you want a *class* (constant assignment, block
|
|
154
|
+
form with typed props):
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Still right in 2.x — type-defining use.
|
|
158
|
+
RecentComments = Quo::RelationBackedQuery.wrap(props: {since: Time}) do
|
|
159
|
+
Comment.where("created_at > ?", since)
|
|
160
|
+
end
|
|
161
|
+
RecentComments.new(since: 1.day.ago).results
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The skill bundled with Quo (`bin/rails generate quo:install`) covers
|
|
165
|
+
the class-vs-instance composition cut and the new `.from` API.
|
|
166
|
+
|
|
167
|
+
### Performance
|
|
168
|
+
|
|
169
|
+
Measured on a representative 3-leaf instance composition (5000
|
|
170
|
+
iterations of `(L.new + R.new + T.new).unwrap.to_sql`):
|
|
171
|
+
|
|
172
|
+
| metric | 1.0.0 | 2.0.0 | Δ |
|
|
173
|
+
|---------------------|-------------|-------------|-------------|
|
|
174
|
+
| wall | 3805 ms | 1740 ms | 2.2× faster |
|
|
175
|
+
| GC time | 238 ms | 61 ms | 4× less |
|
|
176
|
+
| total allocations | 8,000,091 | 4,755,036 | -41% |
|
|
177
|
+
| T_CLASS allocations | 753 | **0** | -100% |
|
|
178
|
+
|
|
179
|
+
The "zero T_CLASS per call" is the structural win — instance
|
|
180
|
+
composition no longer goes through `Class.new`.
|
|
181
|
+
|
|
182
|
+
### Migrating
|
|
183
|
+
|
|
184
|
+
For most apps, just bumping the gem version and running tests is
|
|
185
|
+
sufficient. If tests fail, they'll fall into one of these categories:
|
|
186
|
+
|
|
187
|
+
1. **Test asserted 1.x's prop-fan-out "right wins at unwrap time".**
|
|
188
|
+
Update the test to reflect the new behaviour (filter is the AND of
|
|
189
|
+
each operand's own filter) or restructure your call site to
|
|
190
|
+
construct each leaf with the values you want.
|
|
191
|
+
|
|
192
|
+
2. **Test asserted the cross-pollinated pagination behaviour.** Set
|
|
193
|
+
pagination explicitly on the composed instance, not at the leaf
|
|
194
|
+
level.
|
|
195
|
+
|
|
196
|
+
3. **Test calls `composed.copy(some_user_prop:)` and expects a single
|
|
197
|
+
prop value to be observed.** v2's copy applies the new value to
|
|
198
|
+
every operand that declares the prop (right-wins precedence on
|
|
199
|
+
reads when both have it). Behaviour matches if you wanted "this
|
|
200
|
+
value, everywhere". If you wanted "this value on one side only",
|
|
201
|
+
construct the new operand explicitly: `q.copy(left: q.left.copy(prop: x))`.
|
|
202
|
+
|
|
203
|
+
### Performance opportunities
|
|
204
|
+
|
|
205
|
+
After upgrading, scan for these patterns and consider rewriting:
|
|
206
|
+
|
|
207
|
+
- `Quo::RelationBackedQuery.wrap(some_relation).new` → use
|
|
208
|
+
`Quo::RelationBackedQuery.from(some_relation)`.
|
|
209
|
+
- `(Q1 + Q2).new(prop: value)` at a call site → if `Q1` and `Q2` are
|
|
210
|
+
classes, this still allocates a new class per request. Either hoist
|
|
211
|
+
the composition to a constant (`MyType = Q1 + Q2`) and instantiate
|
|
212
|
+
`MyType.new(...)` per request, or switch to instance composition
|
|
213
|
+
(`Q1.new(prop: value) + Q2.new(...)` ).
|
|
214
|
+
|
|
215
|
+
Both patterns are clearly explained in the bundled skill — see
|
|
216
|
+
`.claude/skills/quo/SKILL.md` after running `bin/rails generate quo:install`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<svg contentScriptType="text/ecmascript" contentStyleType="text/css" preserveAspectRatio="xMidYMid meet" version="1.0" height="20" width="120"
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
<linearGradient id="smooth" x2="0" y2="120">
|
|
7
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
8
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<clipPath id="round">
|
|
11
|
+
<rect height="20" width="120" rx="3" fill="#ffffcc"/>
|
|
12
|
+
</clipPath>
|
|
13
|
+
<g clip-path="url(#round)">
|
|
14
|
+
<rect height="20" width="60" fill="#555"/>
|
|
15
|
+
<rect x="60" height="20" width="60" fill="#cccc00"/>
|
|
16
|
+
<rect height="20" width="120" fill="url(#smooth)"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
|
|
19
|
+
<text x="30" y="15" fill="#010101" fill-opacity="0.3">
|
|
20
|
+
scov total
|
|
21
|
+
</text>
|
|
22
|
+
<text x="30" y="14">
|
|
23
|
+
scov total
|
|
24
|
+
</text>
|
|
25
|
+
</g>
|
|
26
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
|
|
27
|
+
<text x="90" y="15" fill="#010101" fill-opacity="0.3">
|
|
28
|
+
96%
|
|
29
|
+
</text>
|
|
30
|
+
<text x="90" y="14">
|
|
31
|
+
96%
|
|
32
|
+
</text>
|
|
33
|
+
</g>
|
|
34
|
+
|
|
35
|
+
</svg>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<svg contentScriptType="text/ecmascript" contentStyleType="text/css" preserveAspectRatio="xMidYMid meet" version="1.0" height="20" width="200"
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
<linearGradient id="smooth" x2="0" y2="200">
|
|
7
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
8
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<clipPath id="round">
|
|
11
|
+
<rect height="20" width="200" rx="3" fill="#fff"/>
|
|
12
|
+
</clipPath>
|
|
13
|
+
<g clip-path="url(#round)">
|
|
14
|
+
<rect height="20" width="100" fill="#555"/>
|
|
15
|
+
<rect x="100" height="20" width="100" fill="#ff0000"/>
|
|
16
|
+
<rect height="20" width="200" fill="url(#smooth)"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
|
|
19
|
+
<text x="50" y="15" fill="#010101" fill-opacity="0.3">
|
|
20
|
+
rubycritic score
|
|
21
|
+
</text>
|
|
22
|
+
<text x="50" y="14">
|
|
23
|
+
rubycritic score
|
|
24
|
+
</text>
|
|
25
|
+
</g>
|
|
26
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
|
|
27
|
+
<text x="150" y="15" fill="#010101" fill-opacity="0.3">
|
|
28
|
+
87.73/100.0
|
|
29
|
+
</text>
|
|
30
|
+
<text x="150" y="14">
|
|
31
|
+
87.73/100.0
|
|
32
|
+
</text>
|
|
33
|
+
</g>
|
|
34
|
+
|
|
35
|
+
</svg>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Claude Code skill: Quo
|
|
2
|
+
|
|
3
|
+
A [Claude Code](https://claude.com/claude-code) skill that teaches Claude
|
|
4
|
+
how to use the [Quo gem](https://github.com/stevegeek/quo) for building
|
|
5
|
+
composable, type-safe query objects in a Rails application.
|
|
6
|
+
|
|
7
|
+
## What this skill provides
|
|
8
|
+
|
|
9
|
+
`SKILL.md` (loaded automatically by Claude Code) covers:
|
|
10
|
+
|
|
11
|
+
- The two composition modes (class vs instance) and when to use each
|
|
12
|
+
- Core query object patterns for ActiveRecord and collections
|
|
13
|
+
- Type-safe property declarations using Literal
|
|
14
|
+
- Pagination, transformer, and `wrap` patterns
|
|
15
|
+
- A short cookbook of common patterns
|
|
16
|
+
|
|
17
|
+
`references/` (loaded on demand by Claude when it needs depth):
|
|
18
|
+
|
|
19
|
+
| File | Read when… |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `references/QUERY_TYPES.md` | Detail on RelationBackedQuery vs CollectionBackedQuery |
|
|
22
|
+
| `references/COMPOSITION.md` | Composition modes, merge strategies, joins, conditional building |
|
|
23
|
+
| `references/PAGINATION.md` | Page navigation, counts, unpaginated access |
|
|
24
|
+
| `references/TRANSFORMERS.md` | Result transformation, presenter patterns |
|
|
25
|
+
| `references/API_REFERENCE.md` | Method-by-method reference for queries and results |
|
|
26
|
+
|
|
27
|
+
## Versioning
|
|
28
|
+
|
|
29
|
+
Each markdown file in this skill carries a banner near the top declaring
|
|
30
|
+
which Quo version it targets, e.g.:
|
|
31
|
+
|
|
32
|
+
> **Targets Quo `~> 2.0`.**
|
|
33
|
+
|
|
34
|
+
When you upgrade the gem, re-run the install generator with `--force` to
|
|
35
|
+
refresh the skill content:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bin/rails generate quo:install --force
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
The intended path is the bundled Rails generator (ships with Quo `~> 2.0`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bin/rails generate quo:install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This copies the skill into your app's `.claude/skills/quo/` directory.
|
|
50
|
+
Claude Code picks it up automatically on the next session.
|
|
51
|
+
|
|
52
|
+
You can also install manually by copying this directory to
|
|
53
|
+
`.claude/skills/quo/` in your project root.
|
|
54
|
+
|
|
55
|
+
### CLAUDE.md fragment
|
|
56
|
+
|
|
57
|
+
The generator can optionally append a "Quo" section to your project's
|
|
58
|
+
top-level `CLAUDE.md`, telling Claude that the project uses Quo and
|
|
59
|
+
where the skill lives. This is opt-in:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bin/rails generate quo:install --with-claude-md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If you prefer to manage `CLAUDE.md` by hand, just add a line like:
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
## Quo
|
|
69
|
+
|
|
70
|
+
This project uses the Quo gem for query objects. See
|
|
71
|
+
`.claude/skills/quo/SKILL.md` for usage guidance.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
When Claude Code starts a session, it loads `SKILL.md` automatically.
|
|
77
|
+
That makes the core concepts and quick references immediately available
|
|
78
|
+
in context. When Claude needs depth on a specific topic, it reads the
|
|
79
|
+
relevant file from `references/` on demand.
|
|
80
|
+
|
|
81
|
+
The skill is organised around progressive disclosure: keep `SKILL.md`
|
|
82
|
+
short and grep-friendly; put the long detail in references.
|
|
83
|
+
|
|
84
|
+
## Contributing
|
|
85
|
+
|
|
86
|
+
To improve this skill:
|
|
87
|
+
|
|
88
|
+
1. Edit the relevant markdown file
|
|
89
|
+
2. Keep `SKILL.md` concise — quick references and patterns
|
|
90
|
+
3. Put detailed material in the `references/` files
|
|
91
|
+
4. Update the version banner if the API surface you describe changed
|
|
92
|
+
5. Use the `Post` / `Author` / `Comment` fixture models from the gem's
|
|
93
|
+
own test suite for examples — they're verifiable against the gem's
|
|
94
|
+
tests and avoid project-specific terminology
|
|
95
|
+
|
|
96
|
+
## Related
|
|
97
|
+
|
|
98
|
+
- Quo source: <https://github.com/stevegeek/quo>
|
|
99
|
+
- Quo documentation site: <https://quo-gem.diaconou.com/>
|
|
100
|
+
- Literal (type system): <https://github.com/joeldrapper/literal>
|