graph_weaver 0.6.1 → 0.7.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 +4 -4
- data/Gemfile +8 -0
- data/Gemfile.lock +153 -4
- data/README.md +45 -79
- data/docs/alternatives.md +195 -0
- data/docs/cassettes.md +61 -50
- data/docs/editors.md +32 -47
- data/docs/errors.md +360 -103
- data/docs/federation.md +692 -473
- data/docs/generated_modules.md +441 -314
- data/docs/getting_started.md +370 -194
- data/docs/i18n.md +171 -0
- data/docs/logging.md +197 -50
- data/docs/real_world.md +42 -27
- data/docs/scalars.md +307 -176
- data/docs/testing.md +473 -220
- data/docs/transports.md +224 -151
- data/docs/upgrading.md +258 -305
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +19 -3
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +69 -11
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +406 -195
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +284 -46
- data/lib/graph_weaver/federation.rb +129 -27
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +27 -15
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +80 -0
- data/lib/graph_weaver/internal/headers.rb +70 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +249 -14
- data/lib/graph_weaver/log_subscriber.rb +74 -0
- data/lib/graph_weaver/logging.rb +163 -19
- data/lib/graph_weaver/query_module.rb +44 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +45 -13
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +266 -56
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +34 -10
- data/lib/graph_weaver/testing/endpoint.rb +107 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +164 -45
- data/lib/graph_weaver/testing/router.rb +64 -13
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +48 -6
- data/lib/graph_weaver/transport.rb +134 -27
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +495 -106
- metadata +71 -3
- data/CHANGELOG.md +0 -2355
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Alternatives
|
|
2
|
+
|
|
3
|
+
Why another Ruby GraphQL client, what else exists, and where each one wins.
|
|
4
|
+
Read it before adopting — including the last section, which is where this gem
|
|
5
|
+
loses.
|
|
6
|
+
|
|
7
|
+
*Figures checked 2026-09-12. Every claim carries its source in an HTML comment
|
|
8
|
+
beside it; a maintainer re-checking this page should follow those, not trust the
|
|
9
|
+
prose.*
|
|
10
|
+
|
|
11
|
+
## Why another one
|
|
12
|
+
|
|
13
|
+
Ruby has **one** real GraphQL client — GitHub's `graphql-client` — and the two
|
|
14
|
+
maintained alternatives are wrappers around it.<!-- graphlient.gemspec deps: faraday ~>2.0, graphql-client; artemis.gemspec deps include graphql-client >= 0.13.0 — https://rubygems.org/api/v1/gems/graphlient.json, https://rubygems.org/api/v1/gems/artemis.json -->
|
|
15
|
+
All three share one design: the result object is built at runtime by
|
|
16
|
+
metaprogramming against an introspected schema, so a field you misspelled is a
|
|
17
|
+
`NoMethodError` on a production request rather than a red build.<!-- graphql-client lib/graphql/client/schema/object_type.rb:231-266 — method_missing raises UnimplementedFieldError/UnfetchedFieldError at runtime; a camelCase typo re-raises plain NoMethodError at :248-250 -->
|
|
18
|
+
And none of them ship anything to test with.<!-- graphql-client: no fakes/stubs/fixtures anywhere in lib/, no testing guide among its 14 guides — https://github.com/github-community-projects/graphql-client/tree/master/guides -->
|
|
19
|
+
|
|
20
|
+
GraphWeaver makes a different structural bet: **the schema is known at
|
|
21
|
+
generation time**, so result types can be real files on disk that `srb tc`
|
|
22
|
+
reads, and the same knowledge that makes them exact is what lets the gem
|
|
23
|
+
fabricate them for your tests. That second half is the part every code generator
|
|
24
|
+
in every language skips — and it is the reason precise types usually feel
|
|
25
|
+
expensive.
|
|
26
|
+
|
|
27
|
+
The honest counter: if your app doesn't use Sorbet, most of that value
|
|
28
|
+
evaporates, and [graphlient](#graphlient) is the better answer.
|
|
29
|
+
|
|
30
|
+
## The table
|
|
31
|
+
|
|
32
|
+
`graphql-ruby` isn't a column because it isn't a client — see
|
|
33
|
+
[graphql-ruby alone](#graphql-ruby-alone). "Hand-rolled" is `Net::HTTP` or
|
|
34
|
+
Faraday plus a query string and `response["data"]["..."]`, which is what most
|
|
35
|
+
vendor SDKs actually do.
|
|
36
|
+
|
|
37
|
+
| | **graph_weaver** | **graphql-client** | **graphlient** | **artemis** | **hand-rolled** |
|
|
38
|
+
|---|---|---|---|---|---|
|
|
39
|
+
| **Query lives in** | a `.graphql` file, one operation each<!-- README.md; DECISIONS.md "Directories organize queries" --> | a heredoc assigned to a Ruby constant (enforced)<!-- lib/graphql/client.rb:345-347 raises DynamicQueryError when definition.name is nil --> | a heredoc, a Ruby block DSL, or `parse`<!-- https://github.com/ashkan18/graphlient#usage ; lib/graphlient/query/serializer.rb --> | a `.graphql` file under `app/operations`<!-- https://github.com/yuki24/artemis#the-convention --> | a string in your code |
|
|
40
|
+
| **A result is** | a checked-in nested `T::Struct`<!-- README.md; lib/graph_weaver/result_struct.rb --> | an anonymous class, readers per selected field<!-- lib/graphql/client/schema/object_type.rb:10-22, 56-60 --> | the same (it returns graphql-client's `Response`)<!-- lib/graphlient/client.rb:41 --> | the same<!-- artemis lib/artemis/client.rb #execute delegates to client.query --> | a `Hash` |
|
|
41
|
+
| **A typo is caught** | at `srb tc`, before you run it | at runtime, on the request<!-- object_type.rb:243-266 --> | at runtime | at runtime | never |
|
|
42
|
+
| **Schema needed** | at codegen time: live class, introspection dump, SDL, or supergraph<!-- README.md; lib/graph_weaver/schema_loader.rb --> | at boot: dump recommended; SDL support merged but unreleased<!-- README.md:34-42; https://github.com/github-community-projects/graphql-client/pull/60 merged 2025-12-06, not in 0.26.0 --> | at runtime: introspects over HTTP lazily unless you pass `schema_path`<!-- lib/graphlient/schema.rb:16; lib/graphlient/client.rb:76 --> | a checked-in dump per service<!-- lib/artemis/railtie.rb schema_path vendor/graphql/schema/<service>.json --> | none |
|
|
43
|
+
| **Codegen** | yes — you check the Ruby in | none, all runtime metaprogramming<!-- lib/graphql/client/schema.rb:68-81 --> | none | none | none |
|
|
44
|
+
| **Typing** | Sorbet `# typed: strict`, per query<!-- README.md --> | none shipped; Tapioca PR stalled since 2024-11, and schema-wide not per-operation<!-- https://github.com/github-community-projects/graphql-client/pull/7 — open, mergeable_state blocked, last touched 2024-11-07 --> | none<!-- no .rbs/.rbi/sig in the gem --> | none | none |
|
|
45
|
+
| **Testing** | schema-driven fakes, pinning, failure simulation, cassettes<!-- docs/testing.md; lib/graph_weaver/rspec.rb; lib/graph_weaver/testing/ --> | nothing ships<!-- no stub/fake/fixture in lib/ --> | documented WebMock patterns only<!-- https://github.com/ashkan18/graphlient#testing-with-graphlient-and-rspec --> | `stub_graphql` + YAML fixtures, unvalidated against the schema<!-- lib/artemis/test_helper.rb; lib/artemis/adapters/test_adapter.rb returns fixtures verbatim --> | WebMock |
|
|
46
|
+
| **Federation** | plans and runs a supergraph in-process<!-- docs/federation.md; lib/graph_weaver/federation.rb --> | none; open crash against federated routers<!-- https://github.com/github-community-projects/graphql-client/issues/78 open since 2026-01-15 --> | none | none | none |
|
|
47
|
+
| **Errors** | typed envelope keeping partial data, hierarchy by failure site<!-- docs/errors.md; lib/graph_weaver/errors.rb --> | raw hashes; HTTP errors become a fake `errors` array<!-- lib/graphql/client/http.rb:80-85; https://github.com/github-community-projects/graphql-client/issues/67 --> | raises a real class hierarchy on every failure<!-- lib/graphlient/errors/ --> | thin hierarchy, mostly never raised<!-- lib/artemis/exceptions.rb — GraphQLError/GraphQLServerError defined, never raised --> | yours to write |
|
|
48
|
+
| **Transport** | `Net::HTTP` (pooled) or Faraday, plus `Retry`<!-- docs/transports.md; lib/graph_weaver/transport/ --> | stock adapter self-described as trivial<!-- lib/graphql/client/http.rb:17-19 "Production applications should consider implementing their own network adapter" --> | Faraday 2.x, full middleware access<!-- lib/graphlient/adapters/http/faraday_adapter.rb:36-48 --> | four adapters, no middleware layer<!-- lib/artemis/adapters.rb; https://github.com/yuki24/artemis/issues/57 open since 2019 --> | whatever you picked |
|
|
49
|
+
| **Rails** | generator, railtie, reload-on-edit, rake lifecycle<!-- lib/generators/graph_weaver/install_generator.rb; lib/graph_weaver/railtie.rb; lib/graph_weaver/tasks.rb --> | opt-in railtie, no generators; docs call the boot order "a mess"<!-- lib/graphql/client/railtie.rb:34-37 TODO; https://github.com/github-community-projects/graphql-client/blob/master/guides/rails-configuration.md --> | none | the whole pitch: generators, config, callbacks<!-- lib/artemis/railtie.rb; lib/generators/artemis/ --> | n/a |
|
|
50
|
+
| **Last release** | v0.7.1, 2026-09-13 | v0.26.0, 2025-05-29<!-- https://rubygems.org/api/v1/gems/graphql-client.json --> | v0.9.0, 2026-08-02<!-- https://rubygems.org/api/v1/gems/graphlient.json --> | v1.1.0, 2024-08-16<!-- https://rubygems.org/api/v1/gems/artemis.json --> | n/a |
|
|
51
|
+
| **Downloads** | 7.5k<!-- 7,478 — https://rubygems.org/api/v1/gems/graph_weaver.json --> | 94M<!-- 94,408,657 --> | 32M<!-- 32,287,626 --> | 430k<!-- 425,737 --> | n/a |
|
|
52
|
+
|
|
53
|
+
## graphql-client
|
|
54
|
+
|
|
55
|
+
**Where it shines.** It is GitHub's, it has a decade of production use, and 94M
|
|
56
|
+
downloads means someone has hit your problem before you. The duck-typed
|
|
57
|
+
`execute:` slot is genuinely good design — point it at a graphql-ruby schema and
|
|
58
|
+
queries run in-process with no socket.<!-- https://github.com/github-community-projects/graphql-client/blob/master/guides/local-queries.md -->
|
|
59
|
+
It ships two RuboCop cops, including `GraphQL/Overfetch`, which nobody else
|
|
60
|
+
has.<!-- lib/rubocop/cop/graphql/ in the unpacked gem -->
|
|
61
|
+
|
|
62
|
+
**Where it gaps.** Custom scalars don't deserialize when the schema came from a
|
|
63
|
+
dump — the path its own README recommends — and the suggested workaround is
|
|
64
|
+
monkey-patching `GraphQL::Schema::BUILT_IN_TYPES`; the issue has been open since
|
|
65
|
+
February 2024.<!-- https://github.com/github-community-projects/graphql-client/issues/17 --> Network
|
|
66
|
+
errors are discarded: a 403 surfaces as `KeyError: key not found: "data"`.<!-- https://github.com/github-community-projects/graphql-client/issues/67 open since 2025-04-11 -->
|
|
67
|
+
Fragments enforce Relay-style data masking, so a field another fragment fetched
|
|
68
|
+
raises even though the value is right there in the response — and users file
|
|
69
|
+
issues asking for plain reuse.<!-- object_type.rb:161-168, 261-264; https://github.com/github-community-projects/graphql-client/issues/76 -->
|
|
70
|
+
Its CI matrix stops at Ruby 3.2 and Rails 7.1.<!-- https://github.com/github-community-projects/graphql-client/blob/master/.github/workflows/ci.yml -->
|
|
71
|
+
Nine issues and seven PRs are open, several waiting on a maintainer to approve a
|
|
72
|
+
CI run.<!-- gh api search/issues, repo:github-community-projects/graphql-client, 2026-09-12 -->
|
|
73
|
+
|
|
74
|
+
**Pick it over graph_weaver when** institutional safety outweighs static types,
|
|
75
|
+
or when you need data masking as a feature rather than a constraint.
|
|
76
|
+
|
|
77
|
+
## graphlient
|
|
78
|
+
|
|
79
|
+
**Where it shines.** Quietly the healthiest Ruby client: 0.9.0 shipped
|
|
80
|
+
2026-08-02, more recently than graphql-client itself, adding DSL fragments,
|
|
81
|
+
directives and scalar registration.<!-- https://github.com/ashkan18/graphlient/blob/master/CHANGELOG.md -->
|
|
82
|
+
It fixes the failure everyone hits with its substrate — it raises a real,
|
|
83
|
+
rescuable error hierarchy instead of handing you a half-populated response.<!-- lib/graphlient/errors/ --> Faraday
|
|
84
|
+
means your existing middleware just works.
|
|
85
|
+
|
|
86
|
+
**Where it gaps.** It is a wrapper, so it inherits graphql-client's result
|
|
87
|
+
model, its fragment isolation, and its untyped everything — its own README
|
|
88
|
+
offers `to_query_string` as "the escape hatch if you want to replace the
|
|
89
|
+
graphql-client dependency entirely".<!-- https://github.com/ashkan18/graphlient#readme, 0.9.0 -->
|
|
90
|
+
Read and write timeouts default to nil, i.e. none.<!-- README config table; lib/graphlient/adapters/http/adapter.rb:33-40 -->
|
|
91
|
+
No Rails integration, no field aliasing, and 18 open issues, the most-reacted
|
|
92
|
+
dating to 2017.<!-- https://github.com/ashkan18/graphlient/issues/10 ; gh api repos/ashkan18/graphlient -->
|
|
93
|
+
|
|
94
|
+
**Pick it over graph_weaver when** you want to call an API without thinking
|
|
95
|
+
about it. For that job graph_weaver is over-engineered, and this is the right
|
|
96
|
+
answer.
|
|
97
|
+
|
|
98
|
+
## artemis
|
|
99
|
+
|
|
100
|
+
**Where it shines.** The best Rails story of the three: `rails g
|
|
101
|
+
artemis:install` writes the client, the config and the schema dump; `.graphql`
|
|
102
|
+
files map to methods by convention; `before_execute`/`after_execute` are real
|
|
103
|
+
hooks; and it has the only shipped test harness among the alternatives —
|
|
104
|
+
`stub_graphql(Artsy, :artist).to_return(:yayoi_kusama)` against YAML
|
|
105
|
+
fixtures.<!-- lib/artemis/test_helper.rb; https://github.com/yuki24/artemis#testing -->
|
|
106
|
+
It also batches, via `Client.multiplex`.<!-- lib/artemis/client.rb .multiplex/MultiplexQueue -->
|
|
107
|
+
|
|
108
|
+
**Where it gaps.** Fixtures are returned verbatim — nothing checks them against
|
|
109
|
+
the schema or the query's selection set, so a fixture can drift from reality and
|
|
110
|
+
stay green.<!-- lib/artemis/adapters/test_adapter.rb#execute -->
|
|
111
|
+
Its reloader and production preload are both gated on *not* using Zeitwerk,
|
|
112
|
+
which every Rails 7+ app does, so the README's preloading claim no longer
|
|
113
|
+
applies.<!-- lib/artemis/railtie.rb — graphql.client.set_reloader and graphql.client.preload both gated on not_on_zeitwerk -->
|
|
114
|
+
Last release August 2024; Rails 8 support exists only on `main`, and there have
|
|
115
|
+
been no commits since December 2025.<!-- gh api repos/yuki24/artemis/compare/v1.1.0...main ; last commit 8b3d76a 2025-12-04 -->
|
|
116
|
+
|
|
117
|
+
**Pick it over graph_weaver when** you want convention-over-configuration Rails
|
|
118
|
+
ergonomics and don't need types.
|
|
119
|
+
|
|
120
|
+
## graphql-ruby alone
|
|
121
|
+
|
|
122
|
+
`graphql-ruby` is a **server** library and ships no HTTP client at all — its
|
|
123
|
+
only three runtime dependencies are `base64`, `fiber-storage` and `logger`, and
|
|
124
|
+
the only `Net::HTTP` call in the gem fetches a checksum for graphql-pro.<!-- gem spec graphql-2.6.10.gem dependencies; lib/graphql/rake_task/validate.rb:38-44 -->
|
|
125
|
+
The thing called "client" in its docs is
|
|
126
|
+
[JavaScript](https://graphql-ruby.org/javascript_client/overview).
|
|
127
|
+
|
|
128
|
+
What it does give a client author is the substrate everyone here builds on:
|
|
129
|
+
[`GraphQL.parse`](https://graphql-ruby.org/api-doc/2.6.10/GraphQL.html),
|
|
130
|
+
[`GraphQL::Schema.from_definition`](https://graphql-ruby.org/schema/sdl.html)
|
|
131
|
+
and `from_introspection`, and `GraphQL::StaticValidation::Validator` for
|
|
132
|
+
checking a document against a schema.<!-- lib/graphql.rb:49; lib/graphql/schema.rb:105,115; lib/graphql/static_validation/validator.rb:11-18 -->
|
|
133
|
+
GraphWeaver uses exactly these — it is a code generator on top of graphql-ruby,
|
|
134
|
+
not a reimplementation of it.
|
|
135
|
+
|
|
136
|
+
**Subscriptions are server-side only** across the whole ecosystem: graphql-ruby
|
|
137
|
+
delivers them over ActionCable and every documented consumer is
|
|
138
|
+
JavaScript,<!-- https://graphql-ruby.org/subscriptions/action_cable_implementation — "See client usage for: Apollo Client, Relay Modern, GraphiQL" -->
|
|
139
|
+
and no Ruby gem consumes GraphQL subscriptions over websockets.<!-- rubygems search graphql+websocket, graphql-ws, subscriptions-transport-ws all return 0 results, 2026-09-12 -->
|
|
140
|
+
Nobody in Ruby has this, GraphWeaver included.
|
|
141
|
+
|
|
142
|
+
## Hand-rolled HTTP — the real incumbent
|
|
143
|
+
|
|
144
|
+
Most Ruby code talking to a GraphQL API isn't using a client library. It POSTs a
|
|
145
|
+
string and reads a hash, and that includes the vendors' own SDKs.
|
|
146
|
+
|
|
147
|
+
Shopify is the sharpest example. `shopify_api` v10 **removed** graphql-client,
|
|
148
|
+
saying so in its breaking-changes doc — "There is no need to dump the schema to
|
|
149
|
+
a local JSON file before using it anymore" — and the migration example replaces
|
|
150
|
+
`result.data.shop.name` with `response.body["data"]["shop"]["name"]`.<!-- https://github.com/Shopify/shopify-api-ruby/blob/main/BREAKING_CHANGES_FOR_V10.md:3,115,140-159 -->
|
|
151
|
+
The current gemspec declares `httparty`, `oj` and `sorbet-runtime` and no
|
|
152
|
+
`graphql` at all.<!-- https://github.com/Shopify/shopify-api-ruby/blob/main/shopify_api.gemspec:35-46 -->
|
|
153
|
+
The gem is `# typed: strict` throughout — and the GraphQL payload is typed
|
|
154
|
+
`T.any(T::Hash[String, T.untyped], String, OpenStruct)`.<!-- https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/clients/http_response.rb#L15-L16 -->
|
|
155
|
+
A Sorbet shop, shipping a Sorbet-typed SDK, with untyped GraphQL. Braintree does
|
|
156
|
+
the same thing without the Sorbet.<!-- https://github.com/braintree/braintree_ruby/blob/master/lib/braintree/graphql_client.rb#L13-L26 -->
|
|
157
|
+
|
|
158
|
+
**Where it shines.** Zero dependencies, zero build step, nothing to learn, and
|
|
159
|
+
it never gets in your way. For three queries against a stable API this is
|
|
160
|
+
genuinely the correct engineering call.
|
|
161
|
+
|
|
162
|
+
**Where it gaps.** Nothing validates the query, nothing knows the schema
|
|
163
|
+
changed, every response is `T.untyped`, and the error handling is a 40-line
|
|
164
|
+
layer you write once per project. The cost is invisible until the schema moves.
|
|
165
|
+
|
|
166
|
+
**Pick it over graph_weaver when** you have a handful of queries and no
|
|
167
|
+
appetite for a build step.
|
|
168
|
+
|
|
169
|
+
## Where graph_weaver loses
|
|
170
|
+
|
|
171
|
+
Said plainly.
|
|
172
|
+
|
|
173
|
+
- **Sorbet is a hard dependency.** `sorbet-runtime` is a runtime
|
|
174
|
+
dependency,<!-- graph_weaver.gemspec: s.add_dependency "sorbet-runtime" --> and
|
|
175
|
+
the payoff — a typo caught before you run — needs `srb tc` in your build. No
|
|
176
|
+
RBS output. If Sorbet isn't in your stack, you're carrying the cost of
|
|
177
|
+
generated code for a fraction of the benefit.
|
|
178
|
+
- **Codegen is a build step.** You check generated Ruby in, and regenerate on
|
|
179
|
+
every gem upgrade, because any release can change what codegen emits. `rake
|
|
180
|
+
graph_weaver:verify` exists precisely because that step is easy to skip. The
|
|
181
|
+
alternatives have no build step at all.
|
|
182
|
+
- **No subscriptions.** A subscription document is refused outright:
|
|
183
|
+
`GraphWeaver::Error: unsupported operation: subscription`.<!-- lib/graph_weaver/internal/selection.rb:48 --> Nobody
|
|
184
|
+
in Ruby has this, but that doesn't make it present here.
|
|
185
|
+
- **No `@defer`, no file uploads, no persisted queries, no batching.** artemis
|
|
186
|
+
has multiplex batching;<!-- lib/artemis/client.rb .multiplex --> this gem has
|
|
187
|
+
none of the four.
|
|
188
|
+
- **One author, and it's new.** First commit July 2026, 4 stars, pre-1.0, and
|
|
189
|
+
about 30 breaking-change notes in the changelog so far.<!-- gh api repos/dpep/graph_weaver created_at 2026-07-07, stargazers_count 4; grep -c -i breaking CHANGELOG.md -->
|
|
190
|
+
Codegen is unforgiving and there is a lot of surface to get wrong.
|
|
191
|
+
`verify` mitigates drift; nothing mitigates the maintainer. "No static types"
|
|
192
|
+
is a cost many teams will rationally accept over that.
|
|
193
|
+
|
|
194
|
+
If you read that list and none of it stops you, the thing you get in exchange is
|
|
195
|
+
in [getting started](getting_started.md).
|
data/docs/cassettes.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# Cassettes: capture and replay
|
|
2
2
|
|
|
3
3
|
Cassettes record real API responses and replay them in tests — above the
|
|
4
|
-
transport (a client wrapping a client), so there's no HTTP interception and
|
|
5
|
-
|
|
6
|
-
is a YAML list of `{query, variables, operationName, response}` entries,
|
|
7
|
-
matched on everything but the response — the request's identity as the server
|
|
8
|
-
sees it.
|
|
4
|
+
transport (a client wrapping a client), so there's no HTTP interception and they
|
|
5
|
+
work identically over HTTP, Faraday, or in-process execution.
|
|
9
6
|
|
|
10
7
|
`Testing.cassette(name, client:)` returns a client that replays
|
|
11
8
|
`spec/cassettes/<name>.yml`, recording it through `client:` first if the file
|
|
12
|
-
doesn't exist yet
|
|
9
|
+
doesn't exist yet:
|
|
13
10
|
|
|
14
11
|
```ruby
|
|
15
12
|
client = GraphWeaver::Testing.cassette("github", client: live)
|
|
@@ -17,7 +14,7 @@ result = RepoQuery.execute!(client:, owner: "dpep", name: "graph_weaver")
|
|
|
17
14
|
```
|
|
18
15
|
|
|
19
16
|
That first run writes `spec/cassettes/github.yml` (`Testing.config.cassette_dir`
|
|
20
|
-
resolves bare names). Commit it — with anonymization on
|
|
17
|
+
resolves bare names). Commit it — with [anonymization](#anonymization) on, since
|
|
21
18
|
recordings hold real data — and the suite runs offline from then on. Re-record
|
|
22
19
|
when the API's real behavior changes:
|
|
23
20
|
|
|
@@ -27,9 +24,27 @@ GRAPHWEAVER_RECORD=1 bundle exec rspec # every Testing.cassette records afresh
|
|
|
27
24
|
|
|
28
25
|
(`Testing.config.record = true` is the programmatic equivalent.) A call with no
|
|
29
26
|
`client:` raises there, rather than quietly replaying the recording it was told
|
|
30
|
-
to refresh
|
|
31
|
-
`GraphWeaver::Testing::MissingRecording`, naming the variables it was called
|
|
32
|
-
|
|
27
|
+
to refresh; a *request* with no recording raises
|
|
28
|
+
`GraphWeaver::Testing::MissingRecording`, naming the variables it was called with
|
|
29
|
+
and the ones recorded for that same query — what usually differs.
|
|
30
|
+
|
|
31
|
+
**One entry per request.** A cassette is a YAML list of
|
|
32
|
+
`{query, variables, operationName, response}` entries, and the first three
|
|
33
|
+
together are the request key — the request's identity as the server sees it.
|
|
34
|
+
Re-recording a request *replaces* its entry and a request the file hasn't seen
|
|
35
|
+
appends one, so however often `GRAPHWEAVER_RECORD=1` runs, no cassette ends up
|
|
36
|
+
with two entries for the same request.
|
|
37
|
+
|
|
38
|
+
Editing a query changes the key, so the re-record writes a new entry and the old
|
|
39
|
+
one stays behind — a recording of a request nothing sends any more.
|
|
40
|
+
`cassettes:check` counts those ("1 not sent by any query module", below); the way
|
|
41
|
+
to clear them is to delete the cassette and record it afresh.
|
|
42
|
+
|
|
43
|
+
**Recording under `parallel_tests` is safe.** Writing rewrites the whole file, so
|
|
44
|
+
a recorder re-reads it inside an exclusive lock on a `<cassette>.yml.lock`
|
|
45
|
+
sidecar — a mutex for this process's threads, an `flock` for the other
|
|
46
|
+
processes — and every worker's entries survive. That sidecar is an empty lock
|
|
47
|
+
target: gitignore it, or let it sit; nothing reads it.
|
|
33
48
|
|
|
34
49
|
## Has a recording gone stale?
|
|
35
50
|
|
|
@@ -37,34 +52,29 @@ A cassette is the one artifact here recorded from *someone else's* server, and
|
|
|
37
52
|
none of the other checks can see it drift: `verify` asks whether the generated
|
|
38
53
|
Ruby is fresh, `queries:check` whether a query still validates, `schema:diff`
|
|
39
54
|
whether the server's schema moved. When the recorded *answers* stop fitting the
|
|
40
|
-
structs your schema generated — a field that was `Int!` when you recorded and
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
rake graph_weaver:cassettes:check
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
It replays every recording through the generated modules — no network — so it
|
|
49
|
-
belongs in the normal PR run beside `verify`, and exits non-zero on drift:
|
|
55
|
+
structs your schema generated — a field that was `Int!` when you recorded and is
|
|
56
|
+
`String!` now — nothing notices until a spec dies mid-run on a cast error naming
|
|
57
|
+
a struct and nothing else. `rake graph_weaver:cassettes:check` replays every
|
|
58
|
+
recording through the generated modules — no network — so it belongs in the
|
|
59
|
+
normal PR run beside `verify`, and exits non-zero on drift:
|
|
50
60
|
|
|
51
61
|
```
|
|
52
62
|
spec/cassettes/dashboard.yml: 1 stale (3 checked, 1 not sent by any query module)
|
|
53
|
-
DashboardQuery {"id"
|
|
63
|
+
DashboardQuery {"id":"b1"}
|
|
54
64
|
failed to cast response into DashboardQuery::Result::Me::Reviews::Book: Parameter 'price_cents': Can't set …price_cents to 4200 (instance of Integer) - need a String
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
A recording is matched to the module that sends its query, so one written by
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
A recording is matched to the module that sends its query, so one written by hand
|
|
68
|
+
is skipped and counted rather than guessed at. Checking **none** of them fails
|
|
69
|
+
too: a green run that compared nothing would pass whatever the recordings said.
|
|
70
|
+
The fix is a re-record (`GRAPHWEAVER_RECORD=1`, with a live `client:`) — or
|
|
71
|
+
`rake graph_weaver:generate`, if it was the schema dump that moved first.
|
|
62
72
|
|
|
63
73
|
## Anonymization
|
|
64
74
|
|
|
65
|
-
Cassettes hold real responses, so scrub them as they're recorded: real data
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
Cassettes hold real responses, so scrub them as they're recorded: real data never
|
|
76
|
+
reaches disk, and the caller sees the anonymized response too, so assertions
|
|
77
|
+
written during the recording run still hold on replay.
|
|
68
78
|
|
|
69
79
|
```ruby
|
|
70
80
|
GraphWeaver::Testing.configure do |config|
|
|
@@ -82,17 +92,22 @@ preserving everything that makes the recording faithful:
|
|
|
82
92
|
| enums, booleans, `__typename` | numbers, dates |
|
|
83
93
|
| id *relationships* (same original id → same fake id) | the id values themselves |
|
|
84
94
|
|
|
95
|
+
**Every plain string goes**, not the PII-shaped ones — nothing here can tell a
|
|
96
|
+
user's name from a product's, so a recorded `"pikachu"` replays as `"name-1"` and
|
|
97
|
+
an assertion pinned to it fails. Leave it off for a public, non-sensitive API,
|
|
98
|
+
where the real values *are* the point of the cassette.
|
|
99
|
+
|
|
85
100
|
`data` is walked against the schema — which is why it needs one, to know which
|
|
86
|
-
values are enums, dates, ids. `errors` and `extensions` have none behind them,
|
|
87
|
-
|
|
101
|
+
values are enums, dates, ids. `errors` and `extensions` have none behind them, so
|
|
102
|
+
they're walked by shape instead: keys, nesting and structure survive, every
|
|
88
103
|
string and number is replaced. `path`, `locations` and an error's
|
|
89
104
|
`extensions.code` are kept, because they describe the request rather than the
|
|
90
105
|
data — and call sites branch on `code` the way they branch on an enum.
|
|
91
106
|
|
|
92
107
|
**The query and its variables are not anonymized.** They're the key replay
|
|
93
108
|
matches on, so scrubbing them would make the recording unfindable. A mutation's
|
|
94
|
-
input is often the sensitive part, so record with placeholder variables, or
|
|
95
|
-
|
|
109
|
+
input is often the sensitive part, so record with placeholder variables, or don't
|
|
110
|
+
record that request.
|
|
96
111
|
|
|
97
112
|
Recording says so when the bytes it wrote look like a credential:
|
|
98
113
|
|
|
@@ -102,20 +117,16 @@ cassette is committed as written, so review this one first. …
|
|
|
102
117
|
```
|
|
103
118
|
|
|
104
119
|
It recognizes tokens by shape — a JWT, `AKIA…`, `ghp_…`, `xox…`, `sk_live_…`, a
|
|
105
|
-
PEM block, a `Bearer` header — which is every credential that is unmistakable
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
For cassettes recorded before the flag was on
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Anonymization preserves shape, so an anonymized cassette still passes
|
|
116
|
-
`cassettes:check` — including a custom scalar, whose replacement is the same
|
|
117
|
-
one [`FakeClient`](testing.md#fabricated-data--graphql-fake) would fabricate.
|
|
118
|
-
A scalar registered as *your own* class needs a pin for the type in
|
|
120
|
+
PEM block, a `Bearer` header — which is every credential that is unmistakable and
|
|
121
|
+
nothing else. A password like `hunter2` has no shape, so a quiet run is not a
|
|
122
|
+
clean bill of health: **read a cassette before committing it.**
|
|
123
|
+
|
|
124
|
+
For cassettes recorded before the flag was on,
|
|
125
|
+
`rake graph_weaver:cassettes:anonymize` does every cassette in `cassette_dir`, in
|
|
126
|
+
place. Anonymization preserves shape, so an anonymized cassette still passes
|
|
127
|
+
`cassettes:check` — including a custom scalar, whose replacement is the same one
|
|
128
|
+
[`FakeClient`](testing.md#fabricated-data--graphql-fake) would fabricate. A scalar
|
|
129
|
+
registered as *your own* class needs a pin for the type in
|
|
119
130
|
`Testing.config.overrides` (`{ "Money" => "12.00" }` — [pins](testing.md#pins)),
|
|
120
131
|
which the anonymizer reads too; without one, anonymizing refuses rather than
|
|
121
132
|
writing a value the codec can't read back.
|
|
@@ -123,6 +134,6 @@ writing a value the codec can't read back.
|
|
|
123
134
|
## Cassette or FakeClient?
|
|
124
135
|
|
|
125
136
|
[FakeClient](testing.md) needs no recording and is the better default for unit
|
|
126
|
-
tests. Reach for a cassette when the *shape* of a real API's answers is the
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
tests. Reach for a cassette when the *shape* of a real API's answers is the point
|
|
138
|
+
— pagination quirks, which union member came back, where that server puts its
|
|
139
|
+
nulls — and for pinning a regression.
|
data/docs/editors.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# Editor support: five lines of YAML
|
|
2
2
|
|
|
3
|
-
Your `.graphql` files are plain GraphQL documents and your schema dump is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Ruby developers mostly don't know this, which is the only reason it's worth a
|
|
9
|
-
page.
|
|
10
|
-
|
|
11
|
-
## The file
|
|
3
|
+
Your `.graphql` files are plain GraphQL documents and your schema dump is a plain
|
|
4
|
+
introspection result, so the whole JavaScript GraphQL editor toolchain works on a
|
|
5
|
+
Ruby repo — **with no JS project, no `package.json`, and no `npm install`**. It
|
|
6
|
+
just needs one config file telling it where the two live. Ruby developers mostly
|
|
7
|
+
don't know this, which is the only reason it's worth a page.
|
|
12
8
|
|
|
13
9
|
```yaml
|
|
14
10
|
# graphql.config.yml — repo root
|
|
@@ -18,58 +14,47 @@ documents:
|
|
|
18
14
|
- app/graphql/fragments/**/*.{graphql,gql}
|
|
19
15
|
```
|
|
20
16
|
|
|
21
|
-
That's the whole setup
|
|
22
|
-
(`GraphWeaver.schema_path`, `queries_paths`,
|
|
23
|
-
them, move these to match. Include the
|
|
24
|
-
validating a query that spreads a shared fragment
|
|
25
|
-
unless the fragment files are in `documents` too. The
|
|
26
|
-
line whether or not you have fragments yet
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
An SDL dump works just as well if you took one (`cache: :graphql`):
|
|
30
|
-
|
|
31
|
-
```yaml
|
|
32
|
-
schema: app/graphql/schema.graphql
|
|
33
|
-
```
|
|
17
|
+
That's the whole setup, and `rails g graph_weaver:install` writes it. The paths
|
|
18
|
+
are graph_weaver's conventions (`GraphWeaver.schema_path`, `queries_paths`,
|
|
19
|
+
`fragments_paths`) — if you moved them, move these to match. Include the
|
|
20
|
+
fragments directory: an editor validating a query that spreads a shared fragment
|
|
21
|
+
reports `Unknown fragment` unless the fragment files are in `documents` too. The
|
|
22
|
+
generator writes that line whether or not you have fragments yet, and a glob
|
|
23
|
+
matching nothing is fine.
|
|
34
24
|
|
|
35
25
|
Introspection JSON is read directly — graphql-config ships a JSON loader, so
|
|
36
|
-
`schema.json` needs no conversion step.
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
`schema.json` needs no conversion step. An SDL dump works just as well if you
|
|
27
|
+
took one (`cache: :graphql`): point `schema:` at `app/graphql/schema.graphql`.
|
|
28
|
+
graph_weaver also writes a `graph_weaver` provenance key alongside the
|
|
29
|
+
introspection result; if some tool objects to it, use the SDL dump instead.
|
|
39
30
|
|
|
40
31
|
## What it buys you
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- **The JetBrains GraphQL plugin**, bundled with recent RubyMine, reads the
|
|
48
|
-
same file.
|
|
49
|
-
|
|
50
|
-
Either one gives you, inside a `.graphql` file:
|
|
33
|
+
Two editor plugins read this file:
|
|
34
|
+
**[vscode-graphql](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql)**,
|
|
35
|
+
whose README states it **requires** a graphql-config file — which is why nothing
|
|
36
|
+
works without the YAML above — and **the JetBrains GraphQL plugin**, bundled with
|
|
37
|
+
recent RubyMine. Either one gives you, inside a `.graphql` file:
|
|
51
38
|
|
|
52
39
|
- validation as you type — a typo'd field is red before you run anything
|
|
53
40
|
- field and argument autocomplete off the real schema
|
|
54
|
-
- go-to-definition and hover docs into schema types, including the
|
|
55
|
-
|
|
41
|
+
- go-to-definition and hover docs into schema types, including the descriptions
|
|
42
|
+
the API author wrote
|
|
56
43
|
|
|
57
44
|
That is the same feedback the generator gives you, one round trip earlier — you
|
|
58
|
-
find the typo while typing the query, not at `rake graph_weaver:generate`.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[@graphql-eslint](https://the-guild.dev/graphql/eslint/docs) for lint rules
|
|
64
|
-
over your documents.
|
|
45
|
+
find the typo while typing the query, not at `rake graph_weaver:generate`. The
|
|
46
|
+
same globs also feed the JS CI tools, if you want them (these *do* need npm,
|
|
47
|
+
unlike the editor path): [graphql-inspector](https://the-guild.dev/graphql/inspector)
|
|
48
|
+
`validate` and [@graphql-eslint](https://the-guild.dev/graphql/eslint/docs) for
|
|
49
|
+
lint rules over your documents.
|
|
65
50
|
|
|
66
51
|
## What it doesn't buy you
|
|
67
52
|
|
|
68
53
|
**Nothing links a `.graphql` file to the Ruby it generates.** There is no
|
|
69
|
-
go-to-definition from a query field to its `T::Struct`, no rename that moves
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
go-to-definition from a query field to its `T::Struct`, no rename that moves both,
|
|
55
|
+
no warning that a struct went unused. The editor plugin understands GraphQL and
|
|
56
|
+
Sorbet understands Ruby, and no tool in any ecosystem bridges the two except
|
|
57
|
+
where documents and types share a single language service.
|
|
73
58
|
|
|
74
59
|
So the division of labour is:
|
|
75
60
|
|