graph_weaver 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +176 -0
- data/LICENSE.txt +21 -0
- data/Makefile +13 -0
- data/NOTES.md +182 -0
- data/PLAN.md +74 -0
- data/README.md +87 -0
- data/graph_weaver.gemspec +24 -0
- data/lib/graph_weaver/codegen.rb +567 -0
- data/lib/graph_weaver/directive_defaults_patch.rb +32 -0
- data/lib/graph_weaver/http_executor.rb +31 -0
- data/lib/graph_weaver/schema_loader.rb +20 -0
- data/lib/graph_weaver/version.rb +3 -0
- data/lib/graph_weaver.rb +11 -0
- metadata +166 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: df338e7f577c07e2941daf8e7a06af9a04e04ecfceae6c02a7e6d6619e219a0f
|
|
4
|
+
data.tar.gz: 939e9c9db69ed23b484ebd60401b96e5b51fea10aa6ee0b4e6c9a62118243302
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3eaf7a927dbde9d9eac7353a8d01623a099e5904c458113274bf347524d00567a8d0fdd3794312f2931feb9f0a7fbd212eccfdfc9b09671e646cbfd1a8d35294
|
|
7
|
+
data.tar.gz: cc4ae772c0c38f3cb547d37ddc88821a1eb9172d8df42c4b0608242c0df19ca4ad669e246c72806fc53701be95fa7a4a4945723725075812ecf4ec602bcd63f0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
### v0.0.1 (2026-07-07)
|
|
2
|
+
- voila: typed codegen (T::Structs, T::Enums, typed variable kwargs)
|
|
3
|
+
- queries + mutations; fragments, unions, interfaces, enums, custom scalars
|
|
4
|
+
- schema sources: live class, introspection JSON, SDL (incl. supergraph)
|
|
5
|
+
- pluggable executor: in-process schema or HTTP
|
|
6
|
+
- dynamic (no-build) mode for development
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
graph_weaver (0.0.1)
|
|
5
|
+
graphql (>= 2)
|
|
6
|
+
sorbet-runtime
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
base64 (0.3.0)
|
|
12
|
+
benchmark (0.5.0)
|
|
13
|
+
debug (1.11.1)
|
|
14
|
+
irb (~> 1.10)
|
|
15
|
+
reline (>= 0.3.8)
|
|
16
|
+
diff-lcs (1.6.2)
|
|
17
|
+
docile (1.4.1)
|
|
18
|
+
erb (6.0.4)
|
|
19
|
+
erubi (1.13.1)
|
|
20
|
+
fiber-storage (1.0.1)
|
|
21
|
+
graphql (2.6.5)
|
|
22
|
+
base64
|
|
23
|
+
fiber-storage
|
|
24
|
+
logger
|
|
25
|
+
io-console (0.8.2)
|
|
26
|
+
irb (1.18.0)
|
|
27
|
+
pp (>= 0.6.0)
|
|
28
|
+
prism (>= 1.3.0)
|
|
29
|
+
rdoc (>= 4.0.0)
|
|
30
|
+
reline (>= 0.4.2)
|
|
31
|
+
logger (1.7.0)
|
|
32
|
+
netrc (0.11.0)
|
|
33
|
+
parallel (2.1.0)
|
|
34
|
+
pp (0.6.4)
|
|
35
|
+
prettyprint
|
|
36
|
+
prettyprint (0.2.0)
|
|
37
|
+
prism (1.9.0)
|
|
38
|
+
rbi (0.3.14)
|
|
39
|
+
prism (~> 1.0)
|
|
40
|
+
rbs (>= 4.0.1)
|
|
41
|
+
rbs (4.0.3)
|
|
42
|
+
logger
|
|
43
|
+
prism (>= 1.6.0)
|
|
44
|
+
tsort
|
|
45
|
+
rdoc (8.0.0)
|
|
46
|
+
erb
|
|
47
|
+
prism (>= 1.6.0)
|
|
48
|
+
rbs (>= 4.0.0)
|
|
49
|
+
tsort
|
|
50
|
+
reline (0.6.3)
|
|
51
|
+
io-console (~> 0.5)
|
|
52
|
+
require-hooks (0.4.0)
|
|
53
|
+
rexml (3.4.4)
|
|
54
|
+
rspec (3.13.2)
|
|
55
|
+
rspec-core (~> 3.13.0)
|
|
56
|
+
rspec-expectations (~> 3.13.0)
|
|
57
|
+
rspec-mocks (~> 3.13.0)
|
|
58
|
+
rspec-core (3.13.6)
|
|
59
|
+
rspec-support (~> 3.13.0)
|
|
60
|
+
rspec-expectations (3.13.5)
|
|
61
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
62
|
+
rspec-support (~> 3.13.0)
|
|
63
|
+
rspec-mocks (3.13.8)
|
|
64
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
65
|
+
rspec-support (~> 3.13.0)
|
|
66
|
+
rspec-support (3.13.7)
|
|
67
|
+
rubydex (0.2.7-aarch64-linux)
|
|
68
|
+
rubydex (0.2.7-arm64-darwin)
|
|
69
|
+
rubydex (0.2.7-x86_64-darwin)
|
|
70
|
+
rubydex (0.2.7-x86_64-linux)
|
|
71
|
+
simplecov (0.22.0)
|
|
72
|
+
docile (~> 1.1)
|
|
73
|
+
simplecov-html (~> 0.11)
|
|
74
|
+
simplecov_json_formatter (~> 0.1)
|
|
75
|
+
simplecov-html (0.13.2)
|
|
76
|
+
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)
|
|
86
|
+
spoom (1.8.3)
|
|
87
|
+
erubi (>= 1.10.0)
|
|
88
|
+
prism (>= 0.28.0)
|
|
89
|
+
rbi (>= 0.3.14)
|
|
90
|
+
rbs (>= 4.0.0.dev.5)
|
|
91
|
+
rexml (>= 3.2.6)
|
|
92
|
+
sorbet-static-and-runtime (>= 0.5.10187)
|
|
93
|
+
thor (>= 0.19.2)
|
|
94
|
+
tapioca (0.19.2)
|
|
95
|
+
benchmark
|
|
96
|
+
bundler (>= 2.2.25)
|
|
97
|
+
netrc (>= 0.11.0)
|
|
98
|
+
parallel (>= 1.21.0)
|
|
99
|
+
rbi (>= 0.3.7)
|
|
100
|
+
require-hooks (>= 0.2.2)
|
|
101
|
+
rubydex (>= 0.1.0.beta10)
|
|
102
|
+
sorbet-static-and-runtime (>= 0.6.12698)
|
|
103
|
+
spoom (>= 1.7.16)
|
|
104
|
+
thor (>= 1.2.0)
|
|
105
|
+
tsort
|
|
106
|
+
thor (1.5.0)
|
|
107
|
+
tsort (0.2.0)
|
|
108
|
+
webrick (1.9.2)
|
|
109
|
+
|
|
110
|
+
PLATFORMS
|
|
111
|
+
aarch64-linux
|
|
112
|
+
arm64-darwin
|
|
113
|
+
universal-darwin
|
|
114
|
+
x86_64-darwin
|
|
115
|
+
x86_64-linux
|
|
116
|
+
|
|
117
|
+
DEPENDENCIES
|
|
118
|
+
debug
|
|
119
|
+
graph_weaver!
|
|
120
|
+
rspec
|
|
121
|
+
simplecov
|
|
122
|
+
sorbet
|
|
123
|
+
tapioca
|
|
124
|
+
webrick
|
|
125
|
+
|
|
126
|
+
CHECKSUMS
|
|
127
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
128
|
+
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
|
129
|
+
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
|
|
130
|
+
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
131
|
+
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
132
|
+
erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
|
|
133
|
+
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
|
134
|
+
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
|
|
135
|
+
graph_weaver (0.0.1)
|
|
136
|
+
graphql (2.6.5) sha256=1051825bfb4aedb4293cdbcd9726c3150e69e3512556609a32c2ca19d4be3d00
|
|
137
|
+
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
|
138
|
+
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
|
|
139
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
140
|
+
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
|
|
141
|
+
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
|
142
|
+
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
|
|
143
|
+
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
144
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
145
|
+
rbi (0.3.14) sha256=932ed904adbb5ca645c285c8b36da1b7ff6321ec1870d187d6325248cb637658
|
|
146
|
+
rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14
|
|
147
|
+
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
|
148
|
+
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
149
|
+
require-hooks (0.4.0) sha256=005f4c6435b4edae73e358cdbaba48370a4121f9ce893d5d2a3c66fce855677d
|
|
150
|
+
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
151
|
+
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
152
|
+
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
153
|
+
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
154
|
+
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
|
155
|
+
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
|
156
|
+
rubydex (0.2.7-aarch64-linux) sha256=9a016039f7e94e55cd812e073d310de7effcce4cac560edd1185e1ce6b5b4eb0
|
|
157
|
+
rubydex (0.2.7-arm64-darwin) sha256=f0d28bbf4153568be79b671642424750053e0bea971b60ddf5cec19bf4563990
|
|
158
|
+
rubydex (0.2.7-x86_64-darwin) sha256=b002b259d118ac69de44470eff1597143318402c45630c47371f9542631447dc
|
|
159
|
+
rubydex (0.2.7-x86_64-linux) sha256=dacfade9fa42ce4469618da6dac07e69d5f3ac6a313b4caced5234c8f052419a
|
|
160
|
+
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
|
161
|
+
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
|
162
|
+
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
|
|
169
|
+
spoom (1.8.3) sha256=32871fa189bbfa49cf557a50f819f23cc9a6ceefd0346caa7a6adc193becd5dd
|
|
170
|
+
tapioca (0.19.2) sha256=938731b07811aee8d23871b1aee8861d464fbaf2cfffbf79a62b0c869a5120ec
|
|
171
|
+
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
|
172
|
+
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
173
|
+
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
|
|
174
|
+
|
|
175
|
+
BUNDLED WITH
|
|
176
|
+
4.0.10
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Pepper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/Makefile
ADDED
data/NOTES.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Research notes — graphql-client exploration → GraphWeaver
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
**What this is now:** a working prototype of a standalone, Sorbet-typed
|
|
5
|
+
GraphQL client for Ruby — "graphql-codegen for Ruby". `.graphql` queries +
|
|
6
|
+
a schema (live class, introspection JSON, or SDL) generate `# typed: strict`
|
|
7
|
+
Ruby: nested `T::Struct`s, casting code, and a typed `execute`, so `srb tc`
|
|
8
|
+
sees the exact shape of every query result. It is **not** a graphql-client
|
|
9
|
+
extension: generated code depends only on `graphql` (generation time) and
|
|
10
|
+
`sorbet-runtime` (runtime); transport is a pluggable `executor:` (in-process
|
|
11
|
+
schema or the bundled `HttpExecutor`).
|
|
12
|
+
|
|
13
|
+
Start with `PLAN.md` for current state and next steps. Key files:
|
|
14
|
+
`lib/struct_codegen.rb` (the generator), `queries/` → `bin/generate` →
|
|
15
|
+
`lib/generated/` (the build loop), `StructCodegen.load` (build-free dynamic
|
|
16
|
+
mode for development).
|
|
17
|
+
|
|
18
|
+
**How it got here:** the repo began as an exploration of
|
|
19
|
+
[graphql-client](https://github.com/github-community-projects/graphql-client)
|
|
20
|
+
internals — could its class-generation layer be swapped to emit custom
|
|
21
|
+
classes? (Yes: the `StructTypes` spike below.) The per-query codegen
|
|
22
|
+
approach then outgrew graphql-client entirely, and everything below the
|
|
23
|
+
next heading is preserved as the lab notebook: findings in chronological
|
|
24
|
+
order, each backed by a spec.
|
|
25
|
+
|
|
26
|
+
The specs are the documentation — each one asserts an observed behavior:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
bundle exec rspec
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Findings, in exploration order
|
|
33
|
+
|
|
34
|
+
- The client runs fine against an in-process schema: `GraphQL::Client.new(schema: Schema, execute: Schema)` — no HTTP involved.
|
|
35
|
+
- Each query selection gets its own dynamically generated wrapper class (subclass of `GraphQL::Client::Schema::ObjectClass`); fields are snake_case readers, and unselected fields raise instead of returning nil.
|
|
36
|
+
- **Custom scalars are deserialized automatically** when the client is built from a live schema class: the reader casts wire values through the scalar's `coerce_input` (e.g. `"1990-06-15"` → `Date`). This is the built-in hook for producing rich Ruby values.
|
|
37
|
+
- Caveat: this only works with `schema:` as a live schema class. A schema loaded from an introspection JSON dump has no coercion logic, so scalars would stay raw.
|
|
38
|
+
- `to_h` returns the raw wire values (strings), not the casted ones — hydration code should read via the typed readers, not `to_h`.
|
|
39
|
+
- Hydrating into `T::Struct`s is straightforward manually; the interesting next step is generating the structs (or a generic hydrator) from the parsed query definition, since the client already knows each selection's shape and types.
|
|
40
|
+
|
|
41
|
+
## Swapping the class-generation layer (answered: yes)
|
|
42
|
+
|
|
43
|
+
`lib/struct_types.rb` + `spec/struct_types_spec.rb` prove the generation layer
|
|
44
|
+
can be replaced wholesale — the client deserializes straight into generated
|
|
45
|
+
`T::Struct`s, no `ObjectClass` involved.
|
|
46
|
+
|
|
47
|
+
How the pipeline hangs together (graphql-client 0.26.0):
|
|
48
|
+
|
|
49
|
+
- `Client#initialize` builds the types module: `@types = Schema.generate(schema)`
|
|
50
|
+
(`attr_reader :types`, no setter — swap via `instance_variable_set` or a subclass).
|
|
51
|
+
- `Client#parse` → `Definition#initialize` calls
|
|
52
|
+
`client.types.define_class(definition, ast_nodes, type)` and stores the result
|
|
53
|
+
as `definition.schema_class`. This is the ONLY thing the client asks of the
|
|
54
|
+
types module.
|
|
55
|
+
- `Client#query` → `definition.new(data, errors)` → `schema_class.new(data, errors)`.
|
|
56
|
+
- Everything below that is the `cast(value, errors)` protocol, composed
|
|
57
|
+
recursively per the query selection (NonNull/List wrappers, scalars, objects).
|
|
58
|
+
|
|
59
|
+
So the replacement contract is just:
|
|
60
|
+
- `define_class(definition, ast_nodes, type)` returning casters
|
|
61
|
+
- casters respond to `cast(value, errors)`
|
|
62
|
+
- the top-level caster must satisfy `Definition#new`'s case dispatch, which
|
|
63
|
+
tests `===` against the `GraphQL::Client::Schema::ObjectType` module —
|
|
64
|
+
including that module in your caster class is enough, plus a
|
|
65
|
+
`new(data, errors)` method
|
|
66
|
+
|
|
67
|
+
Gotchas found:
|
|
68
|
+
- the client injects `__typename` into every selection (`QueryTypename`), so a
|
|
69
|
+
custom generator must skip/handle `__`-prefixed fields
|
|
70
|
+
- scalar casting reuses the schema type's `coerce_isolated_input` — same hook
|
|
71
|
+
the stock `ScalarType` uses
|
|
72
|
+
- prop nullability comes for free from the type walk: everything is
|
|
73
|
+
`T.nilable` unless wrapped in NON_NULL
|
|
74
|
+
|
|
75
|
+
## Sorbet
|
|
76
|
+
|
|
77
|
+
- `sorbet` + `tapioca` are set up (`bundle exec srb tc` is green); rbis in `sorbet/rbi/gems`
|
|
78
|
+
- `struct_types.rb` typechecks at `# typed: true`
|
|
79
|
+
- generated structs are real `T::Struct`s: schema-derived prop types
|
|
80
|
+
(`T.nilable(Date)`, `T::Array[StructTypes::Pet]`) and runtime type
|
|
81
|
+
enforcement on bad wire data
|
|
82
|
+
|
|
83
|
+
## Codegen: srb tc sees query result types (answered: yes)
|
|
84
|
+
|
|
85
|
+
`lib/struct_codegen.rb` goes one step further than the runtime swap: it
|
|
86
|
+
emits plain `# typed: strict` Ruby source from a query + schema — nested
|
|
87
|
+
`T::Struct` classes, fully generated `from_h` casting code (no runtime
|
|
88
|
+
reflection), and a sig'd `execute`.
|
|
89
|
+
|
|
90
|
+
- source of truth: `queries/*.graphql`; regenerate with `bin/generate`
|
|
91
|
+
into `lib/generated/`; a spec asserts the checked-in output is current
|
|
92
|
+
- queries are validated against the schema at generation time
|
|
93
|
+
- `srb tc` statically checks result access end to end:
|
|
94
|
+
`result.person&.nmae` → `Method nmae does not exist on
|
|
95
|
+
PersonQuery::Result::Person`
|
|
96
|
+
- custom scalar deserialization is inlined by the generator
|
|
97
|
+
(`Date.iso8601(...)`) via a scalar registry; nullability and list
|
|
98
|
+
casting come from the NON_NULL/LIST walk
|
|
99
|
+
- note: generated `execute` runs against the schema directly, replacing
|
|
100
|
+
graphql-client at runtime entirely — the client's remaining value here
|
|
101
|
+
would be its HTTP adapter, which the generated code could target instead
|
|
102
|
+
|
|
103
|
+
## Fragments & unions (answered for codegen)
|
|
104
|
+
|
|
105
|
+
`queries/search.graphql` + `lib/generated/search_query.rb` exercise the
|
|
106
|
+
design:
|
|
107
|
+
|
|
108
|
+
- inline fragments and named fragment spreads are flattened into their
|
|
109
|
+
matching member's selection (exact type-name condition match; interface
|
|
110
|
+
conditions still open)
|
|
111
|
+
- unions emit a module per selection site: one `T::Struct` per possible
|
|
112
|
+
type, a `Type = T.type_alias { T.any(...) }`, and a `from_h` that
|
|
113
|
+
dispatches on `__typename` — codegen refuses union selections that
|
|
114
|
+
don't select `__typename`
|
|
115
|
+
- every possible type gets a member struct even without a fragment (it
|
|
116
|
+
still carries `__typename`), so dispatch is total
|
|
117
|
+
|
|
118
|
+
## Introspection / __type metadata
|
|
119
|
+
|
|
120
|
+
- `__type` / `__schema` queries work against the demo schema as expected
|
|
121
|
+
(see `spec/introspection_spec.rb` for the shapes)
|
|
122
|
+
- the key result: `GraphQL::Schema.from_introspection(Demo::Schema.as_json)`
|
|
123
|
+
produces a schema that codegen runs against **byte-identically** — so
|
|
124
|
+
generation works for remote APIs known only via an introspection dump.
|
|
125
|
+
Custom scalar handling survives because the codegen scalar registry is
|
|
126
|
+
keyed by type *name*, unlike runtime `coerce_input` which needs the live
|
|
127
|
+
schema class (the caveat that broke graphql-client's scalar casting)
|
|
128
|
+
|
|
129
|
+
## Federation / supergraph
|
|
130
|
+
|
|
131
|
+
- join__/link-annotated supergraph SDL parses via
|
|
132
|
+
`GraphQL::Schema.from_definition`, and codegen runs against it
|
|
133
|
+
unchanged — the directives are transparent to result typing
|
|
134
|
+
(`spec/federation_spec.rb` generates from a mini supergraph and casts a
|
|
135
|
+
response with no live subgraphs)
|
|
136
|
+
- gotcha: graphql-ruby's SDL builder does not apply directive-argument
|
|
137
|
+
defaults, so real Apollo `join v0.3` SDL (non-null defaulted args like
|
|
138
|
+
`extension: Boolean! = false`) fails to load unless those args are
|
|
139
|
+
provided or the directive defs are trimmed — a compatibility issue a
|
|
140
|
+
real tool would need to patch around
|
|
141
|
+
- client-side, federation needs nothing more: you query the router like
|
|
142
|
+
any schema. The *server-side* angle (emitting `@key`/`@external` via
|
|
143
|
+
apollo-federation) is a separate exploration — potentially relevant to
|
|
144
|
+
autographql
|
|
145
|
+
|
|
146
|
+
## Round 2: enums, interface conditions, loaders, dynamic mode, HTTP
|
|
147
|
+
|
|
148
|
+
- **enums** generate `T::Enum` classes (`Species::Dog`), deserialized via
|
|
149
|
+
`Species.deserialize(...)` in `from_h`; values sorted so output is
|
|
150
|
+
deterministic across schema sources
|
|
151
|
+
- **interface fragment conditions** (`... on Named { name }`) apply via
|
|
152
|
+
`schema.possible_types`, not just exact type-name match. Interface-typed
|
|
153
|
+
*fields* (a field returning `Named`) are still open — they'd emit like
|
|
154
|
+
unions with `__typename` dispatch
|
|
155
|
+
- **SchemaLoader** accepts both formats a remote service can hand you:
|
|
156
|
+
introspection dump (`.json`) or SDL (`.graphql`/`.gql`); both generate
|
|
157
|
+
byte-identically to the live schema class
|
|
158
|
+
- **dynamic mode**: `StructCodegen.load(...)` generates + evals in one
|
|
159
|
+
step — no build artifact, same runtime semantics, right for development
|
|
160
|
+
or one-off scripts. Tradeoff: the module is invisible to `srb tc`, so
|
|
161
|
+
static checking of result access needs the build step
|
|
162
|
+
- **HTTP transport**: generated `execute` takes `executor:` — anything
|
|
163
|
+
with `execute(query, variables:)` returning `{"data" => ...}`.
|
|
164
|
+
`HttpExecutor` (Net::HTTP POST) runs the same generated structs against
|
|
165
|
+
a live server (`spec/http_spec.rb` proves it against a local WEBrick
|
|
166
|
+
serving Demo::Schema)
|
|
167
|
+
- **directive defaults gap**: root cause found —
|
|
168
|
+
`BuildFromDefinition#prepare_directives` passes only usage-site args
|
|
169
|
+
while `Directive#initialize` validates all defined args without
|
|
170
|
+
applying `default_value`. `lib/directive_defaults_patch.rb` prepends
|
|
171
|
+
the fix; the federation spec now loads the *real* join v0.3 SDL.
|
|
172
|
+
Present in graphql 2.6.3 (latest) — worth an upstream issue/PR
|
|
173
|
+
|
|
174
|
+
## Open questions
|
|
175
|
+
|
|
176
|
+
- interface-typed fields (vs fragment conditions, which work)
|
|
177
|
+
- name collisions: the generator disambiguates one level (field-name
|
|
178
|
+
prefix) and raises otherwise. A real gem needs a *stable* naming scheme:
|
|
179
|
+
names shouldn't shift when unrelated selections are added (generated
|
|
180
|
+
code is checked in and referenced by app code), which argues for
|
|
181
|
+
path-based or explicitly-aliased names over first-come-first-served
|
|
182
|
+
- mutations/subscriptions (only query operations generate)
|
data/PLAN.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Project Plan — typed GraphQL codegen for Ruby/Sorbet
|
|
2
|
+
|
|
3
|
+
_Resume-from-here notes. The README is the lab notebook (findings); this is
|
|
4
|
+
the plan. Update both when state changes._
|
|
5
|
+
|
|
6
|
+
## Vision
|
|
7
|
+
|
|
8
|
+
A "graphql-codegen for Ruby": `.graphql` queries + a schema (live class,
|
|
9
|
+
introspection JSON, or SDL) → checked-in `# typed: strict` Ruby — nested
|
|
10
|
+
T::Structs, generated casting, typed execute — so `srb tc` sees the exact
|
|
11
|
+
shape of every query result. Dynamic (eval) mode for development, build
|
|
12
|
+
step for CI/static checking. Runtime deps: graphql + sorbet-runtime only
|
|
13
|
+
(graphql-client is NOT a dependency; the exploration outgrew it).
|
|
14
|
+
|
|
15
|
+
## State: working prototype, all green
|
|
16
|
+
|
|
17
|
+
`bundle exec rspec` (35 examples) + `bundle exec srb tc` + `bin/generate`
|
|
18
|
+
(regenerates lib/generated/ from queries/; parity specs enforce freshness).
|
|
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).
|
|
25
|
+
Sources: live schema / introspection JSON / SDL — byte-identical output
|
|
26
|
+
(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
|
|
29
|
+
directive_defaults_patch until upstream fix ships).
|
|
30
|
+
|
|
31
|
+
## Next steps (in rough order)
|
|
32
|
+
|
|
33
|
+
~~Extraction~~ DONE 2026-07-07: this repo IS the gem now — GraphWeaver,
|
|
34
|
+
github.com/dpep/graph_weaver, rspec-uuid conventions throughout. The
|
|
35
|
+
graphql-client spikes live in git history (tag: `exploration`) and
|
|
36
|
+
NOTES.md. Prior-art check partially answered: graphql-client PR #7
|
|
37
|
+
(tapioca compiler over schema-wide dynamic classes) stalled since
|
|
38
|
+
Jan 2024 with users asking; schema-wide typing can't catch
|
|
39
|
+
unfetched-field bugs or type unions/interfaces — the niche looks open.
|
|
40
|
+
|
|
41
|
+
1. Stable class naming design — names come from GraphQL type names per
|
|
42
|
+
selection site; must not shift when unrelated selections are added
|
|
43
|
+
(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) —
|
|
48
|
+
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
|
|
52
|
+
on abstract selections), fragment reuse across queries, directives on
|
|
53
|
+
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
|
|
56
|
+
gets static types without the bin/generate build step — tapioca is
|
|
57
|
+
already in every Sorbet shop's workflow. Upstream's
|
|
58
|
+
Tapioca::Dsl::Helpers::GraphqlTypeHelper is prior art for type mapping.
|
|
59
|
+
|
|
60
|
+
## External dependencies
|
|
61
|
+
|
|
62
|
+
- rmosolgo/graphql-ruby#5659 (directive-argument defaults fix; our branch
|
|
63
|
+
`directive-argument-defaults` in ~/code/lib/ruby/graphql, pushed to the
|
|
64
|
+
dpep fork, PR in draft). When it ships in a release: bump graphql,
|
|
65
|
+
delete lib/graph_weaver/directive_defaults_patch.rb + its requires (TODO in file).
|
|
66
|
+
|
|
67
|
+
## Gotchas worth remembering
|
|
68
|
+
|
|
69
|
+
- graphql-ruby to_definition/from_introspection reorder enum values and
|
|
70
|
+
possible_types — codegen sorts both; keep any new emission deterministic
|
|
71
|
+
- schemas built from introspection/SDL have no scalar coercion or
|
|
72
|
+
resolvers — codegen must stay name-keyed, never call schema runtime hooks
|
|
73
|
+
- graphql-client (the gem) casts scalars via coerce_isolated_input and
|
|
74
|
+
only with a live schema class — documented in the early specs
|
data/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
GraphWeaver
|
|
2
|
+
======
|
|
3
|
+

|
|
4
|
+
[](https://codecov.io/gh/dpep/graph_weaver)
|
|
5
|
+
|
|
6
|
+
A typed GraphQL client for Ruby, built for federation, extensibility, Sorbet, and testing.
|
|
7
|
+
|
|
8
|
+
GraphWeaver generates `# typed: strict` Ruby from your queries: nested `T::Struct`s, casting code, and a typed `execute` — so `srb tc` sees the exact shape of every query result, and a typo'd field is a static error, not a runtime surprise.
|
|
9
|
+
|
|
10
|
+
```graphql
|
|
11
|
+
# queries/person.graphql
|
|
12
|
+
query($id: ID!) {
|
|
13
|
+
person(id: $id) {
|
|
14
|
+
name
|
|
15
|
+
birthday
|
|
16
|
+
pets { name }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
result = PersonQuery.execute(id: "1")
|
|
23
|
+
|
|
24
|
+
result.person&.name # => "Daniel" (typed String)
|
|
25
|
+
result.person&.birthday # => Date (custom scalars deserialize)
|
|
26
|
+
result.person&.nmae # => srb tc: Method `nmae` does not exist
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### Features
|
|
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
|
|
36
|
+
|
|
37
|
+
#### Usage
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "graph_weaver"
|
|
41
|
+
|
|
42
|
+
# generate from any schema source
|
|
43
|
+
schema = GraphWeaver::SchemaLoader.load("schema.json") # or .graphql SDL, or a live class
|
|
44
|
+
|
|
45
|
+
source = GraphWeaver::Codegen.new(
|
|
46
|
+
schema:,
|
|
47
|
+
executor_const: "MyApi::Executor",
|
|
48
|
+
query: File.read("queries/person.graphql"),
|
|
49
|
+
module_name: "PersonQuery",
|
|
50
|
+
).generate
|
|
51
|
+
|
|
52
|
+
File.write("app/queries/person_query.rb", source)
|
|
53
|
+
|
|
54
|
+
# at runtime
|
|
55
|
+
executor = GraphWeaver::HttpExecutor.new("https://api.example.com/graphql")
|
|
56
|
+
PersonQuery.execute(id: "1", executor:)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
In development, skip the build step:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
PersonQuery = GraphWeaver::Codegen.load(schema:, executor_const: "...", query:, module_name: "PersonQuery")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
----
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Gemfile
|
|
70
|
+
gem "graph_weaver"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
or
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
gem install graph_weaver
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
----
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
`make check` — regenerate spec fixtures, run specs, typecheck.
|
|
83
|
+
|
|
84
|
+
See `PLAN.md` for roadmap and `NOTES.md` for the research notebook this
|
|
85
|
+
gem grew out of (an exploration of graphql-client internals — GraphWeaver
|
|
86
|
+
is a standalone client, not an extension; it depends only on `graphql`
|
|
87
|
+
and `sorbet-runtime`).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative "lib/graph_weaver/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.authors = ["Daniel Pepper"]
|
|
5
|
+
s.description = "A typed GraphQL client for Ruby — generate Sorbet T::Structs from queries, with federation, extensibility, and testing in mind"
|
|
6
|
+
s.files = `git ls-files * ':!:spec' ':!:sorbet' ':!:bin'`.split("\n")
|
|
7
|
+
s.homepage = "https://github.com/dpep/graph_weaver"
|
|
8
|
+
s.license = "MIT"
|
|
9
|
+
s.name = "graph_weaver"
|
|
10
|
+
s.summary = "GraphWeaver"
|
|
11
|
+
s.version = GraphWeaver::VERSION
|
|
12
|
+
|
|
13
|
+
s.required_ruby_version = ">= 3.3"
|
|
14
|
+
|
|
15
|
+
s.add_dependency "graphql", ">= 2"
|
|
16
|
+
s.add_dependency "sorbet-runtime"
|
|
17
|
+
|
|
18
|
+
s.add_development_dependency "debug"
|
|
19
|
+
s.add_development_dependency "rspec"
|
|
20
|
+
s.add_development_dependency "simplecov"
|
|
21
|
+
s.add_development_dependency "sorbet"
|
|
22
|
+
s.add_development_dependency "tapioca"
|
|
23
|
+
s.add_development_dependency "webrick"
|
|
24
|
+
end
|