graph_weaver 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/CHANGELOG.md +213 -0
- data/Gemfile.lock +32 -2
- data/Makefile +7 -2
- data/NOTES.md +1 -1
- data/PLAN.md +34 -1
- data/README.md +56 -41
- data/docs/cassettes.md +76 -0
- data/docs/errors.md +37 -9
- data/docs/generated_modules.md +139 -29
- data/docs/getting_started.md +134 -0
- data/docs/logging.md +33 -0
- data/docs/real_world.md +27 -20
- data/docs/scalars.md +122 -8
- data/docs/testing.md +44 -34
- data/docs/transports.md +124 -0
- data/graph_weaver.gemspec +7 -1
- data/lib/graph_weaver/client.rb +200 -0
- data/lib/graph_weaver/codegen/emit.rb +88 -39
- data/lib/graph_weaver/codegen/enum_type.rb +149 -0
- data/lib/graph_weaver/codegen/nodes.rb +109 -9
- data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
- data/lib/graph_weaver/codegen.rb +301 -67
- data/lib/graph_weaver/errors.rb +37 -25
- data/lib/graph_weaver/hints.rb +63 -0
- data/lib/graph_weaver/inflect.rb +2 -1
- data/lib/graph_weaver/logging.rb +41 -0
- data/lib/graph_weaver/railtie.rb +29 -0
- data/lib/graph_weaver/retry.rb +97 -0
- data/lib/graph_weaver/rspec.rb +19 -14
- data/lib/graph_weaver/schema_loader.rb +156 -20
- data/lib/graph_weaver/selection.rb +3 -3
- data/lib/graph_weaver/tasks.rb +71 -0
- data/lib/graph_weaver/testing/cassette.rb +42 -21
- data/lib/graph_weaver/testing/failure.rb +22 -22
- data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
- data/lib/graph_weaver/testing/values.rb +2 -2
- data/lib/graph_weaver/testing.rb +32 -16
- data/lib/graph_weaver/transport/faraday.rb +56 -0
- data/lib/graph_weaver/transport/http.rb +87 -0
- data/lib/graph_weaver/transport.rb +120 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +208 -38
- metadata +73 -5
- data/lib/graph_weaver/faraday_executor.rb +0 -61
- data/lib/graph_weaver/http_executor.rb +0 -44
- data/lib/graph_weaver/testing/rspec.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef7de8e2cd115b4c757a3dac2e8c41a0e87865914e246131c1037a231f7a60af
|
|
4
|
+
data.tar.gz: fa89ee78a04a2a07b7c173c82685490ca501a2a29045ef487aae3dd6f3d915d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ae73ed74389baa1f08a42e38b7bf2b0bca2f95ccbe247ccb68fda733c9fee849aed3d17dbba424bdb13104344771d9789401710a5add195c71873edc379530b
|
|
7
|
+
data.tar.gz: 24e15e7a6213c0cd5dee16aa73e675649748b84993be11387f638aeca21c15db77a59b1fa5a85ae406af7182be0f9f6a17164b842add3627a7547ab1670616d1
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,216 @@
|
|
|
1
|
+
### v0.2.0 (2026-07-12)
|
|
2
|
+
- Cleanup pass (staff-engineer review): scalar registrations get the
|
|
3
|
+
same typo validation as enums/types; cassette replay stops recomputing
|
|
4
|
+
its key per entry; dependency-order DFS uses hash bookkeeping (big
|
|
5
|
+
bool_exp graphs); require/vocabulary residue swept; the vestigial
|
|
6
|
+
graph_weaver/testing/rspec shim removed
|
|
7
|
+
- BREAKING: "client" replaces "executor" across the whole surface.
|
|
8
|
+
Generated modules: the per-call override is an optional POSITIONAL
|
|
9
|
+
first argument — PersonQuery.execute(github, id: "1") — so variables
|
|
10
|
+
own the entire kwarg namespace and NOTHING is reserved (a $client or
|
|
11
|
+
$executor variable is fine; only Ruby keywords refuse); per-module is
|
|
12
|
+
MyQuery.client=, the baked param is client:. GraphWeaver.executor= is
|
|
13
|
+
gone — GraphWeaver.client= is the one ambient slot (auto_fake swaps
|
|
14
|
+
it per example; explicit clients are self-contained and never see it).
|
|
15
|
+
Client#executor is now Client#transport (transport: to bring your
|
|
16
|
+
own); SchemaLoader.introspect/stale? speak transport. Renames:
|
|
17
|
+
FakeExecutor => Testing::FakeClient, SequenceExecutor =>
|
|
18
|
+
Testing::Sequence, RetryExecutor => GraphWeaver::Retry,
|
|
19
|
+
Recording/ReplayExecutor => Recorder/Replayer, Cassette.use(client:)
|
|
20
|
+
- Live federation integration: two Ruby subgraphs (apollo-federation
|
|
21
|
+
gem) composed and routed by a real Apollo gateway (node harness under
|
|
22
|
+
spec/support/federation), with GraphWeaver introspecting through the
|
|
23
|
+
router and executing a query stitched across BOTH subgraphs — part of
|
|
24
|
+
make integration. Complements the existing supergraph-SDL codegen spec
|
|
25
|
+
- graphql-over-http: a non-2xx response carrying a GraphQL errors body
|
|
26
|
+
(Apollo Server/Router send request errors as 4xx JSON) flows into the
|
|
27
|
+
Response envelope so QueryError sees the structured errors; only
|
|
28
|
+
non-GraphQL bodies (proxy pages) raise ServerError
|
|
29
|
+
- Fix: input-struct serialize used bare locals (result/value) that a
|
|
30
|
+
same-named prop silently shadowed — a field named "result" dropped
|
|
31
|
+
its value onto the wrong target; generated locals now wear the
|
|
32
|
+
reserved __gw prefix (GraphQL reserves __-names, so no collision is
|
|
33
|
+
possible)
|
|
34
|
+
- Input fields and variables whose Ruby name would be a keyword
|
|
35
|
+
(nil/def/end/...), a generated method (serialize/to_h), or the
|
|
36
|
+
reserved executor kwarg now refuse at generation with a pointed
|
|
37
|
+
error instead of emitting broken code
|
|
38
|
+
- Non-JSON 200 bodies (proxy error pages) classify as ServerError, and
|
|
39
|
+
unserializable variables (NaN/Infinity) raise GraphWeaver::Error —
|
|
40
|
+
raw JSON::* errors no longer escape the umbrella
|
|
41
|
+
- Transports redact on inspect/to_s (class + url only) — Authorization
|
|
42
|
+
headers can't leak through logs or exception dumps
|
|
43
|
+
- Narrowed `... on X` selections require at least one unconditional
|
|
44
|
+
field: with every field behind @skip/@include, a matching response is
|
|
45
|
+
{} — byte-identical to a non-match — so generation refuses rather
|
|
46
|
+
than silently dropping real matches to nil
|
|
47
|
+
- Integration spec against Hasura's PokeAPI: snake_case codegen,
|
|
48
|
+
recursive bool_exp variable filtering, untyped jsonb pass-through
|
|
49
|
+
(make integration)
|
|
50
|
+
- BREAKING: ValidationError now descends from GraphWeaver::Error (was
|
|
51
|
+
ArgumentError) — one `rescue GraphWeaver::Error` catches everything
|
|
52
|
+
- Input-struct .coerce raises on unknown hash keys with a spellchecked
|
|
53
|
+
hint — a typo'd filter key no longer silently drops off the wire
|
|
54
|
+
- Client registrations (register_type/register_enum) validate at the
|
|
55
|
+
call site when the schema is already loaded; lazy clients still
|
|
56
|
+
validate at generation
|
|
57
|
+
- Unregistered custom scalars emit bare T.untyped (not
|
|
58
|
+
T.nilable(T.untyped), an srb tc error under typed: strict)
|
|
59
|
+
- Wire log lines carry [req N OperationName] tags; long queries
|
|
60
|
+
(introspection) truncate at debug
|
|
61
|
+
- Logging: GraphWeaver.logger (any stdlib-compatible Logger; Rails.logger
|
|
62
|
+
auto-wired by the railtie) — wire traffic + timings at debug,
|
|
63
|
+
introspection/cache/codegen at info, every raised error at warn
|
|
64
|
+
- Recursive input types generate — self- and mutually-referential inputs
|
|
65
|
+
(Hasura's bool_exp filter surface) emit dependency-ordered structs with
|
|
66
|
+
runtime forward declarations for cycles, so variable-driven Hasura
|
|
67
|
+
filtering works; previously raised "recursive input type"
|
|
68
|
+
- Fix: snake_case GraphQL type names (Hasura, PostGraphile) camelize
|
|
69
|
+
into valid Ruby constants — pokemon_v2_pokemon => PokemonV2Pokemon
|
|
70
|
+
(previously generated a SyntaxError); wire names (__typename dispatch,
|
|
71
|
+
registry keys) are untouched
|
|
72
|
+
- Everything raised is rescuable: unparseable queries wrap as
|
|
73
|
+
ValidationError (GraphQL::ParseError no longer leaks), and internal
|
|
74
|
+
NotImplementedError raises (recursive inputs, unsupported kinds,
|
|
75
|
+
subscriptions) became GraphWeaver::Error
|
|
76
|
+
- Transport::HTTP takes open_timeout:/read_timeout: (defaults 10s/30s);
|
|
77
|
+
timeouts surface as retriable TransportError
|
|
78
|
+
- Transport::HTTP reuses its connection (keep-alive, mutex-serialized,
|
|
79
|
+
keep_alive_timeout: for the idle window); any failure drops the socket
|
|
80
|
+
so the next call starts fresh
|
|
81
|
+
- GraphQLError#code also reads a top-level "type" (GitHub's dialect:
|
|
82
|
+
NOT_FOUND, FORBIDDEN) when extensions.code is absent
|
|
83
|
+
- Typo'd client-scoped registrations raise at generation with a
|
|
84
|
+
spellchecked hint (register_type("Pett") => "did you mean 'Pet'?")
|
|
85
|
+
instead of silently no-oping
|
|
86
|
+
- Abstract selections narrow: __typename is only required when the
|
|
87
|
+
selection varies by concrete type. Interface-level-fields-only
|
|
88
|
+
selections generate one shared struct (no dispatch); a single
|
|
89
|
+
`... on X` condition generates X's struct, always nilable — a
|
|
90
|
+
non-matching runtime type casts to nil, so narrowing doubles as
|
|
91
|
+
filtering
|
|
92
|
+
- Zero-config rspec: require "graph_weaver/rspec" now defaults
|
|
93
|
+
auto_fake on and auto-locates the schema from the committed dump
|
|
94
|
+
(config.schema= / config.auto_fake = false to override) — one line is
|
|
95
|
+
the whole test setup in a conventional app
|
|
96
|
+
- examples/: runnable demos, all directly executable — countries.rb
|
|
97
|
+
(public API, no auth, all dynamic), rick_and_morty.rb (filtered
|
|
98
|
+
search, pagination, a block-built type helper), and github/ (auth,
|
|
99
|
+
checked-in generated modules; stars the repo ⭐ then tours the
|
|
100
|
+
stargazers, their top repos, and what else they've starred); excluded
|
|
101
|
+
from the gem package
|
|
102
|
+
- Fix: requires: now load before codec probing, so inference sees
|
|
103
|
+
methods the required file provides — register_scalar("DateTime", Time,
|
|
104
|
+
requires: "time") correctly infers Time.parse in a fresh process
|
|
105
|
+
(previously the cast was silently skipped unless "time" was already
|
|
106
|
+
loaded)
|
|
107
|
+
- docs/quickstart.md renamed to docs/getting_started.md
|
|
108
|
+
- Rails Railtie: the graph_weaver:* rake tasks self-register (no
|
|
109
|
+
Rakefile edit) and depend on :environment, and generated modules load
|
|
110
|
+
at boot (after initializers) when generated_path exists; outside
|
|
111
|
+
Rails, require "graph_weaver/tasks" and call load_generated! as before
|
|
112
|
+
- BREAKING (vs 0.1.0): reset_scalars! lost its coerce: flavor —
|
|
113
|
+
GraphWeaver.auto_coerce = true is the one way to default-coerce
|
|
114
|
+
(broader: convertible built-ins AND full cast/serialize scalars,
|
|
115
|
+
resolved lazily, per-registration coerce: still wins)
|
|
116
|
+
- GraphWeaver.client= — the blessed global wiring: assign the app's
|
|
117
|
+
default client and generated modules resolve through it (per call ->
|
|
118
|
+
per module -> baked -> executor= -> client). executor= stays as the
|
|
119
|
+
low-level override, so test fakes still win
|
|
120
|
+
- Enum mappings: register_enum("Species", PetKind) (+ bulk
|
|
121
|
+
register_enums, client-scoped variants) — generated code speaks YOUR
|
|
122
|
+
T::Enum, with the wire mapping inferred by name, pinned via map:,
|
|
123
|
+
exhaustiveness-checked at generation (fails naming gaps), and
|
|
124
|
+
fallback: to absorb unknown wire values on cast (inputs stay strict);
|
|
125
|
+
translation tables emitted into the source (X_FROM_WIRE / X_TO_WIRE)
|
|
126
|
+
- Type helpers: register_type("Pet", PetHelpers) (global or
|
|
127
|
+
client-scoped, additive) — app-owned modules included into every
|
|
128
|
+
struct generated from that GraphQL type, so derived values live as
|
|
129
|
+
methods beside the honest wire data and srb tc checks them against
|
|
130
|
+
each query's selection. Or build the mixin inline with a block
|
|
131
|
+
(module_eval'd into an auto-named GraphWeaver::TypeHelpers constant —
|
|
132
|
+
quick decoration, invisible to srb tc)
|
|
133
|
+
- BREAKING (vs 0.1.0): register_scalar takes the type positionally —
|
|
134
|
+
register_scalar("Money", Money, requires: ...) — matching the new
|
|
135
|
+
registrars: the GraphQL name + your Ruby type up front, options as
|
|
136
|
+
kwargs
|
|
137
|
+
- GraphWeaver::Client — transport, schema, and scalars for one server in
|
|
138
|
+
one object: GraphWeaver.new(url_or_schema) takes a url (transport
|
|
139
|
+
built, schema introspected lazily per cache:/ttl:) or a schema source
|
|
140
|
+
(live class — also the in-process executor — or a path/SDL/dump);
|
|
141
|
+
#parse and #execute/#execute! bind the implicit schema + transport;
|
|
142
|
+
#register_scalar scopes scalar mappings to the client (overlaying the
|
|
143
|
+
global registry), so two servers can disagree about a scalar type
|
|
144
|
+
- BREAKING: GraphWeaver.connect removed — GraphWeaver.new(url) replaces
|
|
145
|
+
it (wire generated modules with GraphWeaver.executor = client.executor)
|
|
146
|
+
- BREAKING: the one-shots are now GraphWeaver.execute(url_or_schema,
|
|
147
|
+
query, **variables) / execute! — Client#execute on a throwaway client;
|
|
148
|
+
variables are plain kwargs, as on a generated module
|
|
149
|
+
- Client#load_queries! — parse every query file into modules named like
|
|
150
|
+
generation would name them (reloadable; namespace: to scope): the
|
|
151
|
+
no-build-step analog of generate! + load_generated!
|
|
152
|
+
- Introspected schema dumps record provenance (source url + timestamp):
|
|
153
|
+
a parsable SDL header comment, a "graph_weaver" sibling key in JSON —
|
|
154
|
+
read it back with SchemaLoader.provenance(path), check drift with
|
|
155
|
+
SchemaLoader.stale?(path) or rake graph_weaver:schema:verify, rewrite
|
|
156
|
+
with rake graph_weaver:schema:refresh (GRAPHWEAVER_AUTH for tokens)
|
|
157
|
+
- generate!/verify_generated!/rake auto-locate the schema dump at
|
|
158
|
+
schema_path in any supported format; SchemaLoader.locate is public
|
|
159
|
+
- Calling a result field by its camelCase wire name raises a pointed
|
|
160
|
+
NoMethodError naming the snake_case prop that does exist
|
|
161
|
+
(result.addPet => "use 'add_pet'"), and near-miss typos in either
|
|
162
|
+
casing get a spellchecked suggestion (result.addPt => "did you mean
|
|
163
|
+
'add_pet'?") — the runtime companion to srb tc's static flag
|
|
164
|
+
- BREAKING: an operation whose only variable is a required input object
|
|
165
|
+
(the Relay convention) now flattens the input's fields into execute's
|
|
166
|
+
kwargs — AdoptQuery.execute!(name:, species:) instead of
|
|
167
|
+
execute!(input: {...}); multi-variable / nullable-input operations
|
|
168
|
+
keep the input: kwarg (struct or hash)
|
|
169
|
+
- Enum kwargs accept the T::Enum or its wire value (T.any(Enum, String))
|
|
170
|
+
everywhere — variables now match input-hash fields
|
|
171
|
+
- BREAKING: HttpExecutor / FaradayExecutor are now Transport::HTTP /
|
|
172
|
+
Transport::Faraday, subclasses of the new abstract GraphWeaver::Transport
|
|
173
|
+
base, which owns the shared flow (encode, TransportError reclassify,
|
|
174
|
+
non-2xx ServerError, parse) — a custom transport just implements
|
|
175
|
+
post(body) => [status, body]. Opt-in require moved:
|
|
176
|
+
"graph_weaver/faraday_executor" -> "graph_weaver/transport/faraday"
|
|
177
|
+
- SchemaLoader.introspect cache: reuses a fresh dump in ANY supported
|
|
178
|
+
format before re-introspecting (an existing schema.graphql wins over
|
|
179
|
+
writing schema.json), and accepts :json / :graphql / :gql to pick the
|
|
180
|
+
format at GraphWeaver.schema_path's location
|
|
181
|
+
- rubydoc.info rendering: ship .yardopts (markdown markup, docs/ guides
|
|
182
|
+
as extra files) and re-indent docstring examples so code blocks and
|
|
183
|
+
backticks render; make docs previews locally
|
|
184
|
+
- GraphWeaver.connect(url, auth:, headers:, retries:): one-shot setup —
|
|
185
|
+
best transport (Faraday when the app loads it; detection is defined?,
|
|
186
|
+
never a require), bearer/verbatim auth, opt-in RetryExecutor wrapping
|
|
187
|
+
(true / options Hash; off by default), wired in as the global executor
|
|
188
|
+
- Generation workflow: GraphWeaver.generate! (queries dir -> generated
|
|
189
|
+
dir), verify_generated! (the freshness guard — raises naming stale
|
|
190
|
+
files), load_generated! (factory_bot-style explicit loading), rake
|
|
191
|
+
tasks (require "graph_weaver/tasks": graph_weaver:generate / :verify),
|
|
192
|
+
all defaulting to configurable conventional paths (queries_path /
|
|
193
|
+
generated_path / schema_path)
|
|
194
|
+
- GraphWeaver.auto_coerce = true: default input coercion for scalars
|
|
195
|
+
without an explicit coerce:, resolved lazily at generation time (no
|
|
196
|
+
reset_scalars! ordering dance) — convertible built-ins take their
|
|
197
|
+
conversion, cast/serialize pairs take parse-style coercion
|
|
198
|
+
- SchemaLoader.introspect cache: true — caches at GraphWeaver.schema_path,
|
|
199
|
+
in the format the extension picks: .json (verbatim wire artifact) or
|
|
200
|
+
.graphql/.gql (SDL — human-readable, PR-reviewable diffs);
|
|
201
|
+
the same dump rake graph_weaver:generate reads
|
|
202
|
+
- docs/transports.md: connect, the executor contract, Faraday, retries
|
|
203
|
+
- Cassette workflow: GRAPHWEAVER_RECORD=1 / config.record force
|
|
204
|
+
re-recording; config.anonymize scrubs responses as they are recorded
|
|
205
|
+
(caller sees the anonymized data too, so assertions hold on replay);
|
|
206
|
+
rake graph_weaver:cassettes:anonymize; docs/cassettes.md guide
|
|
207
|
+
- auto_coerce reaches input-object fields: raw scalar values inside
|
|
208
|
+
input hashes coerce via the registry, mutations included
|
|
209
|
+
- RetryExecutor: composable retries over any transport — tries:,
|
|
210
|
+
exponential/linear/custom backoff with jitter and max clamp,
|
|
211
|
+
retry-by-error-class (5xx yes, 4xx no by default; retry_if: override)
|
|
212
|
+
and retry-by-GraphQL-code (retry_codes: ["THROTTLED"])
|
|
213
|
+
|
|
1
214
|
### v0.1.0 (2026-07-11)
|
|
2
215
|
- Structured errors: execute returns a typed Response envelope (#data/#data!,
|
|
3
216
|
#errors, #errors?, #extensions) instead of raising on GraphQL errors, so
|
data/Gemfile.lock
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
graph_weaver (0.
|
|
4
|
+
graph_weaver (0.2.0)
|
|
5
5
|
graphql (>= 2)
|
|
6
6
|
sorbet-runtime
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
+
apollo-federation (3.10.3)
|
|
12
|
+
google-protobuf (< 5)
|
|
13
|
+
graphql
|
|
11
14
|
base64 (0.3.0)
|
|
12
15
|
benchmark (0.5.0)
|
|
13
16
|
bigdecimal (4.1.2)
|
|
@@ -28,6 +31,18 @@ GEM
|
|
|
28
31
|
faraday-net_http (3.4.4)
|
|
29
32
|
net-http (~> 0.5)
|
|
30
33
|
fiber-storage (1.0.1)
|
|
34
|
+
google-protobuf (4.35.1-aarch64-linux-gnu)
|
|
35
|
+
bigdecimal
|
|
36
|
+
rake (~> 13.3)
|
|
37
|
+
google-protobuf (4.35.1-arm64-darwin)
|
|
38
|
+
bigdecimal
|
|
39
|
+
rake (~> 13.3)
|
|
40
|
+
google-protobuf (4.35.1-x86_64-darwin)
|
|
41
|
+
bigdecimal
|
|
42
|
+
rake (~> 13.3)
|
|
43
|
+
google-protobuf (4.35.1-x86_64-linux-gnu)
|
|
44
|
+
bigdecimal
|
|
45
|
+
rake (~> 13.3)
|
|
31
46
|
graphql (2.6.5)
|
|
32
47
|
base64
|
|
33
48
|
fiber-storage
|
|
@@ -50,6 +65,7 @@ GEM
|
|
|
50
65
|
prettyprint
|
|
51
66
|
prettyprint (0.2.0)
|
|
52
67
|
prism (1.9.0)
|
|
68
|
+
rake (13.4.2)
|
|
53
69
|
rbi (0.3.14)
|
|
54
70
|
prism (~> 1.0)
|
|
55
71
|
rbs (>= 4.0.1)
|
|
@@ -62,6 +78,7 @@ GEM
|
|
|
62
78
|
prism (>= 1.6.0)
|
|
63
79
|
rbs (>= 4.0.0)
|
|
64
80
|
tsort
|
|
81
|
+
redcarpet (3.6.1)
|
|
65
82
|
reline (0.6.3)
|
|
66
83
|
io-console (~> 0.5)
|
|
67
84
|
require-hooks (0.4.0)
|
|
@@ -122,6 +139,7 @@ GEM
|
|
|
122
139
|
tsort (0.2.0)
|
|
123
140
|
uri (1.1.1)
|
|
124
141
|
webrick (1.9.2)
|
|
142
|
+
yard (0.9.44)
|
|
125
143
|
|
|
126
144
|
PLATFORMS
|
|
127
145
|
aarch64-linux
|
|
@@ -131,18 +149,23 @@ PLATFORMS
|
|
|
131
149
|
x86_64-linux
|
|
132
150
|
|
|
133
151
|
DEPENDENCIES
|
|
152
|
+
apollo-federation
|
|
134
153
|
bigdecimal
|
|
135
154
|
debug
|
|
136
155
|
faker
|
|
137
156
|
faraday
|
|
138
157
|
graph_weaver!
|
|
158
|
+
rake
|
|
159
|
+
redcarpet
|
|
139
160
|
rspec
|
|
140
161
|
simplecov
|
|
141
162
|
sorbet
|
|
142
163
|
tapioca
|
|
143
164
|
webrick
|
|
165
|
+
yard
|
|
144
166
|
|
|
145
167
|
CHECKSUMS
|
|
168
|
+
apollo-federation (3.10.3) sha256=61e6d8e1fd695ee45bab5d84f86f586d99c296ad77a86c6f1e7b964ac17be3d7
|
|
146
169
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
147
170
|
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
|
148
171
|
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
@@ -156,7 +179,11 @@ CHECKSUMS
|
|
|
156
179
|
faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278
|
|
157
180
|
faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588
|
|
158
181
|
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
|
|
159
|
-
|
|
182
|
+
google-protobuf (4.35.1-aarch64-linux-gnu) sha256=50ca44d0eeff3f8475e630a1accdd974256f3510694d574e2c9d6119ea8bc9e1
|
|
183
|
+
google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b
|
|
184
|
+
google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee
|
|
185
|
+
google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd
|
|
186
|
+
graph_weaver (0.2.0)
|
|
160
187
|
graphql (2.6.5) sha256=1051825bfb4aedb4293cdbcd9726c3150e69e3512556609a32c2ca19d4be3d00
|
|
161
188
|
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
|
|
162
189
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
|
@@ -169,9 +196,11 @@ CHECKSUMS
|
|
|
169
196
|
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
|
|
170
197
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
171
198
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
199
|
+
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
172
200
|
rbi (0.3.14) sha256=932ed904adbb5ca645c285c8b36da1b7ff6321ec1870d187d6325248cb637658
|
|
173
201
|
rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14
|
|
174
202
|
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
|
203
|
+
redcarpet (3.6.1) sha256=d444910e6aa55480c6bcdc0cdb057626e8a32c054c29e793fa642ba2f155f445
|
|
175
204
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
176
205
|
require-hooks (0.4.0) sha256=005f4c6435b4edae73e358cdbaba48370a4121f9ce893d5d2a3c66fce855677d
|
|
177
206
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
@@ -199,6 +228,7 @@ CHECKSUMS
|
|
|
199
228
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
200
229
|
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
|
201
230
|
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
|
|
231
|
+
yard (0.9.44) sha256=eb087e9b631ccd887b049f303d489963945452d5e2a7eb49a5a74a7cf6887f28
|
|
202
232
|
|
|
203
233
|
BUNDLED WITH
|
|
204
234
|
4.0.10
|
data/Makefile
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
.PHONY: check generate test tc integration
|
|
1
|
+
.PHONY: check generate test tc integration docs
|
|
2
2
|
|
|
3
3
|
# full verify loop: regenerate, test, typecheck
|
|
4
4
|
check: generate test tc
|
|
5
5
|
|
|
6
6
|
# manual/one-off checks against real GraphQL APIs (network; GitHub needs
|
|
7
|
-
# `gh auth login` or GITHUB_TOKEN)
|
|
7
|
+
# `gh auth login` or GITHUB_TOKEN; federation boots an Apollo gateway — node)
|
|
8
8
|
integration:
|
|
9
9
|
INTEGRATION=1 bundle exec rspec spec/integration
|
|
10
10
|
|
|
@@ -16,3 +16,8 @@ test:
|
|
|
16
16
|
|
|
17
17
|
tc:
|
|
18
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
CHANGED
data/PLAN.md
CHANGED
|
@@ -26,7 +26,7 @@ requires:, opt-in input coercion incl. built-ins).
|
|
|
26
26
|
Sources: live schema / introspection JSON / SDL — byte-identical output
|
|
27
27
|
(enum values + abstract-type members sorted for determinism).
|
|
28
28
|
Transport: executor precedence per call → per module → baked const →
|
|
29
|
-
GraphWeaver.executor;
|
|
29
|
+
GraphWeaver.executor; Transport::HTTP (zero-dep) + opt-in Transport::Faraday
|
|
30
30
|
(url / block middleware / ready connection), e2e specs against WEBrick.
|
|
31
31
|
Errors: typed Response envelope (partial data + extensions survive) and
|
|
32
32
|
a GraphWeaver::Error hierarchy (Transport/Server/Query/Validation/Type)
|
|
@@ -102,3 +102,36 @@ Response envelope; execute! for raise-or-result).
|
|
|
102
102
|
resolvers — codegen must stay name-keyed, never call schema runtime hooks
|
|
103
103
|
- graphql-client (the gem) casts scalars via coerce_isolated_input and
|
|
104
104
|
only with a live schema class — documented in the early specs
|
|
105
|
+
|
|
106
|
+
## From the field-test experiments (2026-07-12)
|
|
107
|
+
|
|
108
|
+
A junior + senior agent pair exercised the repo cold (clone, examples,
|
|
109
|
+
extensions, a Pokedex app against Hasura's 4,441-type PokeAPI schema).
|
|
110
|
+
Fixed same-day: snake_case type names generated invalid constants
|
|
111
|
+
(camelize), GraphQL::ParseError/NotImplementedError escaping the Error
|
|
112
|
+
umbrella, HttpExecutor timeouts, GitHub's top-level "type" error codes,
|
|
113
|
+
typo'd client registrations silently no-oping. Still open:
|
|
114
|
+
|
|
115
|
+
~~Recursive input types~~ DONE 2026-07-12: register-before-walk breaks
|
|
116
|
+
the recursion; emission dependency-orders the structs and, for cycles,
|
|
117
|
+
forward-declares the classes in an eval (srb rejects reopening a
|
|
118
|
+
T::Struct statically, but adding props at runtime works — the full
|
|
119
|
+
bodies below the eval are all srb sees). Verified live against Hasura
|
|
120
|
+
bool_exp.
|
|
121
|
+
|
|
122
|
+
~~Connection reuse in Transport::HTTP~~ DONE 2026-07-12: persistent
|
|
123
|
+
keep-alive connection behind a mutex, dropped on any failure; net/http's
|
|
124
|
+
keep_alive_timeout handles idle expiry. Faraday remains the pooling
|
|
125
|
+
answer.
|
|
126
|
+
|
|
127
|
+
- Subscriptions; @defer/@stream (routers send multipart responses — the
|
|
128
|
+
transport classifies them as ServerError today, no incremental support).
|
|
129
|
+
- Shared input-type structs across generated modules: one Hasura
|
|
130
|
+
bool_exp variable pulls its whole recursive closure into EVERY module
|
|
131
|
+
(~28k lines each) — correct but heavy in PRs; needs cross-module
|
|
132
|
+
sharing or selection-based pruning (field-test round 2).
|
|
133
|
+
- Structured logging: log_tag pairs lines and names operations now, but
|
|
134
|
+
events are prose — an optional {event:, url:, ms:} payload contract, a
|
|
135
|
+
scrub_variables hook, and cache-age on hit lines (field-test round 2).
|
|
136
|
+
- Cut 0.1.1 — RubyGems 0.1.0 is materially behind main (snake_case fix,
|
|
137
|
+
recursive inputs, keep-alive, logging, error umbrella).
|
data/README.md
CHANGED
|
@@ -26,85 +26,105 @@ result.person&.birthday # => Date (custom scalars deserialize)
|
|
|
26
26
|
result.person&.nmae # => srb tc: Method `nmae` does not exist
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
New here? The **[getting started](docs/getting_started.md)** guide walks the
|
|
30
|
+
production setup end to end — initializer, codegen, fakes, CI. Or run the
|
|
31
|
+
**[examples](examples/)**, smallest first: `examples/countries.rb` (public
|
|
32
|
+
API, no auth, all dynamic), `examples/rick_and_morty.rb` (filtering,
|
|
33
|
+
pagination, a block-built type helper), and `examples/github/run.rb`
|
|
34
|
+
(auth + checked-in generated modules; it stars this repo ⭐ and introduces
|
|
35
|
+
you to your fellow stargazers).
|
|
36
|
+
|
|
29
37
|
#### Features
|
|
30
38
|
|
|
31
39
|
- **Queries and mutations** with typed variable kwargs — enums as `T::Enum`s, input objects as `T::Struct`s, required vs optional falling out of nullability and defaults
|
|
32
40
|
- **Fragments** (inline, named, type conditions), **unions and interfaces** (member structs, `__typename` dispatch), **custom scalars** (pluggable registry), `@skip`/`@include` nullability
|
|
33
41
|
- **Any schema source**: live schema class, introspection JSON, or SDL — including Apollo Federation supergraph SDL; introspect live endpoints with caching
|
|
34
|
-
- **Any transport**: in-process schema execution, the zero-dependency HTTP executor, or Faraday with your own middleware — swap per call with `executor:`
|
|
42
|
+
- **Any transport**: in-process schema execution, the zero-dependency HTTP executor, or Faraday with your own middleware — plus a composable `Retry` (exponential/linear/custom backoff, jitter, retry-by-error-class or GraphQL code) — swap per call with `executor:`
|
|
35
43
|
- **Structured errors**: a typed response envelope (partial data + extensions survive), an error hierarchy split by failure site, field-level reports with entity ids, and `schema_stale?` detection — every error dual-surfaced as a human message plus JSON-ready `#to_h`
|
|
36
44
|
- **Testing built in**: schema-correct fakes, failure simulation, record/replay cassettes with anonymization, rspec integration
|
|
37
45
|
- **Dynamic mode** for development: `GraphWeaver.parse(...)` generates and evals on the fly, no build step
|
|
38
46
|
|
|
39
47
|
#### Usage
|
|
40
48
|
|
|
49
|
+
Three ways to run a query — pick by context:
|
|
50
|
+
|
|
51
|
+
| Context | Use |
|
|
52
|
+
|---------|-----|
|
|
53
|
+
| Production | checked-in codegen (`rake graph_weaver:generate`) — reviewed, `srb tc`-checked |
|
|
54
|
+
| Development, consoles | `client.parse` / `client.load_queries!` — no build step |
|
|
55
|
+
| Scripts, one-offs | `client.execute!` — no module at all |
|
|
56
|
+
|
|
57
|
+
The production path assembled is the [getting started](docs/getting_started.md);
|
|
58
|
+
the pieces:
|
|
59
|
+
|
|
41
60
|
```ruby
|
|
42
61
|
require "graph_weaver"
|
|
43
62
|
|
|
44
|
-
#
|
|
45
|
-
|
|
63
|
+
# a client for one server: transport (Faraday when loaded), auth, and a
|
|
64
|
+
# lazily introspected schema. The first argument is a url or any schema
|
|
65
|
+
# source — a live schema class, or a .json/.graphql dump
|
|
66
|
+
api = GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"], cache: true)
|
|
46
67
|
|
|
47
|
-
#
|
|
48
|
-
|
|
68
|
+
# make it the app default — generated modules execute through it
|
|
69
|
+
GraphWeaver.client = api
|
|
49
70
|
|
|
71
|
+
# generate checked-in typed modules (rake graph_weaver:generate, or directly)
|
|
50
72
|
source = GraphWeaver::Codegen.generate(
|
|
51
|
-
schema
|
|
73
|
+
schema: api.schema,
|
|
52
74
|
query: File.read("queries/person.graphql"),
|
|
53
75
|
module_name: "PersonQuery",
|
|
54
76
|
)
|
|
55
77
|
File.write("app/queries/person_query.rb", source)
|
|
56
78
|
|
|
57
79
|
# at runtime
|
|
58
|
-
PersonQuery.execute(id: "1") #
|
|
59
|
-
PersonQuery.execute(id: "1"
|
|
80
|
+
PersonQuery.execute(id: "1") # via GraphWeaver.client
|
|
81
|
+
PersonQuery.execute(other_client, id: "1") # or per call
|
|
60
82
|
```
|
|
61
83
|
|
|
62
84
|
Module names derive from the operation name (`query GetPerson` →
|
|
63
|
-
`GetPerson`) or, for `
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
`GetPerson`) or, for `parse` on a `.graphql` file, from the file name;
|
|
86
|
+
pass `module_name:`/`name:` to override. Pass `client:` (a constant) to
|
|
87
|
+
bake a default client into the generated module. Prefer Faraday? It's
|
|
88
|
+
opt-in (`gem "faraday"`), and the client picks it up when loaded —
|
|
89
|
+
middleware blocks and ready connections in [transports](docs/transports.md).
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
require "graph_weaver/faraday_executor"
|
|
71
|
-
|
|
72
|
-
# from a url, with optional middleware customization
|
|
73
|
-
executor = GraphWeaver::FaradayExecutor.new("https://api.example.com/graphql") do |conn|
|
|
74
|
-
conn.request :authorization, "Bearer", -> { Tokens.fetch }
|
|
75
|
-
conn.response :logger
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# or bring a fully configured Faraday connection
|
|
79
|
-
executor = GraphWeaver::FaradayExecutor.new(MyApp.faraday_connection)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
In development, skip the build step entirely:
|
|
91
|
+
In development, skip the build step entirely — modules from `client.parse`
|
|
92
|
+
carry the client's transport, no global wiring needed:
|
|
83
93
|
|
|
84
94
|
```ruby
|
|
85
95
|
# parse a query into a typed module on the fly — a .graphql path or a raw string
|
|
86
|
-
PersonQuery =
|
|
87
|
-
PeopleQuery = GraphWeaver.parse(schema:, query: "query { people { name } }")
|
|
96
|
+
PersonQuery = api.parse("queries/person.graphql")
|
|
88
97
|
PersonQuery.execute(id: "1")
|
|
89
98
|
|
|
90
|
-
# or
|
|
91
|
-
|
|
99
|
+
# or every query file at once (queries_path convention), named like generation would
|
|
100
|
+
api.load_queries!
|
|
101
|
+
|
|
102
|
+
# or one-shot, no module at all — variables are plain kwargs
|
|
103
|
+
api.execute!("query($id: ID!) { person(id: $id) { name } }", id: "1")
|
|
92
104
|
```
|
|
93
105
|
|
|
94
106
|
|
|
95
107
|
#### Dig deeper
|
|
96
108
|
|
|
109
|
+
- **[Getting started](docs/getting_started.md)** — the production path in Rails,
|
|
110
|
+
step by step: initializer, rake tasks, fakes, CI, Sorbet or not
|
|
97
111
|
- **[Generated modules](docs/generated_modules.md)** — module anatomy, typed
|
|
98
112
|
variables (enums, input objects), fragments/unions/interfaces,
|
|
99
|
-
`@skip`/`@include`, naming,
|
|
100
|
-
- **[Against a real API](docs/real_world.md)** —
|
|
101
|
-
(GitHub end to end), schema caching
|
|
113
|
+
`@skip`/`@include`, naming, clients, dynamic mode
|
|
114
|
+
- **[Against a real API](docs/real_world.md)** — the exploratory tour:
|
|
115
|
+
introspect a live endpoint (GitHub end to end), dynamic mode, schema caching
|
|
116
|
+
- **[Transports](docs/transports.md)** — clients, the execute contract,
|
|
117
|
+
Faraday, retries and backoff
|
|
102
118
|
- **[Custom scalars](docs/scalars.md)** — the registry: codec inference,
|
|
103
119
|
requires, input coercion
|
|
104
120
|
- **[Errors](docs/errors.md)** — the Response envelope, the error hierarchy,
|
|
105
121
|
field-level reports with entity ids, stale-schema detection
|
|
122
|
+
- **[Logging](docs/logging.md)** — point `GraphWeaver.logger` at any Logger:
|
|
123
|
+
wire traffic at debug, introspection/cache/codegen at info, errors at warn
|
|
106
124
|
- **[Testing](docs/testing.md)** — schema-correct fakes, failure simulation,
|
|
107
|
-
|
|
125
|
+
rspec integration
|
|
126
|
+
- **[Cassettes](docs/cassettes.md)** — capture and replay real API
|
|
127
|
+
responses; anonymized recording (`GRAPHWEAVER_RECORD=1`, rake tasks)
|
|
108
128
|
|
|
109
129
|
----
|
|
110
130
|
## Installation
|
|
@@ -116,7 +136,7 @@ gem "graph_weaver"
|
|
|
116
136
|
|
|
117
137
|
or
|
|
118
138
|
|
|
119
|
-
```
|
|
139
|
+
```sh
|
|
120
140
|
gem install graph_weaver
|
|
121
141
|
```
|
|
122
142
|
|
|
@@ -125,8 +145,3 @@ gem install graph_weaver
|
|
|
125
145
|
|
|
126
146
|
- `make check` — regenerate spec fixtures, run specs, typecheck
|
|
127
147
|
- `make integration` — one-off checks against the live GitHub and Countries APIs
|
|
128
|
-
|
|
129
|
-
See `PLAN.md` for roadmap and `NOTES.md` for the research notebook this
|
|
130
|
-
gem grew out of (an exploration of graphql-client internals — GraphWeaver
|
|
131
|
-
is a standalone client, not an extension; it depends only on `graphql`
|
|
132
|
-
and `sorbet-runtime`).
|
data/docs/cassettes.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Cassettes: capture and replay
|
|
2
|
+
|
|
3
|
+
Cassettes record real API responses and replay them in tests — above the
|
|
4
|
+
transport (a client wrapping a client), so there's no HTTP
|
|
5
|
+
interception and they work identically over HTTP, Faraday, or in-process
|
|
6
|
+
execution. A cassette is a YAML file of `{query, variables, response}`
|
|
7
|
+
entries, matched on the normalized query + variables.
|
|
8
|
+
|
|
9
|
+
## The workflow
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# spec: replay when the cassette exists, record against `live` when not
|
|
13
|
+
cassette = GraphWeaver::Testing::Cassette.use("github", client: live)
|
|
14
|
+
result = RepoQuery.execute!(cassette, owner: "dpep", name: "graph_weaver")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
1. **Record** — first run hits the live API and writes
|
|
18
|
+
`spec/cassettes/github.yml` (`Testing.config.cassette_dir` resolves bare
|
|
19
|
+
names).
|
|
20
|
+
2. **Anonymize** — cassettes hold real data; scrub before committing (below).
|
|
21
|
+
3. **Commit** — tests now run offline, fast, deterministic.
|
|
22
|
+
4. **Re-record** when the API's real behavior changes:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
GRAPHWEAVER_RECORD=1 bundle exec rspec # every Cassette.use records afresh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
(`Testing.config.record = true` is the programmatic equivalent.)
|
|
29
|
+
|
|
30
|
+
Replaying an unrecorded request raises `MissingRecording` with the query
|
|
31
|
+
and the path — no silent fabrication.
|
|
32
|
+
|
|
33
|
+
## Anonymization
|
|
34
|
+
|
|
35
|
+
Anonymizing rewrites recorded values through the same engine
|
|
36
|
+
[FakeClient](testing.md) uses, while preserving everything that makes
|
|
37
|
+
the recording faithful:
|
|
38
|
+
|
|
39
|
+
| preserved | replaced |
|
|
40
|
+
|-----------|----------|
|
|
41
|
+
| shape: keys, list lengths, null positions | strings (semantically: emails look like emails) |
|
|
42
|
+
| enums, booleans, `__typename` | numbers, dates |
|
|
43
|
+
| id *relationships* (same original id → same fake id) | the id values themselves |
|
|
44
|
+
|
|
45
|
+
Three ways to run it:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# 1. as recordings happen — assertions you write against the recording
|
|
49
|
+
# run hold on replay, and real data never touches disk
|
|
50
|
+
GraphWeaver::Testing.configure do |config|
|
|
51
|
+
config.schema = MySchema
|
|
52
|
+
config.anonymize = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 2. after the fact, per cassette
|
|
56
|
+
GraphWeaver::Testing::Cassette.new("spec/cassettes/github.yml").anonymize!(schema:)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
# 3. the whole cassette_dir at once
|
|
61
|
+
rake graph_weaver:cassettes:anonymize
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Anonymization needs the schema (it walks each recorded query's selections
|
|
65
|
+
to know which values are enums, dates, ids...). Variables are NOT
|
|
66
|
+
anonymized — they're the replay matching key; don't record with secret
|
|
67
|
+
variables.
|
|
68
|
+
|
|
69
|
+
## When to use what
|
|
70
|
+
|
|
71
|
+
- **FakeClient** — no recording needed; schema-correct random data.
|
|
72
|
+
Best default for unit tests.
|
|
73
|
+
- **Cassettes** — real response *shapes* from a real API (pagination
|
|
74
|
+
quirks, actual union members, servers' null habits). Best for
|
|
75
|
+
integration-ish tests and regression pinning.
|
|
76
|
+
- **Anonymized cassettes** — cassette fidelity, committable without PII.
|