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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +213 -0
  4. data/Gemfile.lock +32 -2
  5. data/Makefile +7 -2
  6. data/NOTES.md +1 -1
  7. data/PLAN.md +34 -1
  8. data/README.md +56 -41
  9. data/docs/cassettes.md +76 -0
  10. data/docs/errors.md +37 -9
  11. data/docs/generated_modules.md +139 -29
  12. data/docs/getting_started.md +134 -0
  13. data/docs/logging.md +33 -0
  14. data/docs/real_world.md +27 -20
  15. data/docs/scalars.md +122 -8
  16. data/docs/testing.md +44 -34
  17. data/docs/transports.md +124 -0
  18. data/graph_weaver.gemspec +7 -1
  19. data/lib/graph_weaver/client.rb +200 -0
  20. data/lib/graph_weaver/codegen/emit.rb +88 -39
  21. data/lib/graph_weaver/codegen/enum_type.rb +149 -0
  22. data/lib/graph_weaver/codegen/nodes.rb +109 -9
  23. data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
  24. data/lib/graph_weaver/codegen.rb +301 -67
  25. data/lib/graph_weaver/errors.rb +37 -25
  26. data/lib/graph_weaver/hints.rb +63 -0
  27. data/lib/graph_weaver/inflect.rb +2 -1
  28. data/lib/graph_weaver/logging.rb +41 -0
  29. data/lib/graph_weaver/railtie.rb +29 -0
  30. data/lib/graph_weaver/retry.rb +97 -0
  31. data/lib/graph_weaver/rspec.rb +19 -14
  32. data/lib/graph_weaver/schema_loader.rb +156 -20
  33. data/lib/graph_weaver/selection.rb +3 -3
  34. data/lib/graph_weaver/tasks.rb +71 -0
  35. data/lib/graph_weaver/testing/cassette.rb +42 -21
  36. data/lib/graph_weaver/testing/failure.rb +22 -22
  37. data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
  38. data/lib/graph_weaver/testing/values.rb +2 -2
  39. data/lib/graph_weaver/testing.rb +32 -16
  40. data/lib/graph_weaver/transport/faraday.rb +56 -0
  41. data/lib/graph_weaver/transport/http.rb +87 -0
  42. data/lib/graph_weaver/transport.rb +120 -0
  43. data/lib/graph_weaver/version.rb +1 -1
  44. data/lib/graph_weaver.rb +208 -38
  45. metadata +73 -5
  46. data/lib/graph_weaver/faraday_executor.rb +0 -61
  47. data/lib/graph_weaver/http_executor.rb +0 -44
  48. data/lib/graph_weaver/testing/rspec.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f5ca284d12ebab15746910afb16a7349344b21fae0f93ad77882a3d256d4a75
4
- data.tar.gz: 76e19939e81d70da81aa93da515168cdf670b65fca6d36d0412d6104589cf83d
3
+ metadata.gz: ef7de8e2cd115b4c757a3dac2e8c41a0e87865914e246131c1037a231f7a60af
4
+ data.tar.gz: fa89ee78a04a2a07b7c173c82685490ca501a2a29045ef487aae3dd6f3d915d8
5
5
  SHA512:
6
- metadata.gz: b1404192f58233afd6074365e006b355cca9d9d5aefb554fd318d76430cea089e36311d69a8941e8120e4e07e6333b597ae0bf09fdb4737823ae22b4bb4ad878
7
- data.tar.gz: d02a7b9e61dad2c234a4a89ac9761d493781f49eb730516d09a328dc28d3e276f4f4d68c9cbf97d468d8cec1bfddd718f0d7fce61614fd4cbd1f4dee210745be
6
+ metadata.gz: 4ae73ed74389baa1f08a42e38b7bf2b0bca2f95ccbe247ccb68fda733c9fee849aed3d17dbba424bdb13104344771d9789401710a5add195c71873edc379530b
7
+ data.tar.gz: 24e15e7a6213c0cd5dee16aa73e675649748b84993be11387f638aeca21c15db77a59b1fa5a85ae406af7182be0f9f6a17164b842add3627a7547ab1670616d1
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --markup markdown
2
+ --title GraphWeaver
3
+ -
4
+ docs/*.md
5
+ CHANGELOG.md
6
+ LICENSE
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.1.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
- graph_weaver (0.1.0)
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
@@ -25,7 +25,7 @@ order, each backed by a spec.
25
25
 
26
26
  The specs are the documentation — each one asserts an observed behavior:
27
27
 
28
- ```
28
+ ```sh
29
29
  bundle exec rspec
30
30
  ```
31
31
 
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; HttpExecutor (zero-dep) + opt-in FaradayExecutor
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
- # configure the default transport once (override per module or per call)
45
- GraphWeaver.executor = GraphWeaver::HttpExecutor.new("https://api.example.com/graphql")
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
- # generate from any schema source
48
- schema = GraphWeaver::SchemaLoader.load("schema.json") # or .graphql SDL, or a live class
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") # uses GraphWeaver.executor
59
- PersonQuery.execute(id: "1", executor: other) # or per call
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 `GraphWeaver.parse` on a `.graphql` file, from the
64
- file name; pass `module_name:`/`name:` to override. Pass `executor:` (a
65
- constant) to bake a default transport into the generated module.
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
- Prefer Faraday? It's opt-in (`gem "faraday"` in your Gemfile):
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 = GraphWeaver.parse(schema:, query: "queries/person.graphql")
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 one-shot, no module at all
91
- GraphWeaver.execute(schema:, query: "query($id: ID!) { person(id: $id) { name } }", variables: { id: "1" })
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, executors, dynamic mode
100
- - **[Against a real API](docs/real_world.md)** — introspect a live endpoint
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
- cassettes with anonymized capture, rspec integration
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.