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 +4 -4
- data/CHANGELOG.md +66 -0
- data/Gemfile.lock +45 -17
- data/Makefile +6 -1
- data/PLAN.md +52 -22
- data/README.md +62 -17
- data/docs/errors.md +106 -0
- data/docs/generated_modules.md +119 -0
- data/docs/real_world.md +46 -0
- data/docs/scalars.md +69 -0
- data/docs/testing.md +93 -0
- data/graph_weaver.gemspec +3 -0
- data/lib/graph_weaver/codegen/emit.rb +237 -0
- data/lib/graph_weaver/codegen/nodes.rb +256 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +238 -0
- data/lib/graph_weaver/codegen.rb +156 -395
- data/lib/graph_weaver/errors.rb +310 -0
- data/lib/graph_weaver/faraday_executor.rb +61 -0
- data/lib/graph_weaver/http_executor.rb +16 -3
- data/lib/graph_weaver/inflect.rb +18 -0
- data/lib/graph_weaver/response.rb +55 -0
- data/lib/graph_weaver/rspec.rb +54 -0
- data/lib/graph_weaver/schema_loader.rb +73 -9
- data/lib/graph_weaver/selection.rb +68 -0
- data/lib/graph_weaver/testing/cassette.rb +203 -0
- data/lib/graph_weaver/testing/failure.rb +109 -0
- data/lib/graph_weaver/testing/fake_executor.rb +228 -0
- data/lib/graph_weaver/testing/rspec.rb +5 -0
- data/lib/graph_weaver/testing/values.rb +98 -0
- data/lib/graph_weaver/testing.rb +90 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +97 -3
- metadata +63 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f5ca284d12ebab15746910afb16a7349344b21fae0f93ad77882a3d256d4a75
|
|
4
|
+
data.tar.gz: 76e19939e81d70da81aa93da515168cdf670b65fca6d36d0412d6104589cf83d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
78
|
-
sorbet-static (= 0.6.
|
|
79
|
-
sorbet-runtime (0.6.
|
|
80
|
-
sorbet-static (0.6.
|
|
81
|
-
sorbet-static (0.6.
|
|
82
|
-
sorbet-static (0.6.
|
|
83
|
-
sorbet-static-and-runtime (0.6.
|
|
84
|
-
sorbet (= 0.6.
|
|
85
|
-
sorbet-runtime (= 0.6.
|
|
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
|
|
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.
|
|
164
|
-
sorbet-runtime (0.6.
|
|
165
|
-
sorbet-static (0.6.
|
|
166
|
-
sorbet-static (0.6.
|
|
167
|
-
sorbet-static (0.6.
|
|
168
|
-
sorbet-static-and-runtime (0.6.
|
|
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
|
|
1
|
+
# Project Plan — GraphWeaver, typed GraphQL client for Ruby/Sorbet
|
|
2
2
|
|
|
3
|
-
_Resume-from-here notes.
|
|
4
|
-
the plan. Update
|
|
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:
|
|
15
|
+
## State: v0.0.1 on rubygems; v0.0.2 accumulating on main
|
|
16
16
|
|
|
17
|
-
`
|
|
18
|
-
|
|
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
|
|
21
|
-
optional-when-defaulted
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
PR #7): RBI the GraphWeaver
|
|
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
|
-
####
|
|
29
|
+
#### Features
|
|
30
30
|
|
|
31
|
-
- **Queries and mutations** with typed variable kwargs — required vs optional
|
|
32
|
-
- **Fragments** (inline, named,
|
|
33
|
-
- **Any schema source**: live schema class, introspection JSON, or SDL — including Apollo Federation supergraph SDL
|
|
34
|
-
- **Any transport**: in-process schema execution
|
|
35
|
-
- **
|
|
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
|
-
####
|
|
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.
|
|
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
|
-
)
|
|
51
|
-
|
|
54
|
+
)
|
|
52
55
|
File.write("app/queries/person_query.rb", source)
|
|
53
56
|
|
|
54
57
|
# at runtime
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|