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
data/CLAUDE.md CHANGED
@@ -8,6 +8,50 @@ A typed GraphQL client for Ruby: generates `# typed: strict` Ruby (nested
8
8
  `T::Struct`s + a typed `execute`) from your queries, checked against the schema
9
9
  at generation time. Sorbet is core to the product.
10
10
 
11
+ ## Design principle — correct and simple, in that order
12
+
13
+ Adoption follows delight, and delight follows from a tool that solves the real
14
+ problem without making you think. So the bar for any change is: is it correct,
15
+ and is it the *simplest* thing that is correct? Complexity is not neutral — it
16
+ is confusion, bugs, and frustration, paid for by every future reader and user.
17
+
18
+ What this means when choosing between designs:
19
+
20
+ - **One rule beats a rule with exceptions.** A behavior you can state in a
21
+ sentence, and predict without reading the source, is worth more than one that
22
+ is marginally more capable. Generated class names come from the response key —
23
+ full stop — rather than from the type name with disambiguation-on-collision,
24
+ because the second rule can't be stated without describing its own edge cases.
25
+ - **Prefer removing a decision to adding a knob.** The transport default got
26
+ *better* by deleting auto-detection: one less thing to know, one less way to be
27
+ surprised, and the fast path became the default. Reach for a config option only
28
+ after the simple default has actually failed someone.
29
+ - **A convention can beat a capability.** One file holds one operation. That's a
30
+ convention, and it makes module naming derivable from the filename; supporting
31
+ multiple operations per file would be more capable and worse.
32
+ - **No spooky action at a distance.** Behavior should follow from the code in
33
+ front of you, not from what else is in the Gemfile, what ran first, or which
34
+ selection the walk happened to reach earlier.
35
+ - **Match the ecosystem's conventions** where one exists — a familiar shape costs
36
+ the user zero learning, which is the cheapest simplicity available.
37
+ - **Errors are part of the interface.** A good message names what went wrong,
38
+ where, and what to do about it. `optional: true` in the message beats the same
39
+ advice buried in docs. The best bug fix often makes an error impossible; the
40
+ next best makes it self-explanatory.
41
+ - **Refuse rather than guess.** When intent is ambiguous, fail loudly at
42
+ generation time. A silent wrong answer is the most expensive outcome this
43
+ library can produce, because the generated code looks authoritative.
44
+
45
+ When simplicity and capability genuinely conflict, say so out loud and pick
46
+ deliberately — but the default is simple.
47
+
48
+ **Docs are a complexity detector.** A section that belabors a topic is usually
49
+ not a writing problem — it is the library pushing complexity onto the reader.
50
+ Before expanding an explanation, ask whether the thing being explained should
51
+ exist. If two paragraphs are needed to say which of two ways applies, delete one
52
+ of the ways; the docs then shrink on their own, which is the signal the cut was
53
+ right.
54
+
11
55
  ## Sorbet typing policy — type by value, not for coverage
12
56
 
13
57
  Sorbet being core does **not** mean every file should be `# typed: strict`. Type
@@ -20,10 +64,11 @@ where it pays off in developer experience; leave the rest at `# typed: true`.
20
64
  the product.
21
65
  - **`# typed: true` (loose) — dynamic / boundary internals.** The codegen
22
66
  (`codegen.rb`, `codegen/nodes.rb`, `codegen/emit.rb`, `codegen/scalar_type.rb`,
23
- `codegen/enum_type.rb`) walks graphql-ruby's approximately-typed AST and builds
24
- modules/strings dynamically; `client.rb` wraps a graphql-ruby schema and a
25
- duck-typed transport. Strict here is ~all `T.untyped` — paperwork that documents
26
- shape without catching anything. **Don't promote these to strict.**
67
+ `codegen/enum_type.rb`, `codegen/type_helpers.rb`) walks graphql-ruby's
68
+ approximately-typed AST and builds modules/strings dynamically; `client.rb`
69
+ wraps a graphql-ruby schema and a duck-typed transport. Strict here is ~all
70
+ `T.untyped` — paperwork that documents shape without catching anything.
71
+ **Don't promote these to strict.**
27
72
  - Rule of thumb: if a sig would be mostly `T.untyped`, it isn't worth writing.
28
73
  Concrete types = value; `T.untyped` sigs = paperwork.
29
74
  - `railtie.rb` / `tasks.rb` are `# typed: ignore` (Rails/Rake DSL).
@@ -48,15 +93,62 @@ where it pays off in developer experience; leave the rest at `# typed: true`.
48
93
 
49
94
  ## Green before commit
50
95
 
96
+ `bundle exec` needs the rvm ruby — the default PATH ruby is 2.6 and can't even
97
+ load bundler:
98
+
99
+ ```sh
100
+ source ~/.rvm/scripts/rvm && rvm use 3.4.9
101
+ ```
102
+
103
+ Then:
104
+
51
105
  ```sh
52
- bundle exec rspec # full suite
53
- bundle exec srb tc # Sorbet typecheck (CI gates on this too)
106
+ bundle exec rspec # full suite
107
+ bundle exec srb tc # Sorbet typecheck (CI gates on this too)
108
+ bundle exec ruby bin/generate # regenerate fixtures — must leave the tree clean
54
109
  ```
55
110
 
56
- Both must pass. Sorbet sigs are runtime-checked by sorbet-runtime, so a wrong
57
- sig surfaces as an rspec failure, not only a `srb tc` error — a green suite
111
+ All three must pass. Sorbet sigs are runtime-checked by sorbet-runtime, so a
112
+ wrong sig surfaces as an rspec failure, not only a `srb tc` error — a green suite
58
113
  validates the sigs against real usage.
59
114
 
115
+ Two more when the change could reach them:
116
+
117
+ ```sh
118
+ bundle exec rspec --order rand:1 # and a couple of other seeds
119
+ bundle exec ruby bin/federation-diff # the fixture supergraph still composes
120
+ ```
121
+
122
+ Order-independence is worth checking rather than assuming — four order-dependent
123
+ failures have hidden behind the default `:defined` order, and a *burst* of them
124
+ usually means one shared resource cascading rather than many bugs.
125
+
126
+ ## Drive it from a throwaway app when the host seam changes
127
+
128
+ **The suite cannot test the gem's relationship with its host.** It is not a Rails
129
+ app, so anything that depends on Rails' boot order, Zeitwerk, or rake's task
130
+ graph is structurally invisible to it — and both bugs found that way were silent
131
+ in development and only appeared in production boot or when registrations
132
+ mattered:
133
+
134
+ - `rake graph_weaver:generate` never ran `:environment`, because the task asked
135
+ `Rake::Task.task_defined?("environment")` at *load* time and Rails defines it
136
+ *after* railties' `rake_tasks` blocks. Every `register_scalar`/`extend_type` in
137
+ an initializer was silently dropped at generation, and `verify` reported the
138
+ result up to date.
139
+ - `app/graphql/generated` sits under a Zeitwerk root while its files define
140
+ top-level constants, so eager loading raised `NameError`. Lazy dev boot was
141
+ fine; production was not.
142
+
143
+ So when the railtie, the rake tasks, the generator, or the documented install
144
+ path changes, spin up a scratch Rails app outside the repo, point its Gemfile at
145
+ your checkout, and actually use it. Worth exercising: **both** a remote endpoint
146
+ and the app's own graphql-ruby schema in-process; `RAILS_ENV=production` boot and
147
+ `rails zeitwerk:check`; a registration in `config/initializers` that must reach
148
+ generated output; the rake tasks end to end; and the testing harness from inside
149
+ the app's own specs. The app is disposable — rebuilding it is cheaper than the
150
+ bugs it catches.
151
+
60
152
  ## Version bumps
61
153
 
62
154
  Bump `lib/graph_weaver/version.rb` and, in the **same commit**:
data/DECISIONS.md ADDED
@@ -0,0 +1,309 @@
1
+ # Decisions
2
+
3
+ Roads not taken, and why. The code shows what was chosen; it is silent about
4
+ what was considered and rejected — and those are the ones that get re-litigated,
5
+ usually by someone with the same good instinct that was already followed to its
6
+ end once.
7
+
8
+ Only entries where the rejected path is *tempting* belong here. An obvious call
9
+ needs no record. What changed and when lives in `CHANGELOG.md`; the invariants
10
+ and principles live in `CLAUDE.md`; stated non-goals (subscriptions, `@defer`,
11
+ uploads, normalized caching, fragment masking, batching) live in `REVIEW.md` §7.
12
+
13
+ ---
14
+
15
+ ## The client slot stays duck-typed
16
+
17
+ **Considered:** formalizing `execute(query, variables:, operation_name:)` as a
18
+ Sorbet interface or a base class, so the thing every client implements is
19
+ declared rather than implied.
20
+
21
+ **Rejected because** a live graphql-ruby `Schema` class satisfies the slot and
22
+ cannot inherit from us. An interface would exclude the one implementation we
23
+ neither wrote nor control — and that implementation is the whole in-process
24
+ story. The openness is the feature.
25
+
26
+ Downstream of this: `Parsing` requires `#schema` of its includers and can't
27
+ declare it, hence one `T.unsafe(self)` in that mixin.
28
+
29
+ ## `InProcess` lives outside `Transport::`
30
+
31
+ **Considered:** moving it under `Transport::`, or restructuring so
32
+ `Transport::HTTP` parents a native and a Faraday implementation and `InProcess`
33
+ slots in beside them.
34
+
35
+ **Rejected because** `Transport` is a base class, not a namespace: it owns the
36
+ GraphQL-over-HTTP flow (encode the body, classify network failures, `ServerError`
37
+ on non-2xx, parse). `InProcess` implements none of that. And the restructure
38
+ wouldn't achieve its goal anyway — a bare `Schema` class is in the client slot
39
+ too, so `Transport::` still wouldn't mean "things you can pass as `transport:`".
40
+ The taxonomy can't be clean because the slot is deliberately open.
41
+
42
+ Renaming the abstract base (`Transport::Base`) to stop it sharing a name with its
43
+ namespace is the smaller, honest version if this ever itches again.
44
+
45
+ ## `Transport::HTTP` is the default; Faraday is opt-in
46
+
47
+ **Considered:** keeping the previous `defined?(::Faraday)` auto-detection.
48
+
49
+ **Rejected because** Faraday arrives transitively through stripe, octokit and
50
+ friends, so adding an unrelated gem silently changed your transport, your
51
+ timeouts, and your connection reuse (Faraday's default adapter reconnects per
52
+ request; measured 10 connections for 10 requests versus 1). Behaviour that
53
+ depends on what else is in the Gemfile is unreasonable-about-able. Same code,
54
+ same transport.
55
+
56
+ ## Cassettes live in `spec/cassettes`, not `spec/fixtures`
57
+
58
+ **Considered:** `spec/fixtures/`, or a namespaced `spec/fixtures/graph_weaver/`,
59
+ as the more conventional home for test data.
60
+
61
+ **Rejected because** Rails globs the fixture path for `{**,*}/*.yml`
62
+ (`active_record/test_fixtures.rb`) — recursively, so a subdirectory doesn't save
63
+ you. `fixtures :all` would try to load cassettes as ActiveRecord fixtures. The
64
+ conventional-looking choice is the broken one.
65
+
66
+ ## Directories organize queries; they don't namespace modules
67
+
68
+ **Considered:** deriving module names from nested query directories, so
69
+ `admin/pets.graphql` becomes `AdminPetsQuery` instead of colliding with
70
+ `pets.graphql`.
71
+
72
+ **Rejected because** it *moves* collisions rather than removing them —
73
+ `admin/pets.graphql` and `admin_pets.graphql` would then collide — so the
74
+ refusal has to exist either way, and the naming rule stops being statable
75
+ without describing which path segments count. A duplicate base name refuses,
76
+ naming both files.
77
+
78
+ ## `#parse` requires a schema, so `Retry` doesn't have it
79
+
80
+ **Considered:** delegating `#parse` through `Retry` to whatever it wraps, so
81
+ `Retry.new(client).parse(q)` reads naturally.
82
+
83
+ **Rejected because** `respond_to?(:parse)` would then be true half the time and
84
+ false the other half — a `Retry` over a bare HTTP transport has no schema to
85
+ reach through to. The rule is "anything holding a schema can parse against it",
86
+ which is a domain, not an exception. `Retry` doesn't meet the precondition.
87
+
88
+ ## Mutations generate `…Mutation` modules
89
+
90
+ **Considered, and initially rejected:** keeping the uniform `…Query` suffix,
91
+ because renaming breaks call sites *and* because the generated filename changes,
92
+ leaving a stale `_query.rb` that `load_generated!` keeps requiring.
93
+
94
+ **Reversed once generated-file pruning landed** — the stale-file half of the
95
+ objection dissolved, and the remaining "one rule beats a conditional one"
96
+ argument lost to `SaveListEntryMutation.execute!` being what a user types every
97
+ day. The generated filename mirrors the constant, so `ls generated/` still
98
+ answers "what's the constant".
99
+
100
+ ## One shared types module, not three
101
+
102
+ **Considered:** keeping `GraphQLInputs`, `GraphQLUnions` and `GraphQLEnums`
103
+ separate on the grounds that their file shapes genuinely differ (a manifest plus
104
+ per-type files versus a single file).
105
+
106
+ **Rejected because** file layout is an implementation detail and the constant a
107
+ user types is not. Merging also *deleted* a mechanism: the artifacts used to
108
+ alias each other's constants across files, which lexical scope now handles for
109
+ free. Per-type files were extended to all three rather than dropped — adding an
110
+ enum value diffs one file.
111
+
112
+ ## Subgraph mapping is derived, then verified — never guessed
113
+
114
+ **Considered:** requiring an explicit `subgraphs:` map, on the grounds that
115
+ auto-detection is guessing and a wrong guess silently points a test suite at the
116
+ wrong resolvers.
117
+
118
+ **Rejected because** matching on *what a schema defines* against the routing
119
+ table is a derivation with evidence, not a guess — and the ambiguous cases
120
+ (two candidates, or none) refuse rather than pick. Detection and validation are
121
+ the same check run in two directions, so there is no second code path to
122
+ disagree.
123
+
124
+ ## Only some `ArgumentError`s were branded
125
+
126
+ **Considered:** two uniform answers. Brand all ~50 `raise ArgumentError` sites
127
+ under `GraphWeaver::Error`, so "everything descends from `Error`" is literally
128
+ true; or leave subgraph detection's refusals as `ArgumentError` and qualify the
129
+ sentence in `docs/errors.md`.
130
+
131
+ **Rejected because** the first throws away the one thing `ArgumentError`
132
+ communicates — you passed something wrong at this call site, like any Ruby
133
+ method — and `pool_size: must be >= 1` is exactly that. The second leaves the
134
+ refusals a `Testing::Router` user actually meets outside the umbrella the docs
135
+ point them at, which is where a spec helper rescues.
136
+
137
+ What survives is a line that can be stated: **what the library concludes,
138
+ having read your schema, is a `GraphWeaver::Error`; an argument wrong on its
139
+ face is an `ArgumentError`.** Subgraph mapping (`ConfigurationError`) and a
140
+ query file whose name can't spell a constant are verdicts; `cast:` not being a
141
+ Symbol is not. A rule with a stated boundary beats a uniform one that lies
142
+ about half its cases.
143
+
144
+ ## An abstract type is bucketed on `__typename`, not planned away
145
+
146
+ **Considered:** keeping the `abstract_boundary` refusal, on the reasoning that a
147
+ representation needs one concrete `__typename` and the planner — which takes no
148
+ variables and runs before any fetch — cannot know it.
149
+
150
+ **Rejected because** the planner doesn't have to know it. It only has to plan
151
+ *every* possibility: the supergraph says which concrete types a subgraph can
152
+ answer a union or interface with, so the plan carries a branch per type and
153
+ execution picks the one the data came back as. Deciding at execution is the
154
+ existing precedent — `@skip`/`@include` already filter deferrals against the
155
+ variables in hand for exactly the same reason.
156
+
157
+ The corollary is smaller and sharper than the rule it replaced. A fragment
158
+ whose condition can't hold at a position — `... on Note` where the answering
159
+ subgraph's union holds no Note — is **dropped**, not refused, even though
160
+ "refuse rather than guess" pulls the other way. It isn't a guess: the fragment
161
+ can never match, so `{}` is the only answer, and a real `@apollo/gateway`
162
+ returns exactly that. What still refuses is the case where the supergraph
163
+ genuinely doesn't say — no `@join__unionMember`/`@join__implements`, and the
164
+ type in more than one subgraph — because then the branch list itself would be
165
+ invented.
166
+
167
+ ## A nested field set crosses whole, or not at all
168
+
169
+ **Considered:** assembling a nested `@key`/`@requires` object from more than
170
+ one fetch — `store { id }` from the subgraph in hand and `store { region
171
+ { code } }` from a prefetch, deep-merged into one representation. It is what
172
+ `@apollo/gateway` does internally (`deepMerge(entity, dataReceivedFromService)`),
173
+ and it would close the last nested case rather than refusing it.
174
+
175
+ **Rejected because** the only shapes that produce the split are ones where the
176
+ gateway is no longer an oracle. A field set reaching *through* a key field is
177
+ the common one, and there the gateway doesn't split at all: composition drops
178
+ `@external` from key fields — an entity's key is answerable by any subgraph
179
+ declaring it — so the extending subgraph looks able to resolve the whole path,
180
+ the gateway satisfies the `@requires` locally, and gets back whatever that
181
+ subgraph happens to hold. Merging would mean answering *better* than the
182
+ gateway, which under `0 wrong` is the same failure as answering worse. So a
183
+ root fed by two fetches refuses, naming both halves and where each comes from.
184
+
185
+ That trap is also why the fixture graph's nested `@requires` walks a plain
186
+ external field (`dimensions`) and not the nested `@key`'s object (`store`):
187
+ the first is diffable against a real gateway, the second isn't.
188
+
189
+ ## The in-process router refuses rather than approximates
190
+
191
+ **Considered:** planning every query shape, falling back to a best-effort answer
192
+ where the semantics are uncertain.
193
+
194
+ **Rejected because** a test double that answers 5% of queries differently from
195
+ production is worse than one that answers 80% and declines the rest loudly. The
196
+ refusal boundary *is* the product. Non-null propagation is the concrete reason:
197
+ before that pass existed, three queries returned silently wrong data where the
198
+ real router returned `data: null`.
199
+
200
+ A corollary: a `--strict` mode for the drift differ was built and then deleted,
201
+ because once a partly-local supergraph became a supported setup, failing on any
202
+ skipped subgraph was wrong for every graph except a fully-local one — and that
203
+ one's report already says "checked 3 of 3".
204
+
205
+ ## `federation:diff` fails when it checked *nothing*
206
+
207
+ **Considered:** leaving zero-checked as a pass, on the `--strict` reasoning
208
+ directly above — absence is supported, and the headline already says "checked 0
209
+ of 4".
210
+
211
+ **Rejected because** zero is not a small number of subgraphs, it is a different
212
+ kind of answer: the gate would pass whatever the subgraphs said, so a green run
213
+ carries no information at all. "Checked 3 of 4" did real work. And the failure
214
+ that produced it was silent — Rails leaves `rake_eager_load` false, so a stock
215
+ app's CI gated on nothing while printing honest prose. There is no setup where
216
+ you'd deliberately run this task against a supergraph none of whose subgraphs
217
+ are here; the abort says to drop it from CI if that's really you.
218
+
219
+ The rule stays statable in one sentence: it fails when it found drift, and when
220
+ it had nothing to look at.
221
+
222
+ ## Output structs allow Ruby-keyword prop names
223
+
224
+ **Considered:** narrowing the ban to types with a registered `alias:` whose path
225
+ starts at the prop.
226
+
227
+ **Rejected because** the ban turned out to be unnecessary, not merely too broad.
228
+ All 33 producible keywords construct, deserialize and typecheck as props; the
229
+ only bare read is an `alias:` delegator's first hop, which now spells
230
+ `self.next`. The proposed narrowing would also have made generation depend on
231
+ unrelated global registry state.
232
+
233
+ ## Queries directories are a list again
234
+
235
+ **Considered:** leaving `queries_path` singular, as 0.4.x made it — one
236
+ `generate!` run reads one directory against one schema, and a second entry
237
+ would produce modules at runtime that `rake graph_weaver:generate` never
238
+ generated and `verify` never checked.
239
+
240
+ **Rejected because** that failure was the *divergence*, not the plurality:
241
+ back then `load_queries!` walked the list and `generate!` read only its first
242
+ entry. Every reader now goes through `GraphWeaver.query_files`, so a second
243
+ directory is generated, verified and loaded alike — and a duplicate module
244
+ name across two directories refuses, as it already did within one. What
245
+ survives is the honest half of the argument: one run reads one *schema*, so
246
+ `schema_path` stays singular.
247
+
248
+ ## Deferred, deliberately
249
+
250
+ - **`write_timeout` on `Transport::HTTP`** — a real gap (nothing bounds sending),
251
+ but not yet worth a kwarg. Note `open_timeout` *is* the connect timeout and
252
+ covers the TLS handshake too (`net-http`'s `ssl_socket_connect(s, @open_timeout)`);
253
+ `ssl_timeout` is the OpenSSL session timeout and not a handshake deadline.
254
+ - **A `net_http:` passthrough hash** — the answer if the timeout/TLS kwarg list
255
+ keeps growing. Deliberately a hash and not a block: the pool creates
256
+ connections lazily and on failure, so a block would run an unpredictable number
257
+ of times. Configuration survives that; behaviour doesn't.
258
+
259
+ ## Coercion says *whether*, never *how*
260
+
261
+ **Considered:** keeping `coerce: <Symbol>` (`register_scalar("ID", String, coerce: :to_s)`),
262
+ which let a registration name the conversion as well as opt into it.
263
+
264
+ **Rejected because** it asked the user to answer a question the library already
265
+ answers — the conversion for every scalar that has one is derived from the
266
+ scalar itself, and a custom scalar's conversion is its `cast:`/`serialize:`
267
+ pair. Its documented showcase existed only to re-enable something deliberately
268
+ removed from the auto path: the feature arguing for its own removal.
269
+
270
+ **Also considered:** dropping the `Int`/`Float` conversion entirely, leaving
271
+ parse as the single coercion mechanism. **Rejected because** `first: params[:page_size]`
272
+ arriving as a String is the most common real coercion in a Rails app, and
273
+ without it `auto_coerce` would loosen nothing among the built-ins but `Date` —
274
+ capability loss wearing simplicity's clothes.
275
+
276
+ `coerce:` and `auto_coerce` both survive because they are one question at two
277
+ scopes — a global default with a local override, the standard shape.
278
+
279
+ ## `config.schema` refuses a subgraph rather than splitting in two
280
+
281
+ **Considered:** splitting the setting, since it serves two masters — the schema
282
+ fakes are fabricated against, and the live class `:in_process` runs. In a
283
+ federated app you can't have both, and setting one to make `:in_process` work
284
+ silently repointed `:fake` at a fraction of the graph.
285
+
286
+ **Rejected because** the split is a second knob plus a rule about which one
287
+ applies, and it buys a capability nothing lost: `:router` runs a subgraph's
288
+ real resolvers too, stitched. The two masters only want different objects in a
289
+ federated app, so refusing a subgraph class is the smaller change that makes
290
+ `config.schema` mean one thing again — and the refusal `:in_process` already
291
+ raises ("a federated graph has no one schema class — tag those examples
292
+ `graphql: :router`") becomes the whole story instead of half of it.
293
+
294
+ ## The `graphql:` tag names a mode; a client is built in the example
295
+
296
+ **Considered:** letting the tag carry a client — `graphql: Failure.throttled`,
297
+ or `graphql: FakeClient.new(overrides: …)` — which reads well and would make
298
+ the tag's value a description rather than the cleanup marker it had become.
299
+
300
+ **Rejected because** metadata is evaluated when the file loads: one client
301
+ object would be shared by every example in the group, built before
302
+ `Testing.configure` had run. Spooky at a distance, and stateful — `#requests`
303
+ would accumulate across examples.
304
+
305
+ What the reading was right about was the leak underneath, and that is fixed
306
+ elsewhere: `GraphWeaver.client` is snapshotted and restored around *every*
307
+ example, so the tag no longer earns its keep as a cleanup marker, and building
308
+ a client is a plain assignment in a `before` block. `graphql_fake(**options)`
309
+ exists only because a fake needs the schema derivation the tag was doing.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graph_weaver (0.4.4)
4
+ graph_weaver (0.5.0)
5
5
  graphql (>= 2.6.7)
6
6
  sorbet-runtime
7
7
 
@@ -42,7 +42,7 @@ GEM
42
42
  google-protobuf (4.35.1-x86_64-linux-gnu)
43
43
  bigdecimal
44
44
  rake (~> 13.3)
45
- graphql (2.6.7)
45
+ graphql (2.6.10)
46
46
  base64
47
47
  fiber-storage
48
48
  logger
@@ -54,7 +54,7 @@ GEM
54
54
  prism (>= 1.3.0)
55
55
  rdoc (>= 4.0.0)
56
56
  reline (>= 0.4.2)
57
- json (2.20.0)
57
+ json (2.21.2)
58
58
  logger (1.7.0)
59
59
  net-http (0.9.1)
60
60
  uri (>= 0.11.1)
@@ -99,16 +99,16 @@ GEM
99
99
  rubydex (0.2.7-arm64-darwin)
100
100
  rubydex (0.2.7-x86_64-darwin)
101
101
  rubydex (0.2.7-x86_64-linux)
102
- simplecov (1.0.3)
103
- sorbet (0.6.13365)
104
- sorbet-static (= 0.6.13365)
105
- sorbet-runtime (0.6.13365)
106
- sorbet-static (0.6.13365-aarch64-linux)
107
- sorbet-static (0.6.13365-universal-darwin)
108
- sorbet-static (0.6.13365-x86_64-linux)
109
- sorbet-static-and-runtime (0.6.13365)
110
- sorbet (= 0.6.13365)
111
- sorbet-runtime (= 0.6.13365)
102
+ simplecov (1.1.1)
103
+ sorbet (0.6.13454)
104
+ sorbet-static (= 0.6.13454)
105
+ sorbet-runtime (0.6.13454)
106
+ sorbet-static (0.6.13454-aarch64-linux)
107
+ sorbet-static (0.6.13454-universal-darwin)
108
+ sorbet-static (0.6.13454-x86_64-linux)
109
+ sorbet-static-and-runtime (0.6.13454)
110
+ sorbet (= 0.6.13454)
111
+ sorbet-runtime (= 0.6.13454)
112
112
  spoom (1.8.3)
113
113
  erubi (>= 1.10.0)
114
114
  prism (>= 0.28.0)
@@ -176,12 +176,12 @@ CHECKSUMS
176
176
  google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b
177
177
  google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee
178
178
  google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd
179
- graph_weaver (0.4.4)
180
- graphql (2.6.7) sha256=759755ce5819c965b6459c5039605cf2e4d876bb8ba5e03451676870ea787c45
179
+ graph_weaver (0.5.0)
180
+ graphql (2.6.10) sha256=9b7c8633767f516ff9d48a8d6305b2a00a2101c82aa871b92e69086944f9f83e
181
181
  i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
182
182
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
183
183
  irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
184
- json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b
184
+ json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
185
185
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
186
186
  net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
187
187
  netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
@@ -206,13 +206,13 @@ CHECKSUMS
206
206
  rubydex (0.2.7-arm64-darwin) sha256=f0d28bbf4153568be79b671642424750053e0bea971b60ddf5cec19bf4563990
207
207
  rubydex (0.2.7-x86_64-darwin) sha256=b002b259d118ac69de44470eff1597143318402c45630c47371f9542631447dc
208
208
  rubydex (0.2.7-x86_64-linux) sha256=dacfade9fa42ce4469618da6dac07e69d5f3ac6a313b4caced5234c8f052419a
209
- simplecov (1.0.3) sha256=38ef0514f16ae7562f0d0f4df02610071115103d301b6de7dacbcc000082e39b
210
- sorbet (0.6.13365) sha256=3a642fe7afb031ad670c2e6161b3d5492dd67705cbcafb132c9e87ded7ccf676
211
- sorbet-runtime (0.6.13365) sha256=0657cddfd2319c9695a0b6e13aa7a7c7b093149e5589448c03b911adf1e243cb
212
- sorbet-static (0.6.13365-aarch64-linux) sha256=14b7bf3a227ee102c119763158618bc6b677994cebfda2705b94bca56e4c25e1
213
- sorbet-static (0.6.13365-universal-darwin) sha256=0f6033dde8a0dc7b7e72ada3ce73e1e3097843be16e473fb8bb2b8fb6199212b
214
- sorbet-static (0.6.13365-x86_64-linux) sha256=f94daadcae55f2e0f7797f8fdc8a250c89ec62e11f2d41905ed6fb1e60ff708e
215
- sorbet-static-and-runtime (0.6.13365) sha256=9768c114686f7cac0a6f2e222db4213887d294df6059802242ba6e4e08adbad9
209
+ simplecov (1.1.1) sha256=25825ef13f0b2e74694d769817dad6ab8e90131dabdaa666e522fea105521e78
210
+ sorbet (0.6.13454) sha256=3273dda082b8aa5ecf79b7675e2b79127f202a45370d881124af3af19a3c2b35
211
+ sorbet-runtime (0.6.13454) sha256=b9441ae4bd265f51861c54240e8c5888f6355e3284885b21c5154b184e791d80
212
+ sorbet-static (0.6.13454-aarch64-linux) sha256=e09dd1e6cd7e63b3fb6c8c9c6785eeb55a6c4afecb0f932538ee4401b4552632
213
+ sorbet-static (0.6.13454-universal-darwin) sha256=6ac0cf10ae2b4e0e9ecabb886f9be739046424dc35d100e7888c8538a223f93a
214
+ sorbet-static (0.6.13454-x86_64-linux) sha256=569c52ea926e514ebfdc64825f65cd188b7aba5c1b480cc26e1870fd53d5e2c0
215
+ sorbet-static-and-runtime (0.6.13454) sha256=defc00684a6f8bd498bb70a4541f293b9232938a776137946247cba4baed3d20
216
216
  spoom (1.8.3) sha256=32871fa189bbfa49cf557a50f819f23cc9a6ceefd0346caa7a6adc193becd5dd
217
217
  tapioca (0.19.2) sha256=938731b07811aee8d23871b1aee8861d464fbaf2cfffbf79a62b0c869a5120ec
218
218
  thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
data/NOTES.md CHANGED
@@ -174,9 +174,9 @@ design:
174
174
  ## Open questions
175
175
 
176
176
  - interface-typed fields (vs fragment conditions, which work)
177
- - name collisions: the generator disambiguates one level (field-name
178
- prefix) and raises otherwise. A real gem needs a *stable* naming scheme:
179
- names shouldn't shift when unrelated selections are added (generated
180
- code is checked in and referenced by app code), which argues for
181
- path-based or explicitly-aliased names over first-come-first-served
177
+ - ~~name collisions~~ ANSWERED: path-based won. A generated type is named
178
+ for the response key that selects it, so the name is a function of the
179
+ field's own position no walk order, no first-come-first-served, and an
180
+ unrelated selection can't move it. GraphQL aliases double as the explicit
181
+ naming escape hatch (`pet: pets` names the struct `Pet`)
182
182
  - mutations/subscriptions (only query operations generate)