graph_weaver 0.4.4 → 0.5.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1357 -0
  3. data/CLAUDE.md +100 -8
  4. data/DECISIONS.md +309 -0
  5. data/Gemfile.lock +23 -23
  6. data/NOTES.md +5 -5
  7. data/PLAN.md +106 -135
  8. data/README.md +115 -96
  9. data/REVIEW.md +946 -0
  10. data/docs/cassettes.md +75 -48
  11. data/docs/editors.md +82 -0
  12. data/docs/errors.md +32 -30
  13. data/docs/federation.md +520 -48
  14. data/docs/generated_modules.md +352 -137
  15. data/docs/getting_started.md +237 -67
  16. data/docs/logging.md +35 -6
  17. data/docs/real_world.md +21 -15
  18. data/docs/scalars.md +49 -136
  19. data/docs/testing.md +299 -52
  20. data/docs/transports.md +129 -30
  21. data/docs/upgrading.md +112 -0
  22. data/graph_weaver.gemspec +3 -1
  23. data/lib/generators/graph_weaver/install_generator.rb +259 -0
  24. data/lib/graph_weaver/client.rb +114 -111
  25. data/lib/graph_weaver/codegen/aliases.rb +217 -0
  26. data/lib/graph_weaver/codegen/emit.rb +272 -251
  27. data/lib/graph_weaver/codegen/enum_type.rb +27 -98
  28. data/lib/graph_weaver/codegen/nodes.rb +72 -13
  29. data/lib/graph_weaver/codegen/scalar_type.rb +72 -67
  30. data/lib/graph_weaver/codegen/type_helpers.rb +142 -0
  31. data/lib/graph_weaver/codegen.rb +617 -264
  32. data/lib/graph_weaver/errors.rb +127 -10
  33. data/lib/graph_weaver/federation.rb +272 -0
  34. data/lib/graph_weaver/hints.rb +12 -6
  35. data/lib/graph_weaver/in_process.rb +90 -0
  36. data/lib/graph_weaver/input_struct.rb +21 -2
  37. data/lib/graph_weaver/logging.rb +29 -0
  38. data/lib/graph_weaver/parsing.rb +67 -0
  39. data/lib/graph_weaver/query_module.rb +55 -0
  40. data/lib/graph_weaver/railtie.rb +23 -1
  41. data/lib/graph_weaver/representation.rb +74 -0
  42. data/lib/graph_weaver/response.rb +15 -1
  43. data/lib/graph_weaver/retry.rb +29 -8
  44. data/lib/graph_weaver/rspec.rb +214 -16
  45. data/lib/graph_weaver/schema_loader.rb +820 -57
  46. data/lib/graph_weaver/schemas.rb +46 -0
  47. data/lib/graph_weaver/selection.rb +59 -7
  48. data/lib/graph_weaver/tasks.rb +216 -21
  49. data/lib/graph_weaver/testing/cassette.rb +186 -62
  50. data/lib/graph_weaver/testing/coverage.rb +165 -0
  51. data/lib/graph_weaver/testing/failure.rb +10 -23
  52. data/lib/graph_weaver/testing/fake_client.rb +194 -28
  53. data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
  54. data/lib/graph_weaver/testing/router.rb +1431 -0
  55. data/lib/graph_weaver/testing/subgraphs.rb +130 -0
  56. data/lib/graph_weaver/testing.rb +204 -14
  57. data/lib/graph_weaver/transport/faraday.rb +31 -6
  58. data/lib/graph_weaver/transport/http.rb +99 -36
  59. data/lib/graph_weaver/transport.rb +74 -18
  60. data/lib/graph_weaver/version.rb +1 -1
  61. data/lib/graph_weaver.rb +398 -170
  62. metadata +20 -3
@@ -2,11 +2,11 @@
2
2
 
3
3
  The setup that ships: queries live as `.graphql` files, generation writes
4
4
  `# typed: strict` Ruby you check in, and CI fails when anything drifts.
5
- Mostly copy/paste. (Exploring an API from a console instead? Start with
6
- [dynamic mode](real_world.md) — no build step.)
5
+ In Rails one generator does the setup. (Exploring an API from a console
6
+ instead? Start with [dynamic mode](real_world.md) — no build step.)
7
7
 
8
8
  Rails is assumed below; the [non-Rails note](#not-rails) at the bottom
9
- covers the one difference.
9
+ covers the differences.
10
10
 
11
11
  ## 1. Install
12
12
 
@@ -15,55 +15,150 @@ covers the one difference.
15
15
  gem "graph_weaver"
16
16
  ```
17
17
 
18
- ## 2. Bootstrap the schema dump
18
+ ## 2. Run the generator
19
19
 
20
- Codegen reads a schema dump at `app/graphql/schema.json`
21
- (`GraphWeaver.schema_path`). You never write this file by hand —
22
- `cache: true` writes it on first introspection. Bootstrap once from a
23
- console:
20
+ ```sh
21
+ rails g graph_weaver:install https://api.example.com/graphql
22
+ ```
23
+
24
+ ```
25
+ create config/initializers/graph_weaver.rb
26
+ create app/graphql/queries/.keep
27
+ create app/graphql/generated/.keep
28
+ create graphql.config.yml
29
+ introspect app/graphql/schema.json from https://api.example.com/graphql
30
+ ```
31
+
32
+ The argument is whatever you'd pass to `GraphWeaver.new` — the generator
33
+ takes the same three source forms the library does, and writes the
34
+ initializer that fits:
35
+
36
+ | source | |
37
+ |---|---|
38
+ | `https://api.example.com/graphql` | an endpoint: introspected now, and the dump committed |
39
+ | `MyApp::Schema` | your own graphql-ruby schema, executing [in-process](#your-apps-own-schema-in-process) |
40
+ | `db/schema.graphql` | a [dump you already have](#a-schema-dump-you-already-have) — pointed at, not copied |
41
+
42
+ | flag | |
43
+ |---|---|
44
+ | `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`. Url only. Name a different one and the initializer follows, but `schema:refresh`/`schema:diff` still read `GRAPHWEAVER_AUTH` — set both |
45
+ | `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh URL=...` does it later |
46
+
47
+ Re-running is safe — every file goes through the usual Rails conflict
48
+ prompt, so an initializer you've edited is never overwritten silently.
49
+
50
+ What it wrote:
51
+
52
+ - **`config/initializers/graph_weaver.rb`.** `GraphWeaver.client =` is the
53
+ load-bearing line: generated modules without a baked transport resolve to
54
+ it at execute time (the full
55
+ [resolution order](transports.md#client-resolution)). Custom
56
+ scalars/enums/type helpers register here too — the rake tasks bake them
57
+ into generated source, so they have to run first:
58
+
59
+ ```ruby
60
+ GraphWeaver.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
61
+ ```
62
+
63
+ A registration that names one of your own constants — a `T::Enum` for
64
+ `register_enum`, a mixin for `extend_type` — goes in a `to_prepare` block,
65
+ the same place the in-process client goes and for the same reason:
66
+ autoloading is set up after `config/initializers` run. Generation depends on
67
+ `:environment`, which runs `to_prepare` too, so the registration is in place
68
+ before it emits.
69
+
70
+ ```ruby
71
+ Rails.application.config.to_prepare do
72
+ GraphWeaver.register_enum("Species", PetKind, fallback: PetKind::Unknown)
73
+ GraphWeaver.extend_type("Pet", PetHelpers)
74
+ end
75
+ ```
76
+
77
+ - **`app/graphql/schema.json`.** The schema dump codegen reads
78
+ (`GraphWeaver.schema_path`) — never written by hand, always committed.
79
+ `cache: true` in the initializer reuses it; delete the file to
80
+ re-introspect. Prefer PR-reviewable diffs? `cache: :graphql` writes SDL
81
+ instead; both generate identical code. (`cache:`/`ttl:` apply only to url
82
+ clients — a schema source never introspects, so passing them raises.)
83
+ - **`graphql.config.yml`.** Five lines of YAML that give VS Code and
84
+ RubyMine schema autocomplete, hover docs, and validation as you type in
85
+ `.graphql` files — no JS project, no `npm install`. Details and the honest
86
+ limits in [editors](editors.md).
87
+ - **`app/graphql/queries/`, `app/graphql/generated/`.** Where you write
88
+ queries and where generation writes Ruby.
89
+
90
+ Rake needs no wiring either: in Rails the `graph_weaver:*` tasks register
91
+ themselves (a Railtie) and depend on `:environment`, so your initializer —
92
+ and its registrations — runs first. The generated modules load at boot the
93
+ same way, after `config/initializers`.
94
+
95
+ ### Your app's own schema, in-process
96
+
97
+ An app that *serves* GraphQL with graphql-ruby can have the same typed
98
+ access to its own API — same generated structs, no socket, no HTTP:
99
+
100
+ ```sh
101
+ rails g graph_weaver:install MyApp::Schema
102
+ ```
24
103
 
25
104
  ```ruby
26
- GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"], cache: true).schema
105
+ # config/initializers/graph_weaver.rb
106
+ Rails.application.config.to_prepare do
107
+ # queries run in-process against the app's own schema — no socket
108
+ GraphWeaver.client = GraphWeaver.new(MyApp::Schema)
109
+ end
27
110
  ```
28
111
 
29
- Skip this step and the generate task tells you exactly that — the error
30
- message is the documentation. Prefer PR-reviewable diffs? `cache: :graphql`
31
- writes SDL instead of introspection JSON; both generate identical code.
112
+ `to_prepare`, not a bare assignment: the schema class is autoloaded, so it
113
+ isn't resolvable while initializers run, and a dev reload replaces it with
114
+ a new class object that a captured one would go stale against.
32
115
 
33
- Note `cache:`/`ttl:` apply only to url clients a schema source (a live
34
- class or a dump) never introspects, so passing them raises.
116
+ **Context is per request, not per app.** A resolver reading
117
+ `context[:current_user]` gets nil from the app default build a client
118
+ where you know the request and pass it per call:
35
119
 
36
- ## 3. Wire the client
120
+ ```ruby
121
+ client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
122
+ PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
123
+ ```
124
+
125
+ **Keep the dump in step with the schema.** Codegen reads the committed
126
+ dump at `GraphWeaver.schema_path`, never the live class — that's what
127
+ makes `rake graph_weaver:verify` a deterministic CI check. The generator
128
+ writes the first dump; after that it's an artifact derived from code in
129
+ your own repo, so rebuild it with graphql-ruby's own rake task:
37
130
 
38
131
  ```ruby
39
- # config/initializers/graph_weaver.rb
40
- GraphWeaver.client = GraphWeaver.new(
41
- "https://api.example.com/graphql",
42
- auth: ENV["API_TOKEN"],
43
- cache: true, # reuses the committed dump; delete the file to re-introspect
44
- )
132
+ # lib/tasks/graphql.rake
133
+ require "graphql/rake_task"
134
+ GraphQL::RakeTask.new(schema_name: "MyApp::Schema", directory: "app/graphql",
135
+ dependencies: [:environment])
136
+ ```
45
137
 
46
- # custom scalars/enums/type helpers — register globally, so the rake
47
- # tasks bake them into generated source
48
- GraphWeaver.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
138
+ ```sh
139
+ rake graphql:schema:json # rewrites app/graphql/schema.json
140
+ rake graph_weaver:generate
49
141
  ```
50
142
 
51
- `GraphWeaver.client =` is the load-bearing line: generated modules
52
- without a baked transport resolve to it at execute time (the full
53
- [resolution order](transports.md#client-resolution)). The generated
54
- modules themselves load at boot automatically (the Railtie requires
55
- everything under `generated_path`, after your initializers run)
56
- outside Rails, call `GraphWeaver.load_generated!` wherever your app
57
- boots.
143
+ Run the dump step ahead of `rake graph_weaver:verify` in CI — that check
144
+ compares committed Ruby against the committed dump, so a stale dump makes
145
+ it fail on a query that is fine. `rake graph_weaver:queries:check` is
146
+ unaffected: when `GraphWeaver.client` runs in-process it validates
147
+ against the live class, not the dump. (`graph_weaver:schema:diff` and
148
+ `:refresh` are for servers you *don't* own; a dump taken from a schema
149
+ class records no url, and they say so.)
58
150
 
59
- ## 4. Rake tasks nothing to do
151
+ ### A schema dump you already have
60
152
 
61
- In Rails the `graph_weaver:*` tasks register themselves (a Railtie), and
62
- they depend on `:environment`, so your initializer — and its
63
- registrations, which are baked into generated source — runs first.
64
- Outside Rails, add `require "graph_weaver/tasks"` to your Rakefile.
153
+ ```sh
154
+ rails g graph_weaver:install db/schema.graphql
155
+ ```
65
156
 
66
- ## 5. Write a query, generate, commit
157
+ Sets `GraphWeaver.schema_path` to that file rather than writing a second
158
+ copy, and introspects nothing. A dump has no resolvers, so it can't
159
+ execute — set `GraphWeaver.client` to whatever serves the API.
160
+
161
+ ## 3. Write a query, generate, commit
67
162
 
68
163
  ```graphql
69
164
  # app/graphql/queries/person.graphql
@@ -86,6 +181,12 @@ reviewed like any other code — and never edited by hand.
86
181
  PersonQuery.execute!(id: "1").person&.name # typed, via GraphWeaver.client
87
182
  ```
88
183
 
184
+ ### Autocomplete while you write the query
185
+
186
+ `graphql.config.yml` is already there, so VS Code and RubyMine validate the
187
+ `.graphql` files as you type, with schema autocomplete and hover docs — see
188
+ [editors](editors.md).
189
+
89
190
  ### Shared fragments
90
191
 
91
192
  Define reusable fragments once and spread them from any query:
@@ -107,8 +208,8 @@ default `app/graphql/fragments`).
107
208
  ### Shared unions
108
209
 
109
210
  When a shared fragment *is* the whole selection on a union field, its type is
110
- hoisted once into a `GraphQLUnions` module and every query that spreads it
111
- aliases the same type — so a `union` selected across many queries becomes one
211
+ hoisted once into the shared `GraphQLTypes` module and every query that spreads
212
+ it aliases the same type — so a `union` selected across many queries becomes one
112
213
  Ruby type family, and you write one exhaustive `case … when … T.absurd` that
113
214
  works everywhere:
114
215
 
@@ -124,43 +225,96 @@ fragment FeedItemFields on FeedItem {
124
225
  query { feed { ...FeedItemFields } } # feed : T::Array[FeedItemFields::Type]
125
226
  ```
126
227
 
127
- Hoisting is what the shared fragment buys you — there's no flag. It triggers
128
- only when the union field's selection is exactly that one spread (mix in other
129
- fields, or shadow the fragment with a query-local one of the same name, and the
130
- union stays inlined in that query). Named like the inputs module from the output
131
- path (`GraphQLUnions`, or `GithubUnions` in a multi-schema layout); override
132
- with `GraphWeaver.unions_module=`.
228
+ There's no flag: hoisting triggers when the union field's selection is exactly
229
+ that one spread. Mix in other fields, or shadow the fragment with a query-local
230
+ one of the same name, and the union stays inlined in that query — see
231
+ [abstract types](generated_modules.md#abstract-types).
133
232
 
134
- ## 6. Test against fakes
233
+ ## 4. Test against fakes
135
234
 
136
235
  ```ruby
137
236
  # spec/support/graph_weaver.rb
138
237
  require "graph_weaver/rspec"
238
+ ```
139
239
 
140
- GraphWeaver::Testing.configure { |config| config.auto_fake = true }
240
+ ```ruby
241
+ it "renders the empty state", graphql: :fake do … end # or tag the describe
141
242
  ```
142
243
 
143
- The opt-in is deliberate (no surprise fakes); once on, the schema
144
- auto-locates from the committed dump and every query in every example
145
- executes against a seeded, schema-correct `FakeClient` no server, no
146
- stubs, and `rspec --seed 1234` reproduces the fake data along with test
147
- order. Pin values with `overrides:`, simulate failures with `Failure.*`
244
+ A fresh `rails g rspec:install` leaves the `spec/support` glob commented
245
+ out in `spec/rails_helper.rb`, so uncomment it or put the require in
246
+ `rails_helper.rb` itself. Nothing warns you that a support file went
247
+ unread.
248
+
249
+ The tag installs a seeded, schema-correct `FakeClient` for that example —
250
+ no server, no stubs, and `rspec --seed 1234` reproduces the fake data along
251
+ with test order. The schema it fabricates from is derived (the committed
252
+ dump, or your client's), so there's nothing to configure. Tag
253
+ `graphql: :in_process` instead and the same example runs against your real
254
+ resolvers; pin values with `overrides:`, simulate failures with `Failure.*`
148
255
  — see [testing](testing.md).
149
256
 
150
- ## 7. Verify in CI
257
+ ## 5. Verify in CI
151
258
 
152
259
  ```sh
153
- rake graph_weaver:verify # generated code fresh? fails on any drift
154
- rake graph_weaver:schema:verify # server drifted? re-introspects and compares
260
+ rake graph_weaver:verify # generated code fresh? fails on any drift
261
+ rake graph_weaver:schema:diff # server drifted? re-introspects and compares
262
+ rake graph_weaver:queries:check # did that drift break any of your queries?
155
263
  ```
156
264
 
157
- Two different questions. `graph_weaver:verify` checks that the committed
158
- generated files match what the current schema + queries + registrations
159
- would produce run it in every CI build. `graph_weaver:schema:verify`
160
- asks whether the *server* has moved since the dump was taken — it needs
161
- network, a dump with a recorded source url (introspected dumps have one),
162
- and `GRAPHWEAVER_AUTH` for private APIs; run it on a schedule and refresh
163
- with `rake graph_weaver:schema:refresh`.
265
+ Three different questions four on a federated graph, where
266
+ `rake graph_weaver:federation:diff` asks whether anyone changed a subgraph
267
+ without recomposing the supergraph you committed. It needs no network
268
+ either, so it belongs in the same PR run; see
269
+ [federation](federation.md#has-the-supergraph-been-recomposed).
270
+
271
+ `graph_weaver:verify` compares the committed generated files against what the
272
+ current schema + queries + registrations would produce. No network — run it in
273
+ every CI build.
274
+
275
+ `graph_weaver:schema:diff` asks whether the *server* has moved since the
276
+ dump was taken. It needs network, a dump with a recorded source url
277
+ (introspected dumps have one), and `GRAPHWEAVER_AUTH` for private APIs;
278
+ run it on a schedule and refresh with `rake graph_weaver:schema:refresh`.
279
+
280
+ `graph_weaver:queries:check` answers the question that actually matters
281
+ when it *has* moved: **which of your queries no longer validate, and
282
+ why.** It re-introspects the recorded url (without rewriting the dump) and
283
+ validates every `.graphql` file against the schema as it is right now,
284
+ naming each error's line and column:
285
+
286
+ ```
287
+ app/graphql/queries/person.graphql
288
+ 4:5 Field 'nmae' doesn't exist on type 'Person' (Did you mean `name`?)
289
+
290
+ 1 invalid query
291
+ ```
292
+
293
+ It exits non-zero when anything fails, so it drops straight into CI or a
294
+ scheduled job.
295
+
296
+ The Ruby behind it returns the same thing as data, so you can wire it into
297
+ whatever you already have (a spec, a Slack ping, an issue):
298
+
299
+ ```ruby
300
+ GraphWeaver.check_queries
301
+ # => { "app/graphql/queries/person.graphql" =>
302
+ # [{ "message" => "Field 'nmae' doesn't exist on type 'Person' (Did you mean `name`?)",
303
+ # "line" => 4, "column" => 5 }] }
304
+ ```
305
+
306
+ Empty means everything validates. Pass `schema:` a loaded schema and nothing
307
+ touches the network — handy for checking a *proposed* schema (a subgraph about
308
+ to ship) before it's live:
309
+
310
+ ```ruby
311
+ GraphWeaver.check_queries(schema: GraphWeaver::SchemaLoader.load("proposed.graphql"))
312
+ ```
313
+
314
+ It wants the loaded schema, not the path. Left off, it re-introspects the url
315
+ the dump records — and when that dump is a composed supergraph, each error also
316
+ names the subgraphs behind the type it points at
317
+ ([federation](federation.md#the-routing-table)).
164
318
 
165
319
  ## Sorbet, with or without
166
320
 
@@ -181,8 +335,24 @@ and marked "do not edit," so excluding `generated/**` from rubocop is also fine.
181
335
 
182
336
  ## Not Rails?
183
337
 
184
- Everything above works the same, minus the Railtie conveniences: add
185
- `require "graph_weaver/tasks"` to your Rakefile yourself, and since
186
- there's no `:environment` hook to run your registrations — require the
187
- file that does them from the Rakefile too. `GraphWeaver.load_generated!`
188
- goes wherever your app boots instead of an initializer.
338
+ There's no generator, but what it writes is short — a few lines wherever
339
+ your app boots, two directories, and the schema dump:
340
+
341
+ ```ruby
342
+ GraphWeaver.client = GraphWeaver.new(
343
+ "https://api.example.com/graphql",
344
+ auth: ENV["GRAPHWEAVER_AUTH"],
345
+ cache: true,
346
+ )
347
+ GraphWeaver.load_generated! # no Railtie to require the generated files
348
+ ```
349
+
350
+ ```sh
351
+ mkdir -p app/graphql/queries app/graphql/generated
352
+ rake graph_weaver:schema:refresh URL=https://api.example.com/graphql
353
+ ```
354
+
355
+ Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks —
356
+ and, since there's no `:environment` hook to run your registrations,
357
+ require the file that does them from the Rakefile too. `graphql.config.yml`
358
+ is copy/paste from [editors](editors.md).
data/docs/logging.md CHANGED
@@ -14,7 +14,7 @@ What logs at which level — pick the level, get the story:
14
14
  | Level | What you see |
15
15
  |-------|--------------|
16
16
  | `debug` | the wire: query + variables per call (long queries truncated), response status/bytes, request timing, connection open/drop, dynamically parsed modules |
17
- | `info` | schema introspection (with timing) and cache hits/misses, generated files written, query modules loaded |
17
+ | `info` | schema introspection (with timing) and cache hits/misses, generated files written and any unregistered scalars, query modules loaded |
18
18
  | `warn` | every GraphWeaver error raised — `TransportError`, `ServerError`, `QueryError`, `ValidationError`, `TypeError` |
19
19
 
20
20
  Every line carries `graph_weaver` as the progname, so formatter-based
@@ -22,12 +22,41 @@ filtering works out of the box. Wire lines are tagged
22
22
  `[req 3 FilteredPokemon]` — a per-process request id plus the operation
23
23
  name — so a request's lines stay paired when threads interleave.
24
24
 
25
- Debugging a misbehaving integration is the intended use: crank to
26
- `Logger::DEBUG` and you'll see exactly what went on the wire, what came
27
- back, whether the schema came from cache or a live introspection, and
28
- which connection served it.
29
-
30
25
  **PII note**: queries, variables, and response sizes appear at debug
31
26
  only — variables can carry user data, so keep production loggers at
32
27
  info or above (or scrub in your formatter). Auth headers never log at
33
28
  any level.
29
+
30
+ ## Instrumentation
31
+
32
+ A logger tells a human what happened; an APM needs to time it and count
33
+ it. `GraphWeaver.instrumenter` is one callable wrapping every request —
34
+ over the wire *and* in-process, one seam for both paths. It's a no-op
35
+ until you set one, and `ActiveSupport::Notifications` is a two-line
36
+ adapter:
37
+
38
+ ```ruby
39
+ GraphWeaver.instrumenter = lambda do |event, payload, &block|
40
+ ActiveSupport::Notifications.instrument(event, payload, &block)
41
+ end
42
+
43
+ ActiveSupport::Notifications.subscribe(GraphWeaver::EXECUTE_EVENT) do |*, payload|
44
+ StatsD.timing("graphql.#{payload[:operation] || "anonymous"}", ...)
45
+ end
46
+ ```
47
+
48
+ The one event is `GraphWeaver::EXECUTE_EVENT`
49
+ (`"graph_weaver.execute"`), a single request from start to parsed
50
+ response. Its payload carries:
51
+
52
+ | Key | |
53
+ |-----|--|
54
+ | `:url` | the endpoint — nil in-process |
55
+ | `:schema` | the schema class, in-process only |
56
+ | `:operation` | the operation name sent with the request (a generated module always has one) — what a trace keys on |
57
+ | `:status` | the HTTP status, added once the response lands |
58
+
59
+ Your callable **must** call the block and return its value. A failure
60
+ propagates through it, so the hook sees the exception and can record it.
61
+ The query text and the variables are deliberately absent: they carry
62
+ PII, and belong at debug on the logger where the level gates them.
data/docs/real_world.md CHANGED
@@ -6,24 +6,23 @@ ships is the checked-in codegen path in the [getting started](getting_started.md
6
6
  this page is how you get there (the `parse` below becomes a `.graphql`
7
7
  file plus `rake graph_weaver:generate`, everything else stays).
8
8
 
9
- Everything hangs off a client — transport, schema, and scalars for one
10
- server. GitHub's API, end to end:
9
+ Everything hangs off a client — transport and schema for one server. GitHub's
10
+ API, end to end:
11
11
 
12
12
  ```ruby
13
13
  require "graph_weaver"
14
14
 
15
- # transport + auth in one object (see docs/transports.md for retries and
15
+ # transport + auth in one object (docs/transports.md for retries and
16
16
  # advanced setup). cache: true dumps the schema at GraphWeaver.schema_path
17
- # on first introspection — the same file rake graph_weaver:generate reads
18
- # and any fresh dump already present is reused regardless of format. The
19
- # extension picks the format: .json verbatim, .graphql SDL (reviewable
20
- # diffs) — or say cache: :graphql. Introspected dumps record their source
21
- # url in a header, so a stale dump says where it came from.
17
+ # on first introspection — the same file rake graph_weaver:generate reads
18
+ # (docs/getting_started.md for the formats), with the source url recorded
19
+ # in a header, so a stale dump says where it came from.
22
20
  github = GraphWeaver.new("https://api.github.com/graphql", auth: `gh auth token`.strip, cache: true)
23
21
 
24
- # map GitHub's DateTime scalar onto Time (cast inferred from Time.parse)
25
- # scoped to this client; GraphWeaver.register_scalar sets the global default
26
- github.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
22
+ # map GitHub's DateTime scalar onto Time (cast inferred from Time.parse).
23
+ # Registrations are global and codegen-time, so this line types your
24
+ # console and your checked-in code identically (docs/scalars.md).
25
+ GraphWeaver.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
27
26
 
28
27
  RepoQuery = github.parse(<<~GRAPHQL)
29
28
  query($owner: String!, $name: String!) {
@@ -41,10 +40,17 @@ repo&.created_at # => 2026-07-07 ... (a real Time)
41
40
  repo&.stargazer_count # => Integer
42
41
  ```
43
42
 
44
- Clients are independent — build one per server, each with its own
45
- transport, schema, and scalar mappings. The introspection step (seconds
46
- on a big API) happens lazily on first `schema`/`parse` and caches per
47
- `cache:`/`ttl:`; for finer control the pieces are all public
43
+ Clients are independent — build one per server, each with its own transport
44
+ and schema. When you're ready to check the generated code in, the same client
45
+ is the schema `generate!` wants:
46
+
47
+ ```ruby
48
+ GraphWeaver.generate!(schema: github) # no dump on disk needed
49
+ ```
50
+
51
+ The introspection step (seconds on a big API) happens lazily on first
52
+ `schema`/`parse` and caches per `cache:`/`ttl:`; for finer control the
53
+ pieces are all public
48
54
  (`GraphWeaver::SchemaLoader.introspect(transport, cache:, ttl:)`, or cache
49
55
  `introspect(transport).to_json` in Rails.cache and `SchemaLoader.load` it).
50
56