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 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
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,13 @@
1
+ .PHONY: check generate test tc
2
+
3
+ # full verify loop: regenerate, test, typecheck
4
+ check: generate test tc
5
+
6
+ generate:
7
+ bundle exec ruby bin/generate
8
+
9
+ test:
10
+ bundle exec rspec
11
+
12
+ tc:
13
+ bundle exec srb tc
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
+ ![Gem](https://img.shields.io/gem/dt/graph_weaver?style=plastic)
4
+ [![codecov](https://codecov.io/gh/dpep/graph_weaver/branch/main/graph/badge.svg)](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