graph_weaver 0.0.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df338e7f577c07e2941daf8e7a06af9a04e04ecfceae6c02a7e6d6619e219a0f
4
- data.tar.gz: 939e9c9db69ed23b484ebd60401b96e5b51fea10aa6ee0b4e6c9a62118243302
3
+ metadata.gz: 8f5ca284d12ebab15746910afb16a7349344b21fae0f93ad77882a3d256d4a75
4
+ data.tar.gz: 76e19939e81d70da81aa93da515168cdf670b65fca6d36d0412d6104589cf83d
5
5
  SHA512:
6
- metadata.gz: 3eaf7a927dbde9d9eac7353a8d01623a099e5904c458113274bf347524d00567a8d0fdd3794312f2931feb9f0a7fbd212eccfdfc9b09671e646cbfd1a8d35294
7
- data.tar.gz: cc4ae772c0c38f3cb547d37ddc88821a1eb9172d8df42c4b0608242c0df19ca4ad669e246c72806fc53701be95fa7a4a4945723725075812ecf4ec602bcd63f0
6
+ metadata.gz: b1404192f58233afd6074365e006b355cca9d9d5aefb554fd318d76430cea089e36311d69a8941e8120e4e07e6333b597ae0bf09fdb4737823ae22b4bb4ad878
7
+ data.tar.gz: d02a7b9e61dad2c234a4a89ac9761d493781f49eb730516d09a328dc28d3e276f4f4d68c9cbf97d468d8cec1bfddd718f0d7fce61614fd4cbd1f4dee210745be
data/CHANGELOG.md CHANGED
@@ -1,3 +1,69 @@
1
+ ### v0.1.0 (2026-07-11)
2
+ - Structured errors: execute returns a typed Response envelope (#data/#data!,
3
+ #errors, #errors?, #extensions) instead of raising on GraphQL errors, so
4
+ partial data and top-level extensions (cost/throttle) survive. Error classes
5
+ under GraphWeaver::Error — TransportError (network), ServerError (non-2xx
6
+ HTTP, #status/#body), QueryError (#errors/#data/#extensions/#codes),
7
+ ValidationError (build-time) — plus a GraphQLError value object with #code.
8
+ Transport-error classification is an extensible Set (GraphWeaver.transport_errors
9
+ / register_transport_error): each transport seeds its own network exceptions
10
+ and apps can add more (e.g. a connection-pool timeout).
11
+ The envelope is a single generic GraphWeaver::Response[Result] (no per-query
12
+ wrapper class). execute! is the shortcut for execute(...).data! — the typed
13
+ result or a raised QueryError — on both generated modules and the one-shot
14
+ GraphWeaver.execute!/execute.
15
+ BREAKING: module #execute returns Response; use #execute! (or #data!) for
16
+ the old raise-or-result behavior. GraphWeaver.execute now returns the
17
+ envelope too; GraphWeaver.execute! returns the result.
18
+ - GraphWeaver.register_scalar: custom scalar deserialization into rich Ruby
19
+ objects. cast/serialize inferred from a class type via paired codecs
20
+ (.parse/#to_s or .load/.dump), or given as a Symbol/Proc (:itself opts out);
21
+ requires: emits (validated, and require-checked when type: is a class)
22
+ requires into generated source — the built-in Date scalar carries
23
+ require "date" so Date-using queries are self-contained; coerce: true lets a
24
+ variable accept the value or its raw input (coerce: :to_f for a built-in
25
+ conversion), casting/converting the latter — reset_scalars!(coerce: true)
26
+ reloads the built-ins coercible; built-in scalars pre-registered in one
27
+ overridable registry (reset_scalars!/clear_scalars!)
28
+ - FaradayExecutor: url, Faraday connection, or middleware block
29
+ - GraphWeaver.executor default transport; per-module executor= override
30
+ - GraphWeaver.parse and GraphWeaver.execute (dynamic queries)
31
+ - Codegen.generate shorthand; executor: takes a constant; module_name
32
+ derived from operation or file name
33
+ - Error ergonomics: schema_stale? (validation-shaped rejections hint at
34
+ regeneration), errors_at(path) + each_error/errors_by_field filtering,
35
+ #report (field-keyed rollup with entity ids resolved from partial
36
+ data), #to_h across the hierarchy (JSON-ready machine output), and
37
+ GraphWeaver::TypeError wrapping cast failures with the failing struct
38
+ - SchemaLoader: introspect(executor, cache:, ttl:) fetches schemas from
39
+ live endpoints with file caching; load accepts introspection JSON /
40
+ SDL content / Hashes as well as paths (cache round-trips)
41
+ - GraphWeaver::Testing (require "graph_weaver/testing", or
42
+ "graph_weaver/rspec" for the rspec integration): FakeExecutor
43
+ fabricates schema-correct castable responses (mode: :faker semantic
44
+ values / :literal; overrides by GraphQL name; seeded; list_size /
45
+ null_chance), failure simulation (Failure.transport/server/graphql/
46
+ throttled/stale_schema, SequenceExecutor for retries, fail_at: with
47
+ spec-correct null propagation, corrupt: for derived type mismatches),
48
+ cassette record/replay above the transport, and Cassette#anonymize!
49
+ (shape-preserving, consistent id mapping). rspec: seed follows
50
+ --seed; auto_fake installs a fake executor per example
51
+ - one-off integration specs against live GitHub + Countries APIs
52
+ (make integration)
53
+ - Input objects: INPUT_OBJECT variables generate module-level T::Structs
54
+ with serialize (aliased to_h) producing the wire hash; execute kwargs
55
+ also accept plain hashes, normalized + type-checked via the generated
56
+ .coerce (underscored Symbol/String keys, enums as instances or wire
57
+ values, nested inputs as hashes)
58
+ - fields under @skip/@include generate nilable regardless of schema
59
+ nullability; FakeExecutor honors first/last/limit when sizing lists
60
+ - eval hardening for parse: module names must be constant names, and
61
+ QUERY heredocs can't be terminated early by block strings
62
+ - GraphWeaver::Selection: one shared query-walk (codegen, FakeExecutor,
63
+ anonymizer); codegen split into scalar_type / nodes / emit
64
+ - docs/: generated_modules, real_world, scalars, errors, testing;
65
+ README slimmed to pitch + quickstart
66
+
1
67
  ### v0.0.1 (2026-07-07)
2
68
  - voila: typed codegen (T::Structs, T::Enums, typed variable kwargs)
3
69
  - queries + mutations; fragments, unions, interfaces, enums, custom scalars
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graph_weaver (0.0.1)
4
+ graph_weaver (0.1.0)
5
5
  graphql (>= 2)
6
6
  sorbet-runtime
7
7
 
@@ -10,6 +10,8 @@ GEM
10
10
  specs:
11
11
  base64 (0.3.0)
12
12
  benchmark (0.5.0)
13
+ bigdecimal (4.1.2)
14
+ concurrent-ruby (1.3.7)
13
15
  debug (1.11.1)
14
16
  irb (~> 1.10)
15
17
  reline (>= 0.3.8)
@@ -17,18 +19,31 @@ GEM
17
19
  docile (1.4.1)
18
20
  erb (6.0.4)
19
21
  erubi (1.13.1)
22
+ faker (3.8.0)
23
+ i18n (>= 1.8.11, < 2)
24
+ faraday (2.14.3)
25
+ faraday-net_http (>= 2.0, < 3.5)
26
+ json
27
+ logger
28
+ faraday-net_http (3.4.4)
29
+ net-http (~> 0.5)
20
30
  fiber-storage (1.0.1)
21
31
  graphql (2.6.5)
22
32
  base64
23
33
  fiber-storage
24
34
  logger
35
+ i18n (1.15.2)
36
+ concurrent-ruby (~> 1.0)
25
37
  io-console (0.8.2)
26
38
  irb (1.18.0)
27
39
  pp (>= 0.6.0)
28
40
  prism (>= 1.3.0)
29
41
  rdoc (>= 4.0.0)
30
42
  reline (>= 0.4.2)
43
+ json (2.20.0)
31
44
  logger (1.7.0)
45
+ net-http (0.9.1)
46
+ uri (>= 0.11.1)
32
47
  netrc (0.11.0)
33
48
  parallel (2.1.0)
34
49
  pp (0.6.4)
@@ -74,15 +89,15 @@ GEM
74
89
  simplecov_json_formatter (~> 0.1)
75
90
  simplecov-html (0.13.2)
76
91
  simplecov_json_formatter (0.1.4)
77
- sorbet (0.6.13323)
78
- sorbet-static (= 0.6.13323)
79
- sorbet-runtime (0.6.13323)
80
- sorbet-static (0.6.13323-aarch64-linux)
81
- sorbet-static (0.6.13323-universal-darwin)
82
- sorbet-static (0.6.13323-x86_64-linux)
83
- sorbet-static-and-runtime (0.6.13323)
84
- sorbet (= 0.6.13323)
85
- sorbet-runtime (= 0.6.13323)
92
+ sorbet (0.6.13327)
93
+ sorbet-static (= 0.6.13327)
94
+ sorbet-runtime (0.6.13327)
95
+ sorbet-static (0.6.13327-aarch64-linux)
96
+ sorbet-static (0.6.13327-universal-darwin)
97
+ sorbet-static (0.6.13327-x86_64-linux)
98
+ sorbet-static-and-runtime (0.6.13327)
99
+ sorbet (= 0.6.13327)
100
+ sorbet-runtime (= 0.6.13327)
86
101
  spoom (1.8.3)
87
102
  erubi (>= 1.10.0)
88
103
  prism (>= 0.28.0)
@@ -105,6 +120,7 @@ GEM
105
120
  tsort
106
121
  thor (1.5.0)
107
122
  tsort (0.2.0)
123
+ uri (1.1.1)
108
124
  webrick (1.9.2)
109
125
 
110
126
  PLATFORMS
@@ -115,7 +131,10 @@ PLATFORMS
115
131
  x86_64-linux
116
132
 
117
133
  DEPENDENCIES
134
+ bigdecimal
118
135
  debug
136
+ faker
137
+ faraday
119
138
  graph_weaver!
120
139
  rspec
121
140
  simplecov
@@ -126,17 +145,25 @@ DEPENDENCIES
126
145
  CHECKSUMS
127
146
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
128
147
  benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
148
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
149
+ concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
129
150
  debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
130
151
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
131
152
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
132
153
  erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
133
154
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
155
+ faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc
156
+ faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278
157
+ faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588
134
158
  fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
135
- graph_weaver (0.0.1)
159
+ graph_weaver (0.1.0)
136
160
  graphql (2.6.5) sha256=1051825bfb4aedb4293cdbcd9726c3150e69e3512556609a32c2ca19d4be3d00
161
+ i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
137
162
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
138
163
  irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
164
+ json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b
139
165
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
166
+ net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
140
167
  netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
141
168
  parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
142
169
  pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
@@ -160,16 +187,17 @@ CHECKSUMS
160
187
  simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
161
188
  simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
162
189
  simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
163
- sorbet (0.6.13323) sha256=78b2d4b17fe171b2facd636033ce6e0be8544aa776b7713216209ba015d911d3
164
- sorbet-runtime (0.6.13323) sha256=cf9c4e6793e1a9ef69bd96e3a006e0fd1fead429f4e4370163460bf569a23518
165
- sorbet-static (0.6.13323-aarch64-linux) sha256=33d428073b491970b1b2528d6a89c4f104fdaf7f8752c6489f120040faafc5c4
166
- sorbet-static (0.6.13323-universal-darwin) sha256=35ea7cca7011adea28fd7aa18002aff7f242365d829ca31219ad5e723fe28270
167
- sorbet-static (0.6.13323-x86_64-linux) sha256=8ce81180e0e91394a87bf39722ff797f90315d71e29559f015716b3512347373
168
- sorbet-static-and-runtime (0.6.13323) sha256=04b636933a28c8adedc81ad469f25d3a59b8a3eceaeef9accd6a92e2066d461d
190
+ sorbet (0.6.13327) sha256=4b56cb04542661c6bf5075f1ca38ca00506045325fd689f460aee8466dd3d7a3
191
+ sorbet-runtime (0.6.13327) sha256=86f32b2df6d266bf5c0e0eb8ea81d0376ba36eece4c8610938174e403222948d
192
+ sorbet-static (0.6.13327-aarch64-linux) sha256=da8a575785d34641095631c50fe1d1f645ee8782c8e2aa5a0ecfd12d1fa22a8a
193
+ sorbet-static (0.6.13327-universal-darwin) sha256=a2a8c6b624aada3f6ef68852398de756248f71b3ea691119873515847010dcda
194
+ sorbet-static (0.6.13327-x86_64-linux) sha256=f3a915bab4af1979145c793a08d3b1db207526d5e2f332513da10a95e70f9028
195
+ sorbet-static-and-runtime (0.6.13327) sha256=7b039be023b6a7d2d17b1f751191f32307d9ace4170b6a192bb0e005d222e00e
169
196
  spoom (1.8.3) sha256=32871fa189bbfa49cf557a50f819f23cc9a6ceefd0346caa7a6adc193becd5dd
170
197
  tapioca (0.19.2) sha256=938731b07811aee8d23871b1aee8861d464fbaf2cfffbf79a62b0c869a5120ec
171
198
  thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
172
199
  tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
200
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
173
201
  webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
174
202
 
175
203
  BUNDLED WITH
data/Makefile CHANGED
@@ -1,8 +1,13 @@
1
- .PHONY: check generate test tc
1
+ .PHONY: check generate test tc integration
2
2
 
3
3
  # full verify loop: regenerate, test, typecheck
4
4
  check: generate test tc
5
5
 
6
+ # manual/one-off checks against real GraphQL APIs (network; GitHub needs
7
+ # `gh auth login` or GITHUB_TOKEN)
8
+ integration:
9
+ INTEGRATION=1 bundle exec rspec spec/integration
10
+
6
11
  generate:
7
12
  bundle exec ruby bin/generate
8
13
 
data/PLAN.md CHANGED
@@ -1,7 +1,7 @@
1
- # Project Plan — typed GraphQL codegen for Ruby/Sorbet
1
+ # Project Plan — GraphWeaver, typed GraphQL client for Ruby/Sorbet
2
2
 
3
- _Resume-from-here notes. The README is the lab notebook (findings); this is
4
- the plan. Update both when state changes._
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
5
 
6
6
  ## Vision
7
7
 
@@ -12,21 +12,40 @@ shape of every query result. Dynamic (eval) mode for development, build
12
12
  step for CI/static checking. Runtime deps: graphql + sorbet-runtime only
13
13
  (graphql-client is NOT a dependency; the exploration outgrew it).
14
14
 
15
- ## State: working prototype, all green
15
+ ## State: v0.0.1 on rubygems; v0.0.2 accumulating on main
16
16
 
17
- `bundle exec rspec` (35 examples) + `bundle exec srb tc` + `bin/generate`
18
- (regenerates lib/generated/ from queries/; parity specs enforce freshness).
17
+ `make check` = bin/generate (spec fixture regeneration; parity specs
18
+ enforce freshness) + rspec + srb tc.
19
19
 
20
- Language coverage: queries, mutations, typed variables (kwargs on execute,
21
- optional-when-defaulted, enum/scalar serialization), fragments (inline,
22
- named, interface conditions), union- AND interface-typed fields
23
- (__typename dispatch, required at generation time), enums (T::Enum),
24
- custom scalars (name-keyed SCALAR_CASTS / SCALAR_SERIALIZERS registries).
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).
25
26
  Sources: live schema / introspection JSON / SDL — byte-identical output
26
27
  (enum values + abstract-type members sorted for determinism).
27
- Transport: executor: kwarg in-process schema or HttpExecutor (e2e spec
28
- against WEBrick). Federation supergraph SDL loads transparently (needs
28
+ Transport: executor precedence per call per module baked const →
29
+ GraphWeaver.executor; HttpExecutor (zero-dep) + opt-in FaradayExecutor
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
29
47
  directive_defaults_patch until upstream fix ships).
48
+ Live validation: make integration (GitHub + Countries APIs).
30
49
 
31
50
  ## Next steps (in rough order)
32
51
 
@@ -38,21 +57,32 @@ NOTES.md. Prior-art check partially answered: graphql-client PR #7
38
57
  Jan 2024 with users asking; schema-wide typing can't catch
39
58
  unfetched-field bugs or type unions/interfaces — the niche looks open.
40
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
+
41
65
  1. Stable class naming design — names come from GraphQL type names per
42
66
  selection site; must not shift when unrelated selections are added
43
67
  (generated code is app-code API). Current: one-level field-name
44
- disambiguation, then raise.
45
- 2. Input objects as variables (raise NotImplementedError today) — likely
46
- generated T::Structs with serialize.
47
- 3. CLI entrypoint (graph_weaver generate --schema X --queries dir) —
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) —
48
71
  bin/generate is spec-fixture tooling, not shipped.
49
- 4. Subscriptions (unsupported; raise).
50
- 5. First release: 0.1.0 to rubygems once naming design settles.
51
- 6. Nice-to-haves: __typename auto-injection (currently required manually
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
52
82
  on abstract selections), fragment reuse across queries, directives on
53
83
  selections (@skip/@include make non-null fields nullable).
54
- 7. Tapioca DSL compiler over dynamic mode (idea from graphql-client
55
- PR #7): RBI the GraphWeaver::Codegen.load-eval'd modules so development mode
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
56
86
  gets static types without the bin/generate build step — tapioca is
57
87
  already in every Sorbet shop's workflow. Upstream's
58
88
  Tapioca::Dsl::Helpers::GraphqlTypeHelper is prior art for type mapping.
data/README.md CHANGED
@@ -19,49 +19,93 @@ query($id: ID!) {
19
19
  ```
20
20
 
21
21
  ```ruby
22
- result = PersonQuery.execute(id: "1")
22
+ result = PersonQuery.execute!(id: "1") # typed result, or raises on errors (execute returns an envelope)
23
23
 
24
24
  result.person&.name # => "Daniel" (typed String)
25
25
  result.person&.birthday # => Date (custom scalars deserialize)
26
26
  result.person&.nmae # => srb tc: Method `nmae` does not exist
27
27
  ```
28
28
 
29
- #### Features
29
+ #### Features
30
30
 
31
- - **Queries and mutations** with typed variable kwargs — required vs optional falls out of nullability and defaults
32
- - **Fragments** (inline, named, interface conditions), **unions and interfaces** (member structs, `__typename` dispatch), **enums** (`T::Enum`), **custom scalars** (pluggable registry)
33
- - **Any schema source**: live schema class, introspection JSON, or SDL — including Apollo Federation supergraph SDL
34
- - **Any transport**: in-process schema execution (perfect for tests) or HTTP via the bundled executor — swap per call with `executor:`
35
- - **Dynamic mode** for development: `GraphWeaver::Codegen.load(...)` generates and evals on the fly, no build step
31
+ - **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
+ - **Fragments** (inline, named, type conditions), **unions and interfaces** (member structs, `__typename` dispatch), **custom scalars** (pluggable registry), `@skip`/`@include` nullability
33
+ - **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:`
35
+ - **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
+ - **Testing built in**: schema-correct fakes, failure simulation, record/replay cassettes with anonymization, rspec integration
37
+ - **Dynamic mode** for development: `GraphWeaver.parse(...)` generates and evals on the fly, no build step
36
38
 
37
- #### Usage
39
+ #### Usage
38
40
 
39
41
  ```ruby
40
42
  require "graph_weaver"
41
43
 
44
+ # configure the default transport once (override per module or per call)
45
+ GraphWeaver.executor = GraphWeaver::HttpExecutor.new("https://api.example.com/graphql")
46
+
42
47
  # generate from any schema source
43
48
  schema = GraphWeaver::SchemaLoader.load("schema.json") # or .graphql SDL, or a live class
44
49
 
45
- source = GraphWeaver::Codegen.new(
50
+ source = GraphWeaver::Codegen.generate(
46
51
  schema:,
47
- executor_const: "MyApi::Executor",
48
52
  query: File.read("queries/person.graphql"),
49
53
  module_name: "PersonQuery",
50
- ).generate
51
-
54
+ )
52
55
  File.write("app/queries/person_query.rb", source)
53
56
 
54
57
  # at runtime
55
- executor = GraphWeaver::HttpExecutor.new("https://api.example.com/graphql")
56
- PersonQuery.execute(id: "1", executor:)
58
+ PersonQuery.execute(id: "1") # uses GraphWeaver.executor
59
+ PersonQuery.execute(id: "1", executor: other) # or per call
57
60
  ```
58
61
 
59
- In development, skip the build step:
62
+ 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.
66
+
67
+ Prefer Faraday? It's opt-in (`gem "faraday"` in your Gemfile):
60
68
 
61
69
  ```ruby
62
- PersonQuery = GraphWeaver::Codegen.load(schema:, executor_const: "...", query:, module_name: "PersonQuery")
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)
63
80
  ```
64
81
 
82
+ In development, skip the build step entirely:
83
+
84
+ ```ruby
85
+ # 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 } }")
88
+ PersonQuery.execute(id: "1")
89
+
90
+ # or one-shot, no module at all
91
+ GraphWeaver.execute(schema:, query: "query($id: ID!) { person(id: $id) { name } }", variables: { id: "1" })
92
+ ```
93
+
94
+
95
+ #### Dig deeper
96
+
97
+ - **[Generated modules](docs/generated_modules.md)** — module anatomy, typed
98
+ 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
102
+ - **[Custom scalars](docs/scalars.md)** — the registry: codec inference,
103
+ requires, input coercion
104
+ - **[Errors](docs/errors.md)** — the Response envelope, the error hierarchy,
105
+ field-level reports with entity ids, stale-schema detection
106
+ - **[Testing](docs/testing.md)** — schema-correct fakes, failure simulation,
107
+ cassettes with anonymized capture, rspec integration
108
+
65
109
  ----
66
110
  ## Installation
67
111
 
@@ -79,7 +123,8 @@ gem install graph_weaver
79
123
  ----
80
124
  ## Development
81
125
 
82
- `make check` — regenerate spec fixtures, run specs, typecheck.
126
+ - `make check` — regenerate spec fixtures, run specs, typecheck
127
+ - `make integration` — one-off checks against the live GitHub and Countries APIs
83
128
 
84
129
  See `PLAN.md` for roadmap and `NOTES.md` for the research notebook this
85
130
  gem grew out of (an exploration of graphql-client internals — GraphWeaver
data/docs/errors.md ADDED
@@ -0,0 +1,106 @@
1
+ # Errors
2
+
3
+ `execute` returns a typed **`Response` envelope** rather than raising on GraphQL
4
+ errors — so partial data and top-level `extensions` (cost, throttle) survive.
5
+ `execute!` is the shortcut when you just want the result:
6
+
7
+ ```ruby
8
+ PersonQuery.execute!(id: "1") # => Result, or raises QueryError (== execute(...).data!)
9
+
10
+ response = PersonQuery.execute(id: "1") # => GraphWeaver::Response[Result]
11
+ response.data # T.nilable(Result) — typed, present even on partial success
12
+ response.errors # Array[GraphWeaver::GraphQLError]
13
+ response.errors? # any top-level errors?
14
+ response.extensions # { "cost" => … } — rides on success too
15
+ response.data! # the Result, or raise GraphWeaver::QueryError
16
+ ```
17
+
18
+ The envelope is a single generic `GraphWeaver::Response[Result]` — `response.data`
19
+ stays fully typed to *this* query's result, no per-query wrapper class.
20
+
21
+ Every `GraphQLError` exposes `#message`, `#locations`, `#path`, `#extensions`,
22
+ and `#code` (`extensions["code"]`) — match on the **code**, not the message
23
+ string (`response.errors.first.code == "THROTTLED"`).
24
+
25
+ Everything GraphWeaver raises descends from `GraphWeaver::Error`, split by where
26
+ it failed:
27
+
28
+ | Class | When |
29
+ |-------|------|
30
+ | `TransportError` | never reached the server — DNS, connection refused, TLS, timeout |
31
+ | `ServerError` | reached it, non-2xx HTTP — `#status`, `#body` |
32
+ | `QueryError` | 200 body with top-level GraphQL errors — `#errors`, `#data`, `#extensions`, `#codes` |
33
+ | `ValidationError` | build time: the query didn't validate against the schema (also an `ArgumentError`) |
34
+
35
+ ```ruby
36
+ begin
37
+ person = PersonQuery.execute!(id: "1").person
38
+ rescue GraphWeaver::TransportError
39
+ retry # network blip
40
+ rescue GraphWeaver::ServerError => e
41
+ raise if e.status < 500 # backoff on 5xx only
42
+ rescue GraphWeaver::QueryError => e
43
+ e.codes.include?("THROTTLED") ? backoff : raise
44
+ end
45
+ ```
46
+
47
+ What counts as a `TransportError` is an **extensible set** — each transport
48
+ seeds its own network exceptions (`Errno::*`, `SocketError`, timeouts, TLS; the
49
+ Faraday executor adds its own), and you can register more so a custom adapter's
50
+ or connection pool's failure gets the same treatment:
51
+
52
+ ```ruby
53
+ GraphWeaver.register_transport_error(ConnectionPool::TimeoutError)
54
+ GraphWeaver.transport_errors << MyAdapter::ResetError # it's just a Set
55
+ ```
56
+
57
+ Business/validation failures returned *as data* (Shopify-style `userErrors { field
58
+ message code }`) aren't errors here — they're just fields you selected, so they
59
+ deserialize onto `response.data` like anything else and you inspect them there.
60
+
61
+ The one-shot `GraphWeaver.execute` / `execute!` mirror this: `execute` returns
62
+ the envelope, `execute!` the result-or-raise.
63
+
64
+
65
+ ## Programmatic surfacing
66
+
67
+ Every error is dual-surface: `#message` for humans, `#to_h` for machines — a
68
+ JSON-ready hash (error class, per-error `path`/`code`/`locations`/`extensions`)
69
+ you can nest straight into a log line or an API response.
70
+
71
+ Field-level tooling lives on both `Response` and `QueryError`:
72
+
73
+ ```ruby
74
+ response.errors_at("person.email") # errors touching a path (prefix match)
75
+ response.each_error do |field, errors| # grouped by index-stripped field
76
+ form.add_error(field, errors.map(&:message))
77
+ end
78
+
79
+ response.report
80
+ # { "person.pets.name" => {
81
+ # "messages" => ["name hidden"], "codes" => ["PRIVATE"],
82
+ # "entity_ids" => ["7", "9"], # resolved by walking paths through partial data
83
+ # "errors" => [ ...full to_h detail... ] },
84
+ # nil => { "codes" => ["DOWN"], ... } } # global errors under nil
85
+ ```
86
+
87
+ `GraphQLError#field` strips list indices (`people.3.email` → `people.email`) —
88
+ the stable grouping key; the raw `#path` keeps indices for exact location.
89
+
90
+ ## Stale schemas
91
+
92
+ GraphQL has no schema-version signal, so a schema change surfaces as the
93
+ server rejecting your query's shape. `response.schema_stale?` /
94
+ `QueryError#schema_stale?` detect validation-shaped rejections (Apollo's
95
+ `GRAPHQL_VALIDATION_FAILED` code, or the message patterns graphql-ruby and
96
+ GitHub use), and the raised message says what to do: regenerate modules and/or
97
+ refresh the schema cache.
98
+
99
+ ## Cast failures
100
+
101
+ When wire data disagrees with the types the schema promised at generation time
102
+ (a nil where non-null was declared, a malformed scalar, an unknown enum value),
103
+ casting raises `GraphWeaver::TypeError` naming the failing generated struct,
104
+ with the original exception as `#cause`. Simulate one in tests with
105
+ `FakeExecutor.new(schema:, corrupt: "Person.birthday")` — see
106
+ [testing](testing.md).