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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +153 -4
  4. data/README.md +45 -79
  5. data/docs/alternatives.md +195 -0
  6. data/docs/cassettes.md +61 -50
  7. data/docs/editors.md +32 -47
  8. data/docs/errors.md +360 -103
  9. data/docs/federation.md +692 -473
  10. data/docs/generated_modules.md +441 -314
  11. data/docs/getting_started.md +370 -194
  12. data/docs/i18n.md +171 -0
  13. data/docs/logging.md +197 -50
  14. data/docs/real_world.md +42 -27
  15. data/docs/scalars.md +307 -176
  16. data/docs/testing.md +473 -220
  17. data/docs/transports.md +224 -151
  18. data/docs/upgrading.md +258 -305
  19. data/examples/README.md +38 -0
  20. data/examples/countries.rb +39 -0
  21. data/examples/federation.rb +62 -0
  22. data/examples/github/generate.rb +20 -0
  23. data/examples/github/generated/star_mutation.rb +126 -0
  24. data/examples/github/generated/stargazers_query.rb +232 -0
  25. data/examples/github/generated/starred_query.rb +151 -0
  26. data/examples/github/queries/star.graphql +8 -0
  27. data/examples/github/queries/stargazers.graphql +22 -0
  28. data/examples/github/queries/starred.graphql +11 -0
  29. data/examples/github/run.rb +43 -0
  30. data/examples/github/setup.rb +18 -0
  31. data/examples/rick_and_morty.rb +57 -0
  32. data/graph_weaver.gemspec +19 -3
  33. data/lib/generators/graph_weaver/install_generator.rb +138 -4
  34. data/lib/graph_weaver/client.rb +69 -11
  35. data/lib/graph_weaver/codegen/aliases.rb +7 -5
  36. data/lib/graph_weaver/codegen/emit.rb +98 -29
  37. data/lib/graph_weaver/codegen/enum_type.rb +2 -1
  38. data/lib/graph_weaver/codegen/nodes.rb +39 -6
  39. data/lib/graph_weaver/codegen/registry.rb +175 -0
  40. data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
  41. data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
  42. data/lib/graph_weaver/codegen.rb +406 -195
  43. data/lib/graph_weaver/coerce.rb +155 -26
  44. data/lib/graph_weaver/context_seam.rb +54 -0
  45. data/lib/graph_weaver/errors.rb +284 -46
  46. data/lib/graph_weaver/federation.rb +129 -27
  47. data/lib/graph_weaver/graph.rb +315 -0
  48. data/lib/graph_weaver/hints.rb +100 -24
  49. data/lib/graph_weaver/in_process.rb +27 -15
  50. data/lib/graph_weaver/input_struct.rb +119 -32
  51. data/lib/graph_weaver/internal/endpoint.rb +80 -0
  52. data/lib/graph_weaver/internal/headers.rb +70 -0
  53. data/lib/graph_weaver/internal/overrides.rb +67 -5
  54. data/lib/graph_weaver/internal/planner.rb +45 -15
  55. data/lib/graph_weaver/internal/refusal.rb +49 -0
  56. data/lib/graph_weaver/internal/schemas.rb +23 -9
  57. data/lib/graph_weaver/internal/selection.rb +34 -0
  58. data/lib/graph_weaver/internal/server_input.rb +251 -0
  59. data/lib/graph_weaver/internal/test_clients.rb +276 -0
  60. data/lib/graph_weaver/internal/unused.rb +287 -0
  61. data/lib/graph_weaver/internal/values.rb +40 -4
  62. data/lib/graph_weaver/internal.rb +249 -14
  63. data/lib/graph_weaver/log_subscriber.rb +74 -0
  64. data/lib/graph_weaver/logging.rb +163 -19
  65. data/lib/graph_weaver/query_module.rb +44 -3
  66. data/lib/graph_weaver/railtie.rb +237 -17
  67. data/lib/graph_weaver/representation.rb +55 -17
  68. data/lib/graph_weaver/result_struct.rb +90 -0
  69. data/lib/graph_weaver/retry.rb +45 -13
  70. data/lib/graph_weaver/rspec.rb +404 -93
  71. data/lib/graph_weaver/schema_loader.rb +266 -56
  72. data/lib/graph_weaver/tasks.rb +380 -89
  73. data/lib/graph_weaver/testing/cassette.rb +34 -10
  74. data/lib/graph_weaver/testing/endpoint.rb +107 -0
  75. data/lib/graph_weaver/testing/failure.rb +69 -12
  76. data/lib/graph_weaver/testing/fake_client.rb +164 -45
  77. data/lib/graph_weaver/testing/router.rb +64 -13
  78. data/lib/graph_weaver/testing.rb +200 -58
  79. data/lib/graph_weaver/transport/faraday.rb +41 -8
  80. data/lib/graph_weaver/transport/http.rb +48 -6
  81. data/lib/graph_weaver/transport.rb +134 -27
  82. data/lib/graph_weaver/version.rb +1 -1
  83. data/lib/graph_weaver.rb +495 -106
  84. metadata +71 -3
  85. 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
- they work identically over HTTP, Faraday, or in-process execution. A cassette
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 (below), since
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. A *request* with no recording raises
31
- `GraphWeaver::Testing::MissingRecording`, naming the variables it was called
32
- with and the ones recorded for that same query — what usually differs.
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
- is `String!` now — nothing notices until a spec dies mid-run on a cast error
42
- naming a struct and nothing else.
43
-
44
- ```sh
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" => "b1"}
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
- hand is skipped and counted rather than guessed at. Checking **none** of them
59
- fails too: a green run that compared nothing would pass whatever the recordings
60
- said. The fix is a re-record (`GRAPHWEAVER_RECORD=1`, with a live `client:`) —
61
- or `rake graph_weaver:generate`, if it was the schema dump that moved first.
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
- never reaches disk, and the caller sees the anonymized response too, so
67
- assertions written during the recording run still hold on replay.
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
- so they're walked by shape instead: keys, nesting and structure survive, every
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
- don't record that request.
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
- and nothing else. A password like `hunter2` has no shape, so a quiet run is not
107
- a clean bill of health: **read a cassette before committing it.**
108
-
109
- For cassettes recorded before the flag was on:
110
-
111
- ```sh
112
- rake graph_weaver:cassettes:anonymize # every cassette in cassette_dir, in place
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
- point — pagination quirks, which union member came back, where that server puts
128
- its nulls — and for pinning a regression.
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
- plain introspection result, so the whole JavaScript GraphQL editor toolchain
5
- works on a Ruby repo — **with no JS project, no `package.json`, and no `npm
6
- install`**. It just needs one config file telling it where the two live.
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. The paths are graph_weaver's conventions
22
- (`GraphWeaver.schema_path`, `queries_paths`, `fragments_paths`) — if you moved
23
- them, move these to match. Include the fragments directory: an editor
24
- validating a query that spreads a shared fragment reports `Unknown fragment`
25
- unless the fragment files are in `documents` too. The generator writes that
26
- line whether or not you have fragments yet the directory it names doesn't
27
- exist until you add one, and a glob matching nothing is fine.
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. graph_weaver also writes a
37
- `graph_weaver` provenance key alongside the introspection result; if some tool
38
- objects to it, point `schema:` at an SDL dump instead.
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
- The two editor plugins that read this file:
43
-
44
- - **[vscode-graphql](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql)**
45
- its README states it **requires** a graphql-config file, which is why
46
- nothing works without the YAML above.
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
- descriptions the API author wrote
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
- The same globs also feed the JS CI tools, if you want them (these *do* need
61
- npm, unlike the editor path):
62
- [graphql-inspector](https://the-guild.dev/graphql/inspector) `validate` and
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
- both, no warning that a struct went unused. The editor plugin understands
71
- GraphQL and Sorbet understands Ruby, and no tool in any ecosystem bridges the
72
- two except where documents and types share a single language service.
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