graph_weaver 0.7.0 → 0.7.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/README.md +40 -88
  4. data/docs/alternatives.md +1 -7
  5. data/docs/cassettes.md +54 -59
  6. data/docs/editors.md +32 -47
  7. data/docs/errors.md +261 -369
  8. data/docs/federation.md +650 -837
  9. data/docs/generated_modules.md +380 -463
  10. data/docs/getting_started.md +211 -428
  11. data/docs/i18n.md +114 -177
  12. data/docs/logging.md +127 -116
  13. data/docs/real_world.md +26 -39
  14. data/docs/scalars.md +277 -310
  15. data/docs/testing.md +343 -486
  16. data/docs/transports.md +203 -268
  17. data/docs/upgrading.md +211 -560
  18. data/examples/README.md +38 -0
  19. data/examples/countries.rb +39 -0
  20. data/examples/federation.rb +62 -0
  21. data/examples/github/generate.rb +20 -0
  22. data/examples/github/generated/star_mutation.rb +126 -0
  23. data/examples/github/generated/stargazers_query.rb +232 -0
  24. data/examples/github/generated/starred_query.rb +151 -0
  25. data/examples/github/queries/star.graphql +8 -0
  26. data/examples/github/queries/stargazers.graphql +22 -0
  27. data/examples/github/queries/starred.graphql +11 -0
  28. data/examples/github/run.rb +43 -0
  29. data/examples/github/setup.rb +18 -0
  30. data/examples/rick_and_morty.rb +57 -0
  31. data/graph_weaver.gemspec +12 -3
  32. data/lib/graph_weaver/client.rb +30 -1
  33. data/lib/graph_weaver/codegen/emit.rb +5 -11
  34. data/lib/graph_weaver/codegen.rb +23 -55
  35. data/lib/graph_weaver/context_seam.rb +54 -0
  36. data/lib/graph_weaver/errors.rb +23 -15
  37. data/lib/graph_weaver/federation.rb +11 -2
  38. data/lib/graph_weaver/graph.rb +39 -29
  39. data/lib/graph_weaver/in_process.rb +15 -9
  40. data/lib/graph_weaver/internal/endpoint.rb +7 -5
  41. data/lib/graph_weaver/internal/headers.rb +19 -0
  42. data/lib/graph_weaver/internal/test_clients.rb +7 -11
  43. data/lib/graph_weaver/internal.rb +81 -13
  44. data/lib/graph_weaver/log_subscriber.rb +10 -2
  45. data/lib/graph_weaver/logging.rb +33 -13
  46. data/lib/graph_weaver/query_module.rb +44 -23
  47. data/lib/graph_weaver/retry.rb +12 -8
  48. data/lib/graph_weaver/rspec.rb +13 -24
  49. data/lib/graph_weaver/schema_loader.rb +52 -14
  50. data/lib/graph_weaver/tasks.rb +10 -2
  51. data/lib/graph_weaver/testing/cassette.rb +28 -5
  52. data/lib/graph_weaver/testing/endpoint.rb +14 -13
  53. data/lib/graph_weaver/testing/fake_client.rb +33 -3
  54. data/lib/graph_weaver/testing/router.rb +7 -3
  55. data/lib/graph_weaver/testing.rb +12 -4
  56. data/lib/graph_weaver/transport/http.rb +2 -2
  57. data/lib/graph_weaver/transport.rb +47 -23
  58. data/lib/graph_weaver/version.rb +1 -1
  59. data/lib/graph_weaver.rb +32 -10
  60. metadata +16 -3
  61. data/CHANGELOG.md +0 -3801
@@ -2,15 +2,10 @@
2
2
 
3
3
  The setup that ships, end to end: queries live as `.graphql` files, generation
4
4
  writes `# typed: strict` Ruby you check in, and CI fails when anything drifts.
5
- Follow it once when you add the gem to an app. (Exploring an API from a console
6
- instead? Start with [dynamic mode](real_world.md) no build step.)
7
-
8
- Rails is assumed below; the [non-Rails note](#not-rails) at the bottom
9
- covers the differences. Still deciding whether to adopt at all?
10
- [Alternatives](alternatives.md) compares the field, this gem included.
11
- No Sorbet in your app? None needed: `sorbet-runtime` comes with the gem and the
12
- generated code checks itself at runtime — running `srb tc` on top is your call,
13
- and [Sorbet, with or without](#sorbet-with-or-without) says what that buys.
5
+ Follow it once when you add the gem to an app. Rails is assumed;
6
+ [not Rails?](#not-rails) covers the differences. Exploring an API from a console
7
+ instead? Start with [dynamic mode](real_world.md) — no build step. No Sorbet in
8
+ your app? None needed [Sorbet, with or without](#sorbet-with-or-without).
14
9
 
15
10
  ## 1. Install
16
11
 
@@ -35,9 +30,8 @@ rails g graph_weaver:install https://api.example.com/graphql
35
30
  introspect app/graphql/schema.json from https://api.example.com/graphql
36
31
  ```
37
32
 
38
- The argument is whatever you'd pass to `GraphWeaver.new` — the generator
39
- takes the same three source forms the library does, and writes the
40
- initializer that fits:
33
+ The argument is whatever you'd pass to `GraphWeaver.new` — the same three source
34
+ forms the library takes and the generator writes the initializer that fits:
41
35
 
42
36
  | source | |
43
37
  |---|---|
@@ -47,43 +41,35 @@ initializer that fits:
47
41
 
48
42
  | flag | |
49
43
  |---|---|
50
- | `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`. Url only, and omitted entirely for a public API that needs no token. The name is recorded into the dump, so `schema:refresh`/`schema:diff`/`queries:check` read the same one the initializer does |
44
+ | `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`, url only, omitted for a public API. The name is recorded into the dump, so `schema:refresh`/`schema:diff`/`queries:check` read the same one the initializer does |
51
45
  | `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh` does it later (`URL=...` to name an endpoint the first time) |
52
46
 
53
- Re-running is safe every file it writes goes through the usual Rails
54
- conflict prompt, so an initializer you've edited is never overwritten
55
- silently. The schema dump is the one file that isn't Thor's to diff, so it is
56
- **kept** rather than prompted for replacing it would drop the source url it
57
- records, which `schema:diff`/`:refresh` read:
58
-
59
- ```
60
- keep app/graphql/schema.json — delete it and re-run to re-introspect
61
- ```
47
+ Re-running is safe: every file goes through the usual Rails conflict prompt, so
48
+ an initializer you've edited is never overwritten silently. The schema dump is
49
+ the exception it is **kept** rather than prompted for, since replacing it
50
+ would drop the source url it records; delete it and re-run to re-introspect.
62
51
 
63
52
  What it wrote:
64
53
 
65
54
  - **`config/initializers/graph_weaver.rb`.** `GraphWeaver.client =` is the
66
- load-bearing line: generated modules without a baked transport resolve to
55
+ load-bearing line: generated modules belonging to no declared graph resolve to
67
56
  it at execute time (the full
68
57
  [resolution order](transports.md#client-resolution)). Custom
69
- scalars/enums/type helpers register here too — the rake tasks bake them
70
- into generated source, so they have to run first ([scalars](scalars.md)):
58
+ scalars/enums/type helpers register here too — the rake tasks bake them into
59
+ generated source, so they have to run first ([scalars](scalars.md)):
71
60
 
72
61
  ```ruby
73
62
  GraphWeaver.register_scalar("Money", Money) # a scalar the registry can't know
74
63
  ```
75
64
 
76
- Every setting also takes the block form the file's neighbours use
77
- `GraphWeaver.configure { |config| … }`, where `config` is `GraphWeaver`
78
- itself, so the two spellings are one call.
65
+ Every setting also takes the block form `GraphWeaver.configure { |config| … }`,
66
+ where `config` is `GraphWeaver` itself, so the two spellings are one call.
79
67
 
80
- A registration that names one of your own constants — a `T::Enum` for
68
+ A registration that names one of your **own** constants — a `T::Enum` for
81
69
  `register_enum`, a mixin for [`extend_type`](generated_modules.md#type-helpers)
82
- — goes in a `to_prepare` block, the same place the in-process client goes and
83
- for the same reason:
84
- autoloading is set up after `config/initializers` run. Generation depends on
85
- `:environment`, which runs `to_prepare` too, so the registration is in place
86
- before it emits.
70
+ — goes in a `to_prepare` block instead, because autoloading is set up after
71
+ `config/initializers` run. Generation depends on `:environment`, which runs
72
+ `to_prepare` too, so the registration is still in place before it emits:
87
73
 
88
74
  ```ruby
89
75
  Rails.application.config.to_prepare do
@@ -91,42 +77,34 @@ What it wrote:
91
77
  GraphWeaver.extend_type("Pet", PetHelpers)
92
78
  end
93
79
  ```
94
-
95
- A custom `GraphQL::Schema::Validator` ([the `extensions.input`
96
- recipe](errors.md#what-your-server-can-send)) is installed by *symbol*, so
97
- nothing references its constant and Zeitwerk never autoloads it — name it in
98
- the same `to_prepare` block, above the schema, or the schema raises
99
- `unknown validation: :your_rule` on whichever file boots first.
100
-
101
80
  - **`app/graphql/schema.json`.** The schema dump codegen reads
102
81
  (`GraphWeaver.schema_path`) — never written by hand, always committed.
103
- `cache: true` in the initializer reuses it; delete the file to
104
- re-introspect. Prefer PR-reviewable diffs? `cache: :graphql` writes SDL
105
- instead; both generate identical code. (`cache:`/`ttl:` apply only to url
106
- clients — a schema source never introspects, so passing them raises.)
107
- - **`graphql.config.yml`.** Five lines of YAML that give VS Code and
108
- RubyMine schema autocomplete, hover docs, and validation as you type in
109
- `.graphql` files no JS project, no `npm install`. Details and the honest
110
- limits in [editors](editors.md).
82
+ `cache: true` in the initializer reuses it; delete the file to re-introspect.
83
+ Prefer PR-reviewable diffs? `cache: :graphql` writes SDL instead; both
84
+ generate identical code. (`cache:`/`ttl:` apply only to url clients — a schema
85
+ source never introspects, so passing them raises.) A dump records the url it
86
+ came from, so a second client at a second origin caches under
87
+ `app/graphql/schema-<digest>.json` rather than overwriting the first's; name
88
+ the file yourself (`cache: "app/graphql/billing.json"`) to say which is which.
89
+ - **`graphql.config.yml`.** Five lines of YAML that give VS Code and RubyMine
90
+ schema autocomplete, hover docs, and validation as you type in `.graphql`
91
+ files — no JS project, no `npm install`. See [editors](editors.md).
111
92
  - **`app/graphql/queries/`, `app/graphql/fragments/`, `app/graphql/generated/`.**
112
93
  Where you write queries, where shared fragments live, and where generation
113
94
  writes Ruby.
114
95
  - **`.rubocop.yml`**, if you have one. Generated code is machine-written and
115
- marked "do not edit," so the output directory is added to `AllCops: Exclude:`
116
- otherwise `Style/Documentation`, `Style/ClassAndModuleChildren` and
117
- `Metrics/*` fire on every generated file. An `AllCops:` you already have is
118
- left alone (a second one would replace it, not merge); the generator prints
119
- the line to add.
120
-
121
- Rake needs no wiring either: in Rails the `graph_weaver:*` tasks register
122
- themselves (a Railtie) and depend on `:environment`, so your initializer —
123
- and its registrations — runs first. The generated modules load at boot from
124
- a `to_prepare` block, so a helper or enum you registered in one is already
125
- in place when the file that names it loads.
126
-
127
- Everything above describes **one** schema, which is the usual case. An app
128
- with a second one declares each as a graph — [more than one
129
- schema](#more-than-one-schema), at the end; skip it until you have two.
96
+ marked "do not edit," so the output directory is added to `AllCops: Exclude:`.
97
+ An `AllCops:` you already have is left alone (a second one would replace it,
98
+ not merge); the generator prints the line to add.
99
+
100
+ Rake needs no wiring: in Rails the `graph_weaver:*` tasks register themselves and
101
+ depend on `:environment`, so your initializer runs first. The generated modules
102
+ load at boot from a `to_prepare` block, so a helper or enum you registered is
103
+ already in place when the file that names it loads.
104
+
105
+ All of that describes **one** schema, which is the usual case; an app with a
106
+ second one declares each as a graph ([more than one
107
+ schema](#more-than-one-schema)).
130
108
 
131
109
  ## 3. Write a query, generate, commit
132
110
 
@@ -157,19 +135,16 @@ where the schema calls the type `Country`. The corners are in
157
135
  [generated modules](generated_modules.md#naming).
158
136
 
159
137
  **Not sure what the API offers?** `app/graphql/schema.json` is the whole schema
160
- as plain JSON — types, fields, descriptions — already in your repo. And
161
- `graphql.config.yml` is there too, so VS Code and RubyMine validate the
162
- `.graphql` files as you type, with autocomplete and hover docs off that same
163
- dump — see [editors](editors.md).
138
+ as plain JSON — types, fields, descriptions — already in your repo, and
139
+ [your editor reads it](editors.md) as you type the query.
164
140
 
165
141
  **In development you don't type that command again.** While the server is
166
- running, a `.graphql` edit — or a refreshed schema dump — regenerates before
167
- the next request, the way a route or a locale change takes effect. A query that
168
- doesn't compile is logged with its file and position while the modules already
169
- loaded keep serving, so a file saved mid-edit doesn't take the server down.
170
- Development only, and `config.graph_weaver.watch = false` turns it off. The
171
- generated files are still what ships: commit them, and keep `rake
172
- graph_weaver:verify` in CI.
142
+ running, a `.graphql` edit — or a refreshed schema dump — regenerates before the
143
+ next request, the way a route change takes effect; a query that doesn't compile
144
+ is logged with its file and position while the modules already loaded keep
145
+ serving, so a file saved mid-edit doesn't take the server down. Development
146
+ only, and `config.graph_weaver.watch = false` turns it off. The generated files
147
+ are still what ships: commit them, and keep `rake graph_weaver:verify` in CI.
173
148
 
174
149
  ### Shared fragments
175
150
 
@@ -189,31 +164,16 @@ Fragment files hold only fragments (no operations), and names are unique across
189
164
  them. Point elsewhere with `GraphWeaver.fragments_paths` (an appendable list,
190
165
  default `app/graphql/fragments`).
191
166
 
192
- One payoff worth knowing about: when a shared fragment *is* the whole selection
193
- on a union field, its type is hoisted once into `GraphQLTypes` and every query
194
- that spreads it gets the same Ruby type — so one exhaustive `case … T.absurd`
195
- works everywhere. See
196
- [abstract types](generated_modules.md#abstract-types).
167
+ One payoff: when a shared fragment *is* the whole selection on a union field,
168
+ its type is hoisted once into `GraphQLTypes` and every query that spreads it
169
+ gets the same Ruby type — so one exhaustive `case … T.absurd` works everywhere.
170
+ See [abstract types](generated_modules.md#abstract-types).
197
171
 
198
172
  ## 4. Test against fakes
199
173
 
200
174
  The generator put `require "graph_weaver/rspec"` in your `spec/rails_helper.rb`
201
- — add it there yourself if rspec arrived after the install, since the tags
202
- below do nothing without it.
203
-
204
- ```ruby
205
- it "renders the empty state", graphql: :fake do … end # or tag the describe
206
- ```
207
-
208
- The tag installs a seeded, schema-correct `FakeClient` for that example — and
209
- **nothing leaves the process**: no server, no HTTP at all, so no webmock and
210
- no VCR. `rspec --seed 1234` reproduces the fake data along with test order.
211
- The schema it fabricates from is derived (the committed dump, or your
212
- client's), so there is nothing to configure. Tag `graphql: :in_process`
213
- instead and the same example runs against your real resolvers.
214
-
215
- A request spec is the usual shape: tag it, pin the value the assertion is
216
- about, and everything else in the selection is still fabricated.
175
+ — add it there yourself if rspec arrived after the install, since the tags below
176
+ do nothing without it.
217
177
 
218
178
  ```ruby
219
179
  # spec/requests/people_spec.rb
@@ -228,13 +188,17 @@ RSpec.describe "People", type: :request do
228
188
  end
229
189
  ```
230
190
 
231
- Pins, simulating failures, and the federated `graphql: :router` are in
191
+ The tag installs a seeded, schema-correct `FakeClient` for that example, and
192
+ **nothing leaves the process**: no server, no HTTP, so no webmock and no VCR.
193
+ `rspec --seed 1234` reproduces the fake data along with test order. Pin the value
194
+ the assertion is about and everything else in the selection is still fabricated.
195
+ Tag `graphql: :in_process` instead and the same example runs against your real
196
+ resolvers. Pins, simulating failures, and the federated `graphql: :router` are in
232
197
  [testing](testing.md).
233
198
 
234
- A fresh `rails g rspec:install` leaves the `spec/support` glob commented
235
- out in `spec/rails_helper.rb`, so uncomment it — or put the require in
236
- `rails_helper.rb` itself. Nothing warns you that a support file went
237
- unread.
199
+ A fresh `rails g rspec:install` leaves the `spec/support` glob commented out in
200
+ `spec/rails_helper.rb`, so uncomment it — or put the require in
201
+ `rails_helper.rb` itself. Nothing warns you that a support file went unread.
238
202
 
239
203
  ## 5. Verify in CI
240
204
 
@@ -248,22 +212,17 @@ Five questions, five tasks — the last only on a federated graph:
248
212
  | does the app still read what it selects? | `rake graph_weaver:unused` | no |
249
213
  | did a subgraph change without a recompose? | `rake graph_weaver:federation:diff` | no |
250
214
 
251
- (`rake graph_weaver:graphs` answers a sixth, when an app has more than one
252
- schema: which graphs are configured, where each generates, and what each
253
- registers the one place those registrations are listed, since a registration
254
- belongs to one graph.)
215
+ Every one of them exits non-zero on a finding, so the gate is a chain.
216
+ `verify` compares the committed generated files against what the current schema
217
+ + queries + registrations would produce, so it belongs in every CI build.
218
+ `schema:diff` asks whatever the dump came from — a recorded source url (with
219
+ `GRAPHWEAVER_AUTH` for private APIs), or your own schema class when the app
220
+ [serves the schema itself](#your-apps-own-schema-in-process) — and
221
+ `rake graph_weaver:schema:refresh` is the repair either way. On an app with more
222
+ than one schema, `rake graph_weaver:graphs` lists which graphs are configured,
223
+ where each generates, and what each registers.
255
224
 
256
- `verify` compares the committed generated files against what the current
257
- schema + queries + registrations would produce, so it belongs in every CI
258
- build. `schema:diff` asks whatever the dump came from — a recorded source url
259
- (with `GRAPHWEAVER_AUTH` for private APIs), or your own schema class when the
260
- app [serves the schema itself](#your-apps-own-schema-in-process) — and
261
- `rake graph_weaver:schema:refresh` is the repair either way.
262
- `federation:diff` needs no network, so it goes in the same PR run;
263
- see [federation](federation.md#has-the-supergraph-been-recomposed).
264
-
265
- Every one of them exits non-zero on a finding, so the gate is a chain. The two
266
- topologies differ only in what reaches a network:
225
+ The two topologies differ only in what reaches a network:
267
226
 
268
227
  ```sh
269
228
  # an API you don't own — the dump records the url it was introspected from
@@ -282,55 +241,20 @@ bundle exec rake graph_weaver:cassettes:check # do the recordings still cast?
282
241
  ```
283
242
 
284
243
  In-process the order is the repair order: refresh the dump, regenerate,
285
- re-record. As a GitHub Actions job:
286
-
287
- ```yaml
288
- # .github/workflows/graphql.yml
289
- name: graphql
290
- on: [push]
291
- jobs:
292
- graph_weaver:
293
- runs-on: ubuntu-latest
294
- steps:
295
- - uses: actions/checkout@v4
296
- - uses: ruby/setup-ruby@v1
297
- with:
298
- bundler-cache: true
299
- - run: bundle exec rake graph_weaver:verify
300
- - run: bundle exec rake graph_weaver:cassettes:check
301
- - run: bundle exec rake graph_weaver:queries:check
302
- - run: bundle exec rake graph_weaver:schema:diff
303
- env:
304
- GRAPHWEAVER_AUTH: ${{ secrets.GRAPHWEAVER_AUTH }}
305
- # federated apps only — needs no network, so it runs beside the rest
306
- - run: bundle exec rake graph_weaver:federation:diff
307
- ```
308
-
309
- An in-process schema needs no `env:` — it answers introspection itself.
244
+ re-record. In GitHub Actions each line is one `- run:` step; the remote chain
245
+ wants `GRAPHWEAVER_AUTH: ${{ secrets.GRAPHWEAVER_AUTH }}` in the `env:` of the
246
+ two steps that re-introspect, and an in-process schema needs no `env:` at all,
247
+ since it answers introspection itself. A federated app adds `rake
248
+ graph_weaver:federation:diff` to either chain — it needs no network.
310
249
 
311
250
  **On a federated graph, none of the five looks at the schema production is
312
- serving.** `verify`, `queries:check`, `unused` and `federation:diff` compare the
313
- app to artifacts checked in beside it, and `schema:diff` reads a live source
314
- only where the dump records one a composed supergraph doesn't, because no
315
- endpoint serves it. Hot-reload a router onto a supergraph that dropped a field
316
- your queries select and all five still exit 0 while every one of those requests
317
- fails; the first detector is a user's failed request, answered by the runtime
318
- [drift message](errors.md#stale-schemas). The pre-deploy check against the
319
- live graph is Apollo's, not this gem's — run it in the same job:
320
-
321
- ```sh
322
- rover subgraph check my-graph@prod --name products --schema products.graphql
323
- rover supergraph fetch my-graph@prod # then recompose and diff what you get back
324
- ```
325
-
326
- `rover subgraph check` asks GraphOS whether publishing this subgraph would break
327
- the composition or a client's registered operations; `rover supergraph fetch`
328
- hands you what the router is actually running, which is the artifact
329
- `federation:diff` should have been pointed at all along. See
251
+ serving**, because no endpoint serves a composed supergraph. The pre-deploy
252
+ check against the live graph is Apollo's, not this gem's run `rover subgraph
253
+ check` and `rover supergraph fetch` in the same job. See
330
254
  [federation → in CI](federation.md#in-ci).
331
255
 
332
- `schema:diff` names what moved, breaking changes first — breaking meaning
333
- a query written against your dump stops validating, or stops casting:
256
+ `schema:diff` names what moved, breaking changes first — breaking meaning a
257
+ query written against your dump stops validating, or stops casting:
334
258
 
335
259
  ```
336
260
  app/graphql/schema.json vs https://api.example.com/graphql: 8 changes, 5 breaking
@@ -350,21 +274,17 @@ other:
350
274
  app/graphql/schema.json is stale — the server's schema has drifted (rake graph_weaver:schema:refresh)
351
275
  ```
352
276
 
353
- Nullability is judged from your side, which is why the two above point
354
- opposite ways: `Person.pets` losing its `!` hands a generated struct the
355
- nil it declared it wouldn't get, while `AdoptionInput.nickname` gaining
356
- one rejects a query that omits it. Any drift exits non-zero — whether a
357
- change matters is yours to judge.
358
-
359
- `GraphWeaver::SchemaLoader.diff(path)` is the same summary as an object —
360
- `#breaking`, `#compatible`, `#to_h`, and `#empty?` for the plain "has it
361
- drifted" question.
277
+ Nullability is judged from your side, which is why the two above point opposite
278
+ ways: `Person.pets` losing its `!` hands a generated struct the nil it declared
279
+ it wouldn't get, while `AdoptionInput.nickname` gaining one rejects a query that
280
+ omits it. Any drift exits non-zero — whether a change matters is yours to judge.
281
+ `GraphWeaver::SchemaLoader.diff(path)` is the same summary as an object
282
+ (`#breaking`, `#compatible`, `#to_h`, `#empty?`).
362
283
 
363
- `queries:check` answers the question that actually matters when the schema
364
- *has* moved: **which of your queries no longer validate, and why.** It
365
- re-introspects the recorded url (without rewriting the dump) and validates
366
- every `.graphql` file against the schema as it is right now, naming each
367
- error's line and column, and exits non-zero:
284
+ `queries:check` answers the question that matters when the schema *has* moved:
285
+ **which of your queries no longer validate, and why.** It re-introspects the
286
+ recorded url (without rewriting the dump) and validates every `.graphql` file
287
+ against the schema as it is right now, naming each error's line and column:
368
288
 
369
289
  ```
370
290
  app/graphql/queries/person.graphql
@@ -373,90 +293,50 @@ app/graphql/queries/person.graphql
373
293
  1 invalid query
374
294
  ```
375
295
 
376
- The Ruby behind it returns the same thing as data, so you can wire it into
377
- whatever you already have (a spec, a Slack ping, an issue):
378
-
379
- ```ruby
380
- GraphWeaver.check_queries
381
- # => { "app/graphql/queries/person.graphql" =>
382
- # [{ "message" => "Field 'nmae' doesn't exist on type 'Person' (Did you mean `name`?)",
383
- # "line" => 4, "column" => 5 }] }
384
- ```
385
-
386
- Empty means everything validates. Pass `schema:` a *loaded* schema (not a path)
387
- and nothing touches the network — handy for checking a proposed subgraph before
388
- it's live:
389
-
390
- ```ruby
391
- GraphWeaver.check_queries(schema: GraphWeaver::SchemaLoader.load("proposed.graphql"))
392
- ```
393
-
394
- Left off, it re-introspects the url the dump records — and when that dump is a
395
- composed supergraph, each error also names the subgraphs behind the type it
396
- points at ([federation](federation.md#the-routing-table)).
296
+ `GraphWeaver.check_queries` returns the same findings as data a hash of file
297
+ to `message`/`line`/`column`, empty when everything validates so you can wire
298
+ it into a spec, a Slack ping, an issue. Pass it `schema:` a *loaded* schema (not
299
+ a path) and nothing touches the network, which is how you check your queries
300
+ against a proposed subgraph before it's live.
397
301
 
398
302
  ### The selections nothing reads
399
303
 
400
304
  `rake graph_weaver:unused` asks the one question the others can't: not "is the
401
305
  Ruby fresh" but "does the app still use what the query asks for". A field
402
306
  someone stopped rendering stays in the `.graphql` forever — the query keeps
403
- validating, the struct keeps casting, and the server keeps paying to resolve
404
- it. graphql-client catches that at runtime by masking the data a caller didn't
405
- declare; the structs are checked in here, so it can be recovered without
406
- running anything:
307
+ validating, the struct keeps casting, and the server keeps paying to resolve it.
407
308
 
408
309
  ```
409
310
  app/graphql/queries/products.graphql: Products.sku — selected, never read (Catalog::ProductsQuery::Result::Products#sku)
410
- app/graphql/queries/products.graphql: Products.blurb — selected, never read (Catalog::ProductsQuery::Result::Products#blurb)
411
311
 
412
312
  13 selections, 2 unread — 2 queries, 58 files swept under .
413
313
  ```
414
314
 
415
315
  Each line names the query file, the selection to go and delete, and the
416
- generated prop behind it. It reads the generated structs for the props a
417
- query produced, then sweeps your `.rb`, `.rake`, `.builder`, `.erb`, `.slim`,
418
- `.haml` and `.jbuilder` **once** for every name they could be read by —
419
- `.sku`, `sku:`, `:sku`, `"sku"`. `PATHS=app,lib` narrows the sweep, and a
420
- `PATHS=` naming a directory that isn't there is refused rather than swept as
421
- nothing. Everything under a directory named `generated`, plus `vendor`,
422
- `node_modules`, `tmp` and `log`, is skipped either way, as is any file
423
- defining a graphql-ruby **type** `< GraphQL::Schema::Object` or the
424
- `< Types::BaseObject` the generator writes since a `field :sku` there is
425
- your *server* offering a field, not this app reading one back. A `Resolver` or
426
- a `Mutation` is swept like any other code: that is where a BFF reads the graph
427
- it consumes. Nothing is edited and the exit is 0; `STRICT=1` exits 1 when
428
- anything is unread, for teams who want the gate.
429
-
430
- A line handing a query module straight to a serializer — `render json:`,
431
- `to_h`, `to_json`, `as_json`, `serialize`, `deconstruct_keys` — reads every
432
- prop at once, so that module is excused and the line is quoted, because
433
- matching a serializer by name is the softest thing here and a wrong excuse
434
- should be obvious:
435
-
436
- ```
437
- Accounts::MeQuery: every prop counted as read — handed whole to a serializer at app/controllers/accounts_controller.rb:3
438
- render json: Accounts::MeQuery.execute!.me
439
- ```
440
-
441
- A local counts too, which is what makes the ordinary two-line controller work
442
- — `result = Accounts::MeQuery.execute!` on one line, `render json: result.me`
443
- on the next — and the excuse names the local it followed.
316
+ generated prop behind it. It reads the generated structs for the props a query
317
+ produced, then sweeps your `.rb`, `.rake`, `.builder`, `.erb`, `.slim`, `.haml`
318
+ and `.jbuilder` for every name they could be read by — `.sku`, `sku:`, `:sku`,
319
+ `"sku"`. `PATHS=app,lib` narrows the sweep (a `PATHS=` naming a directory that
320
+ isn't there is refused rather than swept as nothing); anything under a directory
321
+ named `generated`, plus `vendor`, `node_modules`, `tmp` and `log`, is skipped,
322
+ as is any file defining a graphql-ruby **type** a `field :sku` there is your
323
+ *server* offering a field, not this app reading one back. A module handed whole
324
+ to a serializer (`render json:`, `to_h`, `as_json`, a local and all) counts every
325
+ prop as read, and the report quotes the line it followed. Nothing is edited and
326
+ the exit is 0; `STRICT=1` exits 1 when anything is unread.
444
327
 
445
328
  **It is a lint, not a proof**, and the task's own footer says so. It matches
446
- names as text, so a prop called `name` counts as read the moment anything at
447
- all says `.name`; and it can't see a prop reached by `public_send` or a read
448
- in a file type it doesn't sweep. The example above is the best case, not the
449
- typical one: measured against real corpora, **half to two thirds of genuinely
450
- unread selections go unreported**, the share rising with the size of the app,
451
- because common prop names collide with ordinary words somewhere in it.
452
- Silence is the safe direction here. Treat a finding as a prompt to go and
453
- look, and a clean run as nothing more than the absence of an obvious one —
454
- which is why it exits 0 unless you ask it not to.
329
+ names as text, so a prop called `name` counts as read the moment anything says
330
+ `.name`, and it can't see a prop reached by `public_send`. Measured against real
331
+ corpora, **half to two thirds of genuinely unread selections go unreported**,
332
+ the share rising with the size of the app. Silence is the safe direction: treat
333
+ a finding as a prompt to go and look, and a clean run as nothing more than the
334
+ absence of an obvious one.
455
335
 
456
336
  ## Your app's own schema, in-process
457
337
 
458
- An app that *serves* GraphQL with graphql-ruby can have the same typed
459
- access to its own API — same generated structs, no socket, no HTTP:
338
+ An app that *serves* GraphQL with graphql-ruby can have the same typed access to
339
+ its own API — same generated structs, no socket, no HTTP:
460
340
 
461
341
  ```sh
462
342
  rails g graph_weaver:install MyApp::Schema
@@ -470,13 +350,13 @@ Rails.application.config.to_prepare do
470
350
  end
471
351
  ```
472
352
 
473
- `to_prepare`, not a bare assignment: the schema class is autoloaded, so it
474
- isn't resolvable while initializers run, and a dev reload replaces it with
475
- a new class object that a captured one would go stale against.
353
+ `to_prepare`, not a bare assignment: the schema class is autoloaded, so it isn't
354
+ resolvable while initializers run, and a dev reload replaces it with a new class
355
+ object that a captured one would go stale against.
476
356
 
477
357
  **Context is per request, not per app.** A resolver reading
478
- `context[:current_user]` gets nil from the app default — build a client
479
- where you know the request and pass it per call:
358
+ `context[:current_user]` gets nil from the app default — build a client where
359
+ you know the request and pass it per call:
480
360
 
481
361
  ```ruby
482
362
  client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
@@ -484,14 +364,14 @@ PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
484
364
  ```
485
365
 
486
366
  Each query gets its own copy of that hash, so a resolver writing
487
- `context[:loader] =` can't hand what it wrote to the next request — which
488
- matters because one in-process client is normally the whole app's.
367
+ `context[:loader] =` can't hand what it wrote to the next request.
489
368
 
490
- **Keep the dump in step with the schema.** Codegen reads the committed
491
- dump at `GraphWeaver.schema_path`, never the live class — that's what
492
- makes `rake graph_weaver:verify` a deterministic CI check. The generator
493
- writes the first dump; after that it's an artifact derived from code in
494
- your own repo, and the same two tasks a remote schema uses keep it in step:
369
+ **Keep the dump in step with the schema.** Codegen reads the committed dump at
370
+ `GraphWeaver.schema_path`, never the live class — that's what makes `rake
371
+ graph_weaver:verify` a deterministic CI check, and why it **fails** when the
372
+ dump has fallen behind the class rather than calling the tree up to date. The
373
+ same two tasks a remote schema uses keep it in step, and neither touches a
374
+ network here:
495
375
 
496
376
  ```sh
497
377
  rake graph_weaver:schema:diff # what has the class changed since the dump?
@@ -499,26 +379,16 @@ rake graph_weaver:schema:refresh # rewrite the dump from the class
499
379
  rake graph_weaver:generate
500
380
  ```
501
381
 
502
- Neither touches a network here your schema class answers introspection
503
- itself and `refresh` rewrites the dump in whatever format it already is, so
504
- a repo that chose `cache: :graphql` keeps SDL. graphql-ruby's own
505
- `GraphQL::RakeTask` writes the same artifact, but you don't need to wire it
506
- up: `schema:refresh` writes to the path graph_weaver already reads, and is
507
- what `schema:diff` and a runtime "the schema may have changed" error both
508
- name.
509
-
510
- `verify` **fails** when the dump has fallen behind the class, rather than
511
- calling the tree up to date: generating from a dump that old would produce
512
- Ruby for a schema your resolvers have already left. `queries:check` is
513
- unaffected either way — running in-process it validates against the live
514
- class, not the dump — so the two can disagree about the same query while the
515
- dump is stale, and `schema:refresh` is what settles it.
382
+ `refresh` rewrites the dump in whatever format it already is, so a repo that
383
+ chose `cache: :graphql` keeps SDL. `queries:check` validates against the live
384
+ class rather than the dump, so the two can disagree about one query while the
385
+ dump is stale; `schema:refresh` settles it.
516
386
 
517
387
  **Scaffolding the app too?** On a `rails new --skip-active-record`,
518
388
  `rails g graphql:install` writes `config.active_record.query_log_tags` lines
519
- into `config/application.rb` that an app without ActiveRecord can't boot
520
- with — a graphql-ruby bug. Run it as
521
- `rails g graphql:install --skip-query-logs`, or delete the lines it wrote.
389
+ into `config/application.rb` that an app without ActiveRecord can't boot with —
390
+ a graphql-ruby bug. Run it as `rails g graphql:install --skip-query-logs`, or
391
+ delete the lines it wrote.
522
392
 
523
393
  ## A schema dump you already have
524
394
 
@@ -526,14 +396,14 @@ with — a graphql-ruby bug. Run it as
526
396
  rails g graph_weaver:install db/schema.graphql
527
397
  ```
528
398
 
529
- Sets `GraphWeaver.schema_path` to that file rather than writing a second
530
- copy, and introspects nothing. A dump has no resolvers, so it can't
531
- execute — set `GraphWeaver.client` to whatever serves the API.
399
+ Sets `GraphWeaver.schema_path` to that file rather than writing a second copy,
400
+ and introspects nothing. A dump has no resolvers, so it can't execute — set
401
+ `GraphWeaver.client` to whatever serves the API.
532
402
 
533
403
  ## More than one schema
534
404
 
535
- The five steps above describe one graph — a schema, its queries, its output.
536
- An app with a second schema declares each one:
405
+ The five steps above describe one graph — a schema, its queries, its output. An
406
+ app with a second schema declares each one:
537
407
 
538
408
  ```ruby
539
409
  # config/initializers/graph_weaver.rb
@@ -545,176 +415,89 @@ GraphWeaver.graph :billing do
545
415
  namespace "Billing"
546
416
  register_scalar "Money", BigDecimal
547
417
  end
548
-
549
- GraphWeaver.graph :github do
550
- schema "db/github.json"
551
- queries "app/graphql/github/queries"
552
- output "app/graphql/github/generated"
553
- client "GITHUB"
554
- namespace "GitHub"
555
- end
556
- ```
557
-
558
- **Declaring graph two means declaring graph one.** A declared graph *replaces*
559
- the implicit one the top-level settings describe, so the moment any graph is
560
- named, the `app/graphql/queries` and `app/graphql/generated` an existing app was
561
- already using belong to no graph — nothing generates or prunes them, and
562
- `generate` refuses the whole app rather than leave them there silently. Wrap
563
- them in a graph of their own, its directories and nothing else:
564
-
565
- ```ruby
566
- GraphWeaver.graph :app do
567
- queries "app/graphql/queries"
568
- output "app/graphql/generated"
569
- end
570
418
  ```
571
419
 
572
- No `schema`, so it keeps reading the one `schema_path` names; no `namespace`,
573
- so every constant keeps the name it has. Nothing about the existing queries or
574
- generated files changes.
575
-
576
420
  One `rake graph_weaver:generate` does the app, one `rake graph_weaver:verify`
577
- gates it, and `rake graph_weaver:graphs` lists what is configured. Everything a
578
- graph knows is said inside the block six settings, and the same three
579
- registrations you write at the top level. Each setting falls back to the
580
- matching top-level one, so a graph says only what differs, and anything else
581
- the block calls is refused naming the nine it takes.
421
+ gates it, and `rake graph_weaver:graphs` lists what is configured. Each setting
422
+ falls back to the matching top-level one, so a graph says only what differs, and
423
+ anything else the block calls is refused naming the nine it takes (these six,
424
+ plus `register_scalar`, `register_enum` and `extend_type`).
582
425
 
583
426
  | setting | takes |
584
427
  |---|---|
585
428
  | `schema` | a graphql-ruby schema class, a [`Client`](transports.md), a path to a dump, SDL, or a lambda returning one |
586
429
  | `queries` | a directory, or a list of them — the `.graphql` files this graph generates from |
587
430
  | `output` | one directory — where this graph's generated Ruby is written |
588
- | `client` | a constant, or its name what this graph's modules execute against |
431
+ | `client` | what this graph's modules execute against — a client, or the name of the constant holding one |
589
432
  | `namespace` | a constant, or its name — what every constant this graph generates nests under |
590
433
  | `types_module` | a constant name for the shared types module (default: `GraphQLTypes`, under `namespace`) |
591
434
 
592
- **`client` names a constant, not a url.** Its value is spelled into every module
593
- this graph generates and resolved the first time one of them executes so it
594
- has to be something generated source can write down, and a url is not. Build
595
- the client wherever you like and put the constant holding it here:
596
-
597
- ```ruby
598
- GITHUB = GraphWeaver.new("https://api.github.com/graphql", auth: ENV["GITHUB_TOKEN"])
599
- ```
600
-
601
- Resolving at first use rather than at declaration is what lets an initializer
602
- name `Billing::Schema` before Zeitwerk has loaded it, and what lets a dev reload
603
- swap the class object underneath. A graph with no `client` generates modules
604
- that fall back to `GraphWeaver.client`, the app default.
605
-
606
- `schema "x"` sets and a bare `schema` reads back. There is no `schema = "x"`
607
- form: the block is `instance_eval`'d, so that would be a local variable that
608
- silently does nothing — the same reason graphql-ruby writes `field :name`.
609
-
610
- ### Two remote APIs, and no schema of your own
611
-
612
- An app that is a pure client of someone else's GraphQL owns no schema class at
613
- all, so every graph's schema is a dump — and the dumps have to come from
614
- somewhere. Name the file you want and the client that can fetch it, and
615
- `schema:refresh` writes it:
435
+ **Declaring graph two means declaring graph one.** A declared graph *replaces*
436
+ the implicit one the top-level settings describe, so the moment any graph is
437
+ named, the `app/graphql/queries` and `app/graphql/generated` an existing app was
438
+ already using belong to no graph — and `generate` refuses the whole app rather
439
+ than leave them there silently. Wrap them in a graph of their own, its
440
+ directories and nothing else: no `schema`, so it keeps reading the one
441
+ `schema_path` names, and no `namespace`, so every constant keeps the name it has.
616
442
 
617
443
  ```ruby
618
- # config/initializers/graph_weaver.rb
619
- COUNTRIES = GraphWeaver.new("https://countries.trevorblades.com/")
620
- POKE = GraphWeaver.new("https://beta.pokeapi.co/graphql/v1beta")
621
-
622
- GraphWeaver.graph :countries do
623
- schema "app/graphql/countries/schema.json"
624
- queries "app/graphql/countries/queries"
625
- output "app/graphql/countries/generated"
626
- client "COUNTRIES"
627
- namespace "Countries"
628
- end
629
-
630
- GraphWeaver.graph :poke do
631
- schema "app/graphql/poke/schema.json"
632
- queries "app/graphql/poke/queries"
633
- output "app/graphql/poke/generated"
634
- client "POKE"
635
- namespace "Poke"
444
+ GraphWeaver.graph :app do
445
+ queries "app/graphql/queries"
446
+ output "app/graphql/generated"
636
447
  end
637
448
  ```
638
449
 
639
- ```sh
640
- rake graph_weaver:schema:refresh # introspects each graph's client into its schema
641
- rake graph_weaver:generate
642
- ```
643
-
644
- A graph whose dump isn't there yet is introspected from the url its own client
645
- posts to, and the dump records that url so every later `schema:refresh` and
646
- `schema:diff` re-reads the right server without being told again. `URL=` is for
647
- the app that has one dump and no graphs; it names a single endpoint, and here
648
- each graph has its own.
450
+ **`client` is where this graph's endpoint lives** — its modules say which graph
451
+ they belong to and nothing about transport, so they read it when they execute.
452
+ A graph with no `client` falls back to `GraphWeaver.client`, the app default.
453
+ Name the object (`client GraphWeaver.new(url, auth: …)`) or, when the constant
454
+ holding it is defined later than the graph block, its name (`client "GITHUB"`),
455
+ which is resolved on first use. `schema "x"` sets and a bare `schema` reads
456
+ back; there is no `schema = "x"` form, since the block is `instance_eval`'d and
457
+ that would be a local variable that silently does nothing.
458
+
459
+ **`namespace` nests everything that graph generates** `person.graphql` becomes
460
+ `Billing::PersonQuery` ([naming](generated_modules.md#naming)). Constants are
461
+ global, so two schemas that both have a `person.graphql` would otherwise fight
462
+ over one name; without a namespace the collision is refused at generation,
463
+ naming both files. **The block's registrations reach that graph alone**, laid
464
+ over the top-level ones — so `Money` is checked against the schema it was
465
+ registered for, and against no other.
466
+
467
+ An app that is a pure client of someone else's GraphQL owns no schema class, so
468
+ every graph's `schema` is a dump. Give each the file you want and a `client`
469
+ that can fetch it: `rake graph_weaver:schema:refresh` introspects each graph's
470
+ client into its own dump, recording the url so every later `schema:refresh` and
471
+ `schema:diff` re-reads the right server. (`URL=` is for the app that has one
472
+ dump and no graphs.)
649
473
 
650
474
  In specs, `graph:` is how an example says which graph a helper stands in for —
651
475
  `graphql_fake(graph: :poke, "pokemon_v2_pokemon.name" => "pikachu")`. See
652
- [testing.md](testing.md).
476
+ [testing](testing.md).
653
477
 
654
478
  **In Rails, declare graphs in the initializer itself, and name an autoloaded
655
- schema class with a lambda** — `schema -> { Billing::Schema }` as above.
479
+ schema class with a lambda** — `schema -> { Billing::Schema }`, as above.
656
480
  Zeitwerk is set up *after* `config/initializers` run, so a bare
657
481
  `Billing::Schema` there raises `uninitialized constant`; the lambda is resolved
658
- when generation asks, and resolved again after a dev reload has replaced the
659
- class object. (`client` and `namespace` take the constant or its name, since
660
- either way it is baked into generated source as a name.)
661
-
662
- **The block runs where you write it**, registrations included so a
663
- registration naming one of your own constants is in exactly the position a
664
- top-level one is, and has the same answer: declare that graph from a
665
- `to_prepare` block, as [above](#2-run-the-generator).
666
-
667
- ```ruby
668
- Rails.application.config.to_prepare do
669
- GraphWeaver.graph :billing do
670
- schema Billing::Schema
671
- queries "app/graphql/billing/queries"
672
- output "app/graphql/billing/generated"
673
- namespace "Billing"
674
- register_enum "Species", PetKind
675
- end
676
- end
677
- ```
678
-
679
- Re-running is safe — the name is the identity, so the second declaration
680
- replaces the first — and watch mode sees the graph either way.
681
-
682
- One constraint: `to_prepare` runs after Rails has set Zeitwerk up, and Zeitwerk
683
- reads its ignore list only then, so an `output` declared there can't be hidden
684
- from autoloading. Under the conventional `app/graphql/*/generated` it already
685
- is; anywhere else is refused at boot, naming the two fixes — declare the graph
686
- in `config/initializers` with `schema -> { Billing::Schema }`, or add the
687
- directory to `GraphWeaver.generated_paths` there.
688
-
689
- Two things are worth knowing:
690
-
691
- - **`namespace` nests everything that graph generates** — `person.graphql`
692
- becomes `Billing::PersonQuery`, and its shared types module becomes
693
- `Billing::GraphQLTypes`. Constants are global, so two schemas that both have a
694
- `person.graphql`, or that both hoist an enum, would otherwise fight over one
695
- name. Without a namespace the collision is refused at generation, naming both
696
- files.
697
- - **The block's registrations reach that graph alone**, laid over the top-level
698
- ones. That is what makes the build quiet: `Money` is checked against the
699
- schema it was registered for, and against no other. The top-level layer is
700
- read when generation asks, not when the graph is declared, so a
701
- `register_scalar` in another initializer reaches every graph whichever
702
- initializer Rails happened to run first.
703
-
704
- Declaring any graph replaces the implicit one the settings describe — an app
705
- either has graphs or has settings, never a silent third thing. The name is the
706
- identity, so re-declaring `:billing` replaces it rather than adding a second
707
- one; a `to_prepare` block that re-runs on every reload is safe.
482
+ when generation asks, and again after a dev reload has replaced the class
483
+ object.
484
+
485
+ A block **runs where you write it**, registrations included, so a graph whose
486
+ registrations name your own constants is declared from a `to_prepare` block for
487
+ the reason [above](#2-run-the-generator) safely, since the name is the
488
+ identity and the second declaration replaces the first. One constraint there:
489
+ Zeitwerk reads its ignore list before `to_prepare` runs, so an `output` declared
490
+ in one can't be hidden from autoloading. Under the conventional
491
+ `app/graphql/*/generated` it already is; anywhere else is refused at boot,
492
+ naming the two fixes.
708
493
 
709
494
  ## Sorbet, with or without
710
495
 
711
496
  `sorbet-runtime` is a hard dependency, so generated `T::Struct`s and sigs
712
- enforce at runtime in every app — no Sorbet setup required on your end.
713
- The *static* layer (`srb tc` flagging a typo'd field before anything
714
- runs) applies only when your app runs Sorbet, and only to checked-in
715
- generated files — dynamic `parse` is invisible to `srb tc`. Everything
716
- works without Sorbet; codegen plus Sorbet is what moves type errors from
717
- runtime to CI.
497
+ enforce at runtime in every app — no Sorbet setup required on your end. The
498
+ *static* layer (`srb tc` flagging a typo'd field before anything runs) applies
499
+ only when your app runs Sorbet, and only to checked-in generated files — dynamic
500
+ `parse` is invisible to `srb tc`.
718
501
 
719
502
  A misspelled field is caught either way — by `srb tc` before it runs, or by
720
503
  `NoMethodError` the first time it does. Nullability is the gap:
@@ -724,14 +507,14 @@ may be none of your dev data and plenty of production's.
724
507
 
725
508
  If your app globally injects `T::Sig` (`class Module; include T::Sig`), the
726
509
  per-struct `extend T::Sig` in generated files is redundant — rubocop's
727
- `Sorbet/RedundantExtendTSig` flags it. GraphWeaver auto-detects that at
728
- generation time and skips the `extend`; override with
729
- `GraphWeaver.extend_t_sig = true`/`false`.
510
+ `Sorbet/RedundantExtendTSig` flags it. GraphWeaver detects that at generation
511
+ time and skips the `extend`; override with `GraphWeaver.extend_t_sig =
512
+ true`/`false`.
730
513
 
731
514
  ## Not Rails?
732
515
 
733
- There's no generator, but what it writes is short — a few lines wherever
734
- your app boots, two directories, and the schema dump:
516
+ There's no generator, but what it writes is short — a few lines wherever your
517
+ app boots, two directories, and the schema dump:
735
518
 
736
519
  ```ruby
737
520
  GraphWeaver.client = GraphWeaver.new(
@@ -748,12 +531,12 @@ rake graph_weaver:schema:refresh URL=https://api.example.com/graphql
748
531
  ```
749
532
 
750
533
  Those paths are the Rails convention, not a requirement: put the files where
751
- your project already puts things and say so with `GraphWeaver.queries_paths`
752
- and `GraphWeaver.generated_paths`.
534
+ your project already puts things and say so with `GraphWeaver.queries_paths` and
535
+ `GraphWeaver.generated_paths`.
753
536
 
754
- Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks —
755
- and, since there's no `:environment` hook to run your registrations,
756
- require the file that does them from the Rakefile too.
537
+ Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks — and,
538
+ since there's no `:environment` hook to run your registrations, require the file
539
+ that does them from the Rakefile too.
757
540
 
758
541
  **Or skip rake too.** The tasks are a thin wrapper over public calls, so a
759
542
  script of your own does the same work — and `cache: true` writes the dump on
@@ -768,8 +551,8 @@ GraphWeaver.generate!(schema:) # => every file the plan produces
768
551
  GraphWeaver.changed_files # => only the ones whose bytes moved
769
552
  ```
770
553
 
771
- Pruning, the shared types module, and `verify_generated!` — the freshness
772
- guard `rake graph_weaver:verify` runs — are in
554
+ Pruning, the shared types module, and `verify_generated!` — the freshness guard
555
+ `rake graph_weaver:verify` runs — are in
773
556
  [generated modules](generated_modules.md#generating).
774
557
 
775
558
  `graphql.config.yml` is copy/paste from [editors](editors.md).