graph_weaver 0.4.6 → 0.5.1

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/CHANGELOG.md +1442 -0
  3. data/Gemfile.lock +23 -23
  4. data/README.md +115 -96
  5. data/docs/cassettes.md +93 -46
  6. data/docs/editors.md +82 -0
  7. data/docs/errors.md +34 -30
  8. data/docs/federation.md +521 -48
  9. data/docs/generated_modules.md +352 -137
  10. data/docs/getting_started.md +237 -67
  11. data/docs/logging.md +35 -6
  12. data/docs/real_world.md +21 -15
  13. data/docs/scalars.md +49 -154
  14. data/docs/testing.md +300 -52
  15. data/docs/transports.md +129 -30
  16. data/docs/upgrading.md +134 -0
  17. data/graph_weaver.gemspec +19 -3
  18. data/lib/generators/graph_weaver/install_generator.rb +259 -0
  19. data/lib/graph_weaver/client.rb +118 -111
  20. data/lib/graph_weaver/codegen/aliases.rb +223 -0
  21. data/lib/graph_weaver/codegen/emit.rb +283 -261
  22. data/lib/graph_weaver/codegen/enum_type.rb +25 -124
  23. data/lib/graph_weaver/codegen/nodes.rb +72 -13
  24. data/lib/graph_weaver/codegen/scalar_type.rb +69 -66
  25. data/lib/graph_weaver/codegen/type_helpers.rb +140 -0
  26. data/lib/graph_weaver/codegen.rb +672 -336
  27. data/lib/graph_weaver/errors.rb +154 -16
  28. data/lib/graph_weaver/federation.rb +259 -0
  29. data/lib/graph_weaver/hints.rb +9 -1
  30. data/lib/graph_weaver/in_process.rb +90 -0
  31. data/lib/graph_weaver/input_struct.rb +14 -2
  32. data/lib/graph_weaver/logging.rb +29 -0
  33. data/lib/graph_weaver/parsing.rb +59 -0
  34. data/lib/graph_weaver/query_module.rb +55 -0
  35. data/lib/graph_weaver/railtie.rb +23 -1
  36. data/lib/graph_weaver/representation.rb +74 -0
  37. data/lib/graph_weaver/response.rb +7 -0
  38. data/lib/graph_weaver/retry.rb +29 -8
  39. data/lib/graph_weaver/rspec.rb +220 -16
  40. data/lib/graph_weaver/schema_loader.rb +819 -60
  41. data/lib/graph_weaver/schemas.rb +48 -0
  42. data/lib/graph_weaver/selection.rb +43 -8
  43. data/lib/graph_weaver/tasks.rb +220 -22
  44. data/lib/graph_weaver/testing/cassette.rb +249 -81
  45. data/lib/graph_weaver/testing/coverage.rb +160 -0
  46. data/lib/graph_weaver/testing/failure.rb +14 -25
  47. data/lib/graph_weaver/testing/fake_client.rb +182 -22
  48. data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
  49. data/lib/graph_weaver/testing/router.rb +1452 -0
  50. data/lib/graph_weaver/testing/subgraphs.rb +134 -0
  51. data/lib/graph_weaver/testing.rb +209 -13
  52. data/lib/graph_weaver/transport/faraday.rb +28 -10
  53. data/lib/graph_weaver/transport/http.rb +99 -36
  54. data/lib/graph_weaver/transport.rb +67 -14
  55. data/lib/graph_weaver/version.rb +1 -1
  56. data/lib/graph_weaver.rb +416 -172
  57. metadata +25 -9
  58. data/CLAUDE.md +0 -69
  59. data/Makefile +0 -23
  60. data/NOTES.md +0 -182
  61. data/PLAN.md +0 -144
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graph_weaver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
@@ -219,23 +219,20 @@ dependencies:
219
219
  - - ">="
220
220
  - !ruby/object:Gem::Version
221
221
  version: '0'
222
- description: A typed GraphQL client for Rubygenerate Sorbet T::Structs from queries,
223
- with federation, extensibility, and testing in mind
222
+ description: Your .graphql files, compiled into Sorbet types and the fakes to test
223
+ them.
224
224
  executables: []
225
225
  extensions: []
226
226
  extra_rdoc_files: []
227
227
  files:
228
228
  - ".yardopts"
229
229
  - CHANGELOG.md
230
- - CLAUDE.md
231
230
  - Gemfile
232
231
  - Gemfile.lock
233
232
  - LICENSE.txt
234
- - Makefile
235
- - NOTES.md
236
- - PLAN.md
237
233
  - README.md
238
234
  - docs/cassettes.md
235
+ - docs/editors.md
239
236
  - docs/errors.md
240
237
  - docs/federation.md
241
238
  - docs/generated_modules.md
@@ -245,30 +242,44 @@ files:
245
242
  - docs/scalars.md
246
243
  - docs/testing.md
247
244
  - docs/transports.md
245
+ - docs/upgrading.md
248
246
  - graph_weaver.gemspec
247
+ - lib/generators/graph_weaver/install_generator.rb
249
248
  - lib/graph_weaver.rb
250
249
  - lib/graph_weaver/client.rb
251
250
  - lib/graph_weaver/codegen.rb
251
+ - lib/graph_weaver/codegen/aliases.rb
252
252
  - lib/graph_weaver/codegen/emit.rb
253
253
  - lib/graph_weaver/codegen/enum_type.rb
254
254
  - lib/graph_weaver/codegen/nodes.rb
255
255
  - lib/graph_weaver/codegen/scalar_type.rb
256
+ - lib/graph_weaver/codegen/type_helpers.rb
256
257
  - lib/graph_weaver/errors.rb
258
+ - lib/graph_weaver/federation.rb
257
259
  - lib/graph_weaver/hints.rb
260
+ - lib/graph_weaver/in_process.rb
258
261
  - lib/graph_weaver/inflect.rb
259
262
  - lib/graph_weaver/input_struct.rb
260
263
  - lib/graph_weaver/logging.rb
264
+ - lib/graph_weaver/parsing.rb
265
+ - lib/graph_weaver/query_module.rb
261
266
  - lib/graph_weaver/railtie.rb
267
+ - lib/graph_weaver/representation.rb
262
268
  - lib/graph_weaver/response.rb
263
269
  - lib/graph_weaver/retry.rb
264
270
  - lib/graph_weaver/rspec.rb
265
271
  - lib/graph_weaver/schema_loader.rb
272
+ - lib/graph_weaver/schemas.rb
266
273
  - lib/graph_weaver/selection.rb
267
274
  - lib/graph_weaver/tasks.rb
268
275
  - lib/graph_weaver/testing.rb
269
276
  - lib/graph_weaver/testing/cassette.rb
277
+ - lib/graph_weaver/testing/coverage.rb
270
278
  - lib/graph_weaver/testing/failure.rb
271
279
  - lib/graph_weaver/testing/fake_client.rb
280
+ - lib/graph_weaver/testing/fake_subgraph.rb
281
+ - lib/graph_weaver/testing/router.rb
282
+ - lib/graph_weaver/testing/subgraphs.rb
272
283
  - lib/graph_weaver/testing/values.rb
273
284
  - lib/graph_weaver/transport.rb
274
285
  - lib/graph_weaver/transport/faraday.rb
@@ -277,7 +288,12 @@ files:
277
288
  homepage: https://github.com/dpep/graph_weaver
278
289
  licenses:
279
290
  - MIT
280
- metadata: {}
291
+ metadata:
292
+ bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
293
+ changelog_uri: https://github.com/dpep/graph_weaver/blob/main/CHANGELOG.md
294
+ documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
295
+ rubygems_mfa_required: 'true'
296
+ source_code_uri: https://github.com/dpep/graph_weaver
281
297
  rdoc_options: []
282
298
  require_paths:
283
299
  - lib
@@ -294,5 +310,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
294
310
  requirements: []
295
311
  rubygems_version: 3.6.9
296
312
  specification_version: 4
297
- summary: GraphWeaver
313
+ summary: A typed GraphQL client for Ruby
298
314
  test_files: []
data/CLAUDE.md DELETED
@@ -1,69 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## What this is
6
-
7
- A typed GraphQL client for Ruby: generates `# typed: strict` Ruby (nested
8
- `T::Struct`s + a typed `execute`) from your queries, checked against the schema
9
- at generation time. Sorbet is core to the product.
10
-
11
- ## Sorbet typing policy — type by value, not for coverage
12
-
13
- Sorbet being core does **not** mean every file should be `# typed: strict`. Type
14
- where it pays off in developer experience; leave the rest at `# typed: true`.
15
-
16
- - **Strict (full sigs) — developer-facing contracts.** The types users touch:
17
- `response.rb` (the envelope every `execute` returns), the error hierarchy
18
- (`errors.rb`), and the **generated code** (emitted `# typed: strict`). Concrete
19
- types here give downstream apps real call-site checking + autocomplete — that's
20
- the product.
21
- - **`# typed: true` (loose) — dynamic / boundary internals.** The codegen
22
- (`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.**
27
- - Rule of thumb: if a sig would be mostly `T.untyped`, it isn't worth writing.
28
- Concrete types = value; `T.untyped` sigs = paperwork.
29
- - `railtie.rb` / `tasks.rb` are `# typed: ignore` (Rails/Rake DSL).
30
-
31
- ## Design invariants (don't "fix" these)
32
-
33
- - **The client slot is duck-typed.** A transport, `Retry`, a live graphql-ruby
34
- schema class, or a test fake all satisfy one contract —
35
- `execute(query, variables:) => {"data" => ..., "errors" => ...}` — with no
36
- shared base class. **Don't formalize it as a strict Sorbet interface**: a
37
- graphql-ruby `Schema` class fits the slot without inheriting anything, and a
38
- strict interface would exclude it. This is why the transport/client seams stay
39
- loosely typed.
40
- - **Codegen is query-driven.** Structs are generated per selection set, only for
41
- the types a query actually touches — not the whole schema (so extra schema
42
- types, e.g. federation `join__*`, generate no code).
43
- - **Leaf codecs vs composite decoration.** `register_scalar` / `register_enum`
44
- *define/replace* how a leaf deserializes (its Ruby shape is fixed);
45
- `extend_type` only *decorates* a generated composite struct with mixins — it
46
- can't replace one, because a composite's shape varies per query. Don't add a
47
- "replace a composite's deserializer" path.
48
-
49
- ## Green before commit
50
-
51
- ```sh
52
- bundle exec rspec # full suite
53
- bundle exec srb tc # Sorbet typecheck (CI gates on this too)
54
- ```
55
-
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
58
- validates the sigs against real usage.
59
-
60
- ## Version bumps
61
-
62
- Bump `lib/graph_weaver/version.rb` and, in the **same commit**:
63
-
64
- - update `Gemfile.lock` (the gem pins its own version there; CI runs a frozen
65
- `bundle install`, which fails at the *setup* step with exit code 16 — before
66
- tests — if the lock is stale), and
67
- - add a `CHANGELOG.md` entry.
68
-
69
- `gem push` (the actual RubyGems release) is a separate, manual step.
data/Makefile DELETED
@@ -1,23 +0,0 @@
1
- .PHONY: check generate test tc integration docs
2
-
3
- # full verify loop: regenerate, test, typecheck
4
- check: generate test tc
5
-
6
- # manual/one-off checks against real GraphQL APIs (network; GitHub needs
7
- # `gh auth login` or GITHUB_TOKEN; federation boots an Apollo gateway — node)
8
- integration:
9
- INTEGRATION=1 bundle exec rspec spec/integration
10
-
11
- generate:
12
- bundle exec ruby bin/generate
13
-
14
- test:
15
- bundle exec rspec
16
-
17
- tc:
18
- bundle exec srb tc
19
-
20
- # preview what rubydoc.info will render
21
- docs:
22
- bundle exec yard doc
23
- open doc/index.html
data/NOTES.md DELETED
@@ -1,182 +0,0 @@
1
- Research notes — graphql-client exploration → GraphWeaver
2
- ======
3
-
4
- **What this is now:** a working prototype of a standalone, Sorbet-typed
5
- GraphQL client for Ruby — "graphql-codegen for Ruby". `.graphql` queries +
6
- a schema (live class, introspection JSON, or SDL) generate `# typed: strict`
7
- Ruby: nested `T::Struct`s, casting code, and a typed `execute`, so `srb tc`
8
- sees the exact shape of every query result. It is **not** a graphql-client
9
- extension: generated code depends only on `graphql` (generation time) and
10
- `sorbet-runtime` (runtime); transport is a pluggable `executor:` (in-process
11
- schema or the bundled `HttpExecutor`).
12
-
13
- Start with `PLAN.md` for current state and next steps. Key files:
14
- `lib/struct_codegen.rb` (the generator), `queries/` → `bin/generate` →
15
- `lib/generated/` (the build loop), `StructCodegen.load` (build-free dynamic
16
- mode for development).
17
-
18
- **How it got here:** the repo began as an exploration of
19
- [graphql-client](https://github.com/github-community-projects/graphql-client)
20
- internals — could its class-generation layer be swapped to emit custom
21
- classes? (Yes: the `StructTypes` spike below.) The per-query codegen
22
- approach then outgrew graphql-client entirely, and everything below the
23
- next heading is preserved as the lab notebook: findings in chronological
24
- order, each backed by a spec.
25
-
26
- The specs are the documentation — each one asserts an observed behavior:
27
-
28
- ```sh
29
- bundle exec rspec
30
- ```
31
-
32
- ## Findings, in exploration order
33
-
34
- - The client runs fine against an in-process schema: `GraphQL::Client.new(schema: Schema, execute: Schema)` — no HTTP involved.
35
- - Each query selection gets its own dynamically generated wrapper class (subclass of `GraphQL::Client::Schema::ObjectClass`); fields are snake_case readers, and unselected fields raise instead of returning nil.
36
- - **Custom scalars are deserialized automatically** when the client is built from a live schema class: the reader casts wire values through the scalar's `coerce_input` (e.g. `"1990-06-15"` → `Date`). This is the built-in hook for producing rich Ruby values.
37
- - Caveat: this only works with `schema:` as a live schema class. A schema loaded from an introspection JSON dump has no coercion logic, so scalars would stay raw.
38
- - `to_h` returns the raw wire values (strings), not the casted ones — hydration code should read via the typed readers, not `to_h`.
39
- - Hydrating into `T::Struct`s is straightforward manually; the interesting next step is generating the structs (or a generic hydrator) from the parsed query definition, since the client already knows each selection's shape and types.
40
-
41
- ## Swapping the class-generation layer (answered: yes)
42
-
43
- `lib/struct_types.rb` + `spec/struct_types_spec.rb` prove the generation layer
44
- can be replaced wholesale — the client deserializes straight into generated
45
- `T::Struct`s, no `ObjectClass` involved.
46
-
47
- How the pipeline hangs together (graphql-client 0.26.0):
48
-
49
- - `Client#initialize` builds the types module: `@types = Schema.generate(schema)`
50
- (`attr_reader :types`, no setter — swap via `instance_variable_set` or a subclass).
51
- - `Client#parse` → `Definition#initialize` calls
52
- `client.types.define_class(definition, ast_nodes, type)` and stores the result
53
- as `definition.schema_class`. This is the ONLY thing the client asks of the
54
- types module.
55
- - `Client#query` → `definition.new(data, errors)` → `schema_class.new(data, errors)`.
56
- - Everything below that is the `cast(value, errors)` protocol, composed
57
- recursively per the query selection (NonNull/List wrappers, scalars, objects).
58
-
59
- So the replacement contract is just:
60
- - `define_class(definition, ast_nodes, type)` returning casters
61
- - casters respond to `cast(value, errors)`
62
- - the top-level caster must satisfy `Definition#new`'s case dispatch, which
63
- tests `===` against the `GraphQL::Client::Schema::ObjectType` module —
64
- including that module in your caster class is enough, plus a
65
- `new(data, errors)` method
66
-
67
- Gotchas found:
68
- - the client injects `__typename` into every selection (`QueryTypename`), so a
69
- custom generator must skip/handle `__`-prefixed fields
70
- - scalar casting reuses the schema type's `coerce_isolated_input` — same hook
71
- the stock `ScalarType` uses
72
- - prop nullability comes for free from the type walk: everything is
73
- `T.nilable` unless wrapped in NON_NULL
74
-
75
- ## Sorbet
76
-
77
- - `sorbet` + `tapioca` are set up (`bundle exec srb tc` is green); rbis in `sorbet/rbi/gems`
78
- - `struct_types.rb` typechecks at `# typed: true`
79
- - generated structs are real `T::Struct`s: schema-derived prop types
80
- (`T.nilable(Date)`, `T::Array[StructTypes::Pet]`) and runtime type
81
- enforcement on bad wire data
82
-
83
- ## Codegen: srb tc sees query result types (answered: yes)
84
-
85
- `lib/struct_codegen.rb` goes one step further than the runtime swap: it
86
- emits plain `# typed: strict` Ruby source from a query + schema — nested
87
- `T::Struct` classes, fully generated `from_h` casting code (no runtime
88
- reflection), and a sig'd `execute`.
89
-
90
- - source of truth: `queries/*.graphql`; regenerate with `bin/generate`
91
- into `lib/generated/`; a spec asserts the checked-in output is current
92
- - queries are validated against the schema at generation time
93
- - `srb tc` statically checks result access end to end:
94
- `result.person&.nmae` → `Method nmae does not exist on
95
- PersonQuery::Result::Person`
96
- - custom scalar deserialization is inlined by the generator
97
- (`Date.iso8601(...)`) via a scalar registry; nullability and list
98
- casting come from the NON_NULL/LIST walk
99
- - note: generated `execute` runs against the schema directly, replacing
100
- graphql-client at runtime entirely — the client's remaining value here
101
- would be its HTTP adapter, which the generated code could target instead
102
-
103
- ## Fragments & unions (answered for codegen)
104
-
105
- `queries/search.graphql` + `lib/generated/search_query.rb` exercise the
106
- design:
107
-
108
- - inline fragments and named fragment spreads are flattened into their
109
- matching member's selection (exact type-name condition match; interface
110
- conditions still open)
111
- - unions emit a module per selection site: one `T::Struct` per possible
112
- type, a `Type = T.type_alias { T.any(...) }`, and a `from_h` that
113
- dispatches on `__typename` — codegen refuses union selections that
114
- don't select `__typename`
115
- - every possible type gets a member struct even without a fragment (it
116
- still carries `__typename`), so dispatch is total
117
-
118
- ## Introspection / __type metadata
119
-
120
- - `__type` / `__schema` queries work against the demo schema as expected
121
- (see `spec/introspection_spec.rb` for the shapes)
122
- - the key result: `GraphQL::Schema.from_introspection(Demo::Schema.as_json)`
123
- produces a schema that codegen runs against **byte-identically** — so
124
- generation works for remote APIs known only via an introspection dump.
125
- Custom scalar handling survives because the codegen scalar registry is
126
- keyed by type *name*, unlike runtime `coerce_input` which needs the live
127
- schema class (the caveat that broke graphql-client's scalar casting)
128
-
129
- ## Federation / supergraph
130
-
131
- - join__/link-annotated supergraph SDL parses via
132
- `GraphQL::Schema.from_definition`, and codegen runs against it
133
- unchanged — the directives are transparent to result typing
134
- (`spec/federation_spec.rb` generates from a mini supergraph and casts a
135
- response with no live subgraphs)
136
- - gotcha: graphql-ruby's SDL builder does not apply directive-argument
137
- defaults, so real Apollo `join v0.3` SDL (non-null defaulted args like
138
- `extension: Boolean! = false`) fails to load unless those args are
139
- provided or the directive defs are trimmed — a compatibility issue a
140
- real tool would need to patch around
141
- - client-side, federation needs nothing more: you query the router like
142
- any schema. The *server-side* angle (emitting `@key`/`@external` via
143
- apollo-federation) is a separate exploration — potentially relevant to
144
- autographql
145
-
146
- ## Round 2: enums, interface conditions, loaders, dynamic mode, HTTP
147
-
148
- - **enums** generate `T::Enum` classes (`Species::Dog`), deserialized via
149
- `Species.deserialize(...)` in `from_h`; values sorted so output is
150
- deterministic across schema sources
151
- - **interface fragment conditions** (`... on Named { name }`) apply via
152
- `schema.possible_types`, not just exact type-name match. Interface-typed
153
- *fields* (a field returning `Named`) are still open — they'd emit like
154
- unions with `__typename` dispatch
155
- - **SchemaLoader** accepts both formats a remote service can hand you:
156
- introspection dump (`.json`) or SDL (`.graphql`/`.gql`); both generate
157
- byte-identically to the live schema class
158
- - **dynamic mode**: `StructCodegen.load(...)` generates + evals in one
159
- step — no build artifact, same runtime semantics, right for development
160
- or one-off scripts. Tradeoff: the module is invisible to `srb tc`, so
161
- static checking of result access needs the build step
162
- - **HTTP transport**: generated `execute` takes `executor:` — anything
163
- with `execute(query, variables:)` returning `{"data" => ...}`.
164
- `HttpExecutor` (Net::HTTP POST) runs the same generated structs against
165
- a live server (`spec/http_spec.rb` proves it against a local WEBrick
166
- serving Demo::Schema)
167
- - **directive defaults gap**: root cause found —
168
- `BuildFromDefinition#prepare_directives` passes only usage-site args
169
- while `Directive#initialize` validates all defined args without
170
- applying `default_value`. `lib/directive_defaults_patch.rb` prepends
171
- the fix; the federation spec now loads the *real* join v0.3 SDL.
172
- Present in graphql 2.6.3 (latest) — worth an upstream issue/PR
173
-
174
- ## Open questions
175
-
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
182
- - mutations/subscriptions (only query operations generate)
data/PLAN.md DELETED
@@ -1,144 +0,0 @@
1
- # Project Plan — GraphWeaver, typed GraphQL client for Ruby/Sorbet
2
-
3
- _Resume-from-here notes. README documents the product, NOTES.md is the
4
- research notebook this grew out of; this is the plan. Update on change._
5
-
6
- ## Vision
7
-
8
- A "graphql-codegen for Ruby": `.graphql` queries + a schema (live class,
9
- introspection JSON, or SDL) → checked-in `# typed: strict` Ruby — nested
10
- T::Structs, generated casting, typed execute — so `srb tc` sees the exact
11
- shape of every query result. Dynamic (eval) mode for development, build
12
- step for CI/static checking. Runtime deps: graphql + sorbet-runtime only
13
- (graphql-client is NOT a dependency; the exploration outgrew it).
14
-
15
- ## State: v0.0.1 on rubygems; v0.0.2 accumulating on main
16
-
17
- `make check` = bin/generate (spec fixture regeneration; parity specs
18
- enforce freshness) + rspec + srb tc.
19
-
20
- Language coverage: queries, mutations, typed variables (kwargs on
21
- execute, optional-when-defaulted), fragments (inline, named, interface
22
- conditions), union- AND interface-typed fields (__typename dispatch,
23
- required at generation time), enums (T::Enum), custom scalars via the
24
- ScalarType registry (register_scalar: codec inference off .parse/.load,
25
- requires:, opt-in input coercion incl. built-ins).
26
- Sources: live schema / introspection JSON / SDL — byte-identical output
27
- (enum values + abstract-type members sorted for determinism).
28
- Transport: executor precedence per call → per module → baked const →
29
- GraphWeaver.executor; Transport::HTTP (zero-dep) + opt-in Transport::Faraday
30
- (url / block middleware / ready connection), e2e specs against WEBrick.
31
- Errors: typed Response envelope (partial data + extensions survive) and
32
- a GraphWeaver::Error hierarchy (Transport/Server/Query/Validation/Type)
33
- with extensible transport-error classification, schema_stale? staleness
34
- detection, errors_at/each_error/report field-level surfacing (entity
35
- ids resolved from partial data), and #to_h machine output throughout.
36
- Dynamic mode: GraphWeaver.parse (paths or raw strings, derived names,
37
- container-scoped constants) and GraphWeaver.execute one-shots.
38
- Schema fetching: SchemaLoader.introspect(executor, cache:, ttl:) off
39
- live endpoints; load takes paths, content, or Hashes.
40
- Testing (graph_weaver/testing + graph_weaver/rspec): FakeExecutor
41
- (schema-correct castable fakes, faker semantics, GraphQL-name
42
- overrides, corrupt:, fail_at: with null propagation), Failure canned
43
- executors + SequenceExecutor, cassettes with shape-preserving
44
- anonymization, rspec seed + auto_fake integration. Selection module is
45
- the single shared query-walk (codegen/fake/anonymizer).
46
- Federation supergraph SDL loads transparently (needs
47
- directive_defaults_patch until upstream fix ships).
48
- Live validation: make integration (GitHub + Countries APIs).
49
-
50
- ## Next steps (in rough order)
51
-
52
- ~~Extraction~~ DONE 2026-07-07: this repo IS the gem now — GraphWeaver,
53
- github.com/dpep/graph_weaver, rspec-uuid conventions throughout. The
54
- graphql-client spikes live in git history (tag: `exploration`) and
55
- NOTES.md. Prior-art check partially answered: graphql-client PR #7
56
- (tapioca compiler over schema-wide dynamic classes) stalled since
57
- Jan 2024 with users asking; schema-wide typing can't catch
58
- unfetched-field bugs or type unions/interfaces — the niche looks open.
59
-
60
- ~~Input objects~~ DONE 2026-07-11: module-level T::Structs, serialize/
61
- to_h, hash coercion at the execute boundary.
62
- ~~Release~~ 0.1.0 cut 2026-07-11 (breaking: execute returns the
63
- Response envelope; execute! for raise-or-result).
64
-
65
- 1. Stable class naming design — names come from GraphQL type names per
66
- selection site; must not shift when unrelated selections are added
67
- (generated code is app-code API). Current: one-level field-name
68
- disambiguation, then raise. Shipped in 0.1.0 as-is — a naming change
69
- is fair game pre-1.0 but should land early.
70
- 2. CLI entrypoint (graph_weaver generate --schema X --queries dir) —
71
- bin/generate is spec-fixture tooling, not shipped.
72
- 3. Subscriptions (unsupported; raise). Recursive input types (raise).
73
- 6. Parse/execute memoization: repeated GraphWeaver.parse/execute of the
74
- same [schema, query] re-generates and re-evals every call (~3x the
75
- cost of a cached module; benchmarked 2026-07-09) — memo keyed on
76
- schema/query/name/executor, minding shared executor= mutation.
77
- (Schema-side caching landed: SchemaLoader.introspect cache:/ttl: +
78
- introspection_result primitive for Rails.cache et al. Possible
79
- follow-up: re-introspect + retry once on validation-shaped
80
- QueryErrors, since GraphQL has no standard schema-version signal.)
81
- 7. Nice-to-haves: __typename auto-injection (currently required manually
82
- on abstract selections), fragment reuse across queries, directives on
83
- selections (@skip/@include make non-null fields nullable).
84
- 8. Tapioca DSL compiler over dynamic mode (idea from graphql-client
85
- PR #7): RBI the GraphWeaver.parse-eval'd modules so development mode
86
- gets static types without the bin/generate build step — tapioca is
87
- already in every Sorbet shop's workflow. Upstream's
88
- Tapioca::Dsl::Helpers::GraphqlTypeHelper is prior art for type mapping.
89
-
90
- ## External dependencies
91
-
92
- - rmosolgo/graphql-ruby#5659 (directive-argument defaults fix; our branch
93
- `directive-argument-defaults` in ~/code/lib/ruby/graphql, pushed to the
94
- dpep fork, PR in draft). When it ships in a release: bump graphql,
95
- delete lib/graph_weaver/directive_defaults_patch.rb + its requires (TODO in file).
96
-
97
- ## Gotchas worth remembering
98
-
99
- - graphql-ruby to_definition/from_introspection reorder enum values and
100
- possible_types — codegen sorts both; keep any new emission deterministic
101
- - schemas built from introspection/SDL have no scalar coercion or
102
- resolvers — codegen must stay name-keyed, never call schema runtime hooks
103
- - graphql-client (the gem) casts scalars via coerce_isolated_input and
104
- only with a live schema class — documented in the early specs
105
-
106
- ## From the field-test experiments (2026-07-12)
107
-
108
- Generated-module size (2026-07-12, post-0.2.0): input structs are now
109
- table-driven (InputStruct runtime + FIELDS) — the PokeAPI filtered-query
110
- module dropped 29,233 -> 11,562 lines. The floor is ~2 lines/field
111
- (typed const + FIELDS entry). Remaining lever if it matters again:
112
- shared input structs emitted once per schema instead of per module.
113
-
114
-
115
- A junior + senior agent pair exercised the repo cold (clone, examples,
116
- extensions, a Pokedex app against Hasura's 4,441-type PokeAPI schema).
117
- Fixed same-day: snake_case type names generated invalid constants
118
- (camelize), GraphQL::ParseError/NotImplementedError escaping the Error
119
- umbrella, HttpExecutor timeouts, GitHub's top-level "type" error codes,
120
- typo'd client registrations silently no-oping. Still open:
121
-
122
- ~~Recursive input types~~ DONE 2026-07-12: register-before-walk breaks
123
- the recursion; emission dependency-orders the structs and, for cycles,
124
- forward-declares the classes in an eval (srb rejects reopening a
125
- T::Struct statically, but adding props at runtime works — the full
126
- bodies below the eval are all srb sees). Verified live against Hasura
127
- bool_exp.
128
-
129
- ~~Connection reuse in Transport::HTTP~~ DONE 2026-07-12: persistent
130
- keep-alive connection behind a mutex, dropped on any failure; net/http's
131
- keep_alive_timeout handles idle expiry. Faraday remains the pooling
132
- answer.
133
-
134
- - Subscriptions; @defer/@stream (routers send multipart responses — the
135
- transport classifies them as ServerError today, no incremental support).
136
- - Shared input-type structs across generated modules: one Hasura
137
- bool_exp variable pulls its whole recursive closure into EVERY module
138
- (~28k lines each) — correct but heavy in PRs; needs cross-module
139
- sharing or selection-based pruning (field-test round 2).
140
- - Structured logging: log_tag pairs lines and names operations now, but
141
- events are prose — an optional {event:, url:, ms:} payload contract, a
142
- scrub_variables hook, and cache-age on hit lines (field-test round 2).
143
- - Cut 0.1.1 — RubyGems 0.1.0 is materially behind main (snake_case fix,
144
- recursive inputs, keep-alive, logging, error umbrella).