graph_weaver 0.7.0 → 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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/README.md +40 -88
  4. data/docs/alternatives.md +1 -7
  5. data/docs/cassettes.md +54 -59
  6. data/docs/editors.md +32 -47
  7. data/docs/errors.md +261 -369
  8. data/docs/federation.md +650 -837
  9. data/docs/generated_modules.md +370 -459
  10. data/docs/getting_started.md +211 -428
  11. data/docs/i18n.md +114 -177
  12. data/docs/logging.md +127 -116
  13. data/docs/real_world.md +26 -39
  14. data/docs/scalars.md +277 -310
  15. data/docs/testing.md +340 -486
  16. data/docs/transports.md +191 -263
  17. data/docs/upgrading.md +188 -560
  18. data/examples/README.md +38 -0
  19. data/examples/countries.rb +39 -0
  20. data/examples/federation.rb +62 -0
  21. data/examples/github/generate.rb +20 -0
  22. data/examples/github/generated/star_mutation.rb +126 -0
  23. data/examples/github/generated/stargazers_query.rb +232 -0
  24. data/examples/github/generated/starred_query.rb +151 -0
  25. data/examples/github/queries/star.graphql +8 -0
  26. data/examples/github/queries/stargazers.graphql +22 -0
  27. data/examples/github/queries/starred.graphql +11 -0
  28. data/examples/github/run.rb +43 -0
  29. data/examples/github/setup.rb +18 -0
  30. data/examples/rick_and_morty.rb +57 -0
  31. data/graph_weaver.gemspec +12 -3
  32. data/lib/graph_weaver/client.rb +22 -1
  33. data/lib/graph_weaver/codegen.rb +5 -1
  34. data/lib/graph_weaver/context_seam.rb +54 -0
  35. data/lib/graph_weaver/errors.rb +23 -15
  36. data/lib/graph_weaver/federation.rb +11 -2
  37. data/lib/graph_weaver/in_process.rb +15 -9
  38. data/lib/graph_weaver/internal/endpoint.rb +7 -5
  39. data/lib/graph_weaver/internal/headers.rb +19 -0
  40. data/lib/graph_weaver/internal.rb +66 -13
  41. data/lib/graph_weaver/log_subscriber.rb +10 -2
  42. data/lib/graph_weaver/logging.rb +33 -13
  43. data/lib/graph_weaver/query_module.rb +8 -0
  44. data/lib/graph_weaver/retry.rb +12 -8
  45. data/lib/graph_weaver/schema_loader.rb +52 -14
  46. data/lib/graph_weaver/testing/cassette.rb +28 -5
  47. data/lib/graph_weaver/testing/endpoint.rb +14 -13
  48. data/lib/graph_weaver/testing/fake_client.rb +33 -3
  49. data/lib/graph_weaver/testing/router.rb +7 -3
  50. data/lib/graph_weaver/transport/http.rb +2 -2
  51. data/lib/graph_weaver/transport.rb +47 -23
  52. data/lib/graph_weaver/version.rb +1 -1
  53. data/lib/graph_weaver.rb +22 -1
  54. metadata +16 -3
  55. data/CHANGELOG.md +0 -3801
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0953218f185ebe717f72c69937276325bc22f50eb9052547861857ba3274b062'
4
- data.tar.gz: 6affb87a7444cb7a6dcfb62335cbeb3b1342ac028c3b856899dd20ca76bb0fea
3
+ metadata.gz: e0ae9c23b2566bfa26c786e07703e4fad357cbd09a29112d3e0c9dd5d3af916c
4
+ data.tar.gz: fb5f4534174e5ba9cb7b9d323af26c2a13456c022f8682bcc3ccae5e512a1c96
5
5
  SHA512:
6
- metadata.gz: 61465762c1cf75a539d00e7d7236d7c5ab1872e0131ca523667bb56e7ced985ad4b4793764bdb15cd30174ef5141eb5f0f984ab97a96ed06cd81ac80f0a20212
7
- data.tar.gz: f72e17d16c336a66beb3f421dd4c0ae5754dae985d309eda617666189488c5ca6c9aea6082a55f228b169a886a2a61bd0bcb5eaf5d09458ed8171e50db9c1fd3
6
+ metadata.gz: dc780e4f155ea89b9977822a7bb62ed9880d3b7e707fd5046229b1c91c843a9c1ec37c39fb5e2039e71c9095dfb7a0df006347a19ce4544f75f2b3fe13a35a5c
7
+ data.tar.gz: 3dcc86b06e670408ebc18eaaaad7447d5825d1d580b9640ed8860133ba29908eb42553d52da4a2619eb23f072f4999f5aceb622271fafc8d5431fd57a3106ed3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graph_weaver (0.7.0)
4
+ graph_weaver (0.7.1)
5
5
  graphql (>= 2.6.7)
6
6
  sorbet-runtime
7
7
 
@@ -240,7 +240,7 @@ GEM
240
240
  crack (>= 0.3.2)
241
241
  hashdiff (>= 0.4.0, < 2.0.0)
242
242
  webrick (1.9.2)
243
- yard (0.9.44)
243
+ yard (0.9.45)
244
244
  zeitwerk (2.8.3)
245
245
 
246
246
  PLATFORMS
@@ -299,7 +299,7 @@ CHECKSUMS
299
299
  google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b
300
300
  google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee
301
301
  google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd
302
- graph_weaver (0.7.0)
302
+ graph_weaver (0.7.1)
303
303
  graphql (2.6.10) sha256=9b7c8633767f516ff9d48a8d6305b2a00a2101c82aa871b92e69086944f9f83e
304
304
  hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
305
305
  i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
@@ -367,7 +367,7 @@ CHECKSUMS
367
367
  useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
368
368
  webmock (3.26.4) sha256=8d8da206d217ebe6968cfb09c77f4533c23074e1432bad865f3994eacbaad50d
369
369
  webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
370
- yard (0.9.44) sha256=eb087e9b631ccd887b049f303d489963945452d5e2a7eb49a5a74a7cf6887f28
370
+ yard (0.9.45) sha256=52e211493f7cb8a3ebf7e104a25a1e73937a3103092545d34cb88fafebb3dc51
371
371
  zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
372
372
 
373
373
  BUNDLED WITH
data/README.md CHANGED
@@ -39,21 +39,15 @@ result.person&.nmae
39
39
  ```
40
40
 
41
41
  `person` is `T.nilable` because the schema says the field is nullable — the `&.`
42
- isn't defensive, it's the schema talking. A field you misspelled, or never
43
- selected, is a typecheck error rather than a `NoMethodError` in production.
44
- Static Sorbet is optional: `sorbet-runtime` is the only Sorbet gem this one
45
- needs, so if your app doesn't run `srb tc`, that same typo surfaces as a
46
- `NoMethodError` the first time the line runs instead of in CI — [Sorbet, with or
42
+ isn't defensive, it's the schema talking. Static Sorbet is optional:
43
+ `sorbet-runtime` is the only Sorbet gem this one needs, so in an app that doesn't
44
+ run `srb tc` the same typo surfaces as a `NoMethodError` the first time the line
45
+ runs, rather than in CI [Sorbet, with or
47
46
  without](docs/getting_started.md#sorbet-with-or-without).
48
47
 
49
48
  ## Start here
50
49
 
51
- ```ruby
52
- # Gemfile
53
- gem "graph_weaver"
54
- ```
55
-
56
- In Rails, setup is one command:
50
+ Add `gem "graph_weaver"` to your Gemfile. In Rails, the rest is one command:
57
51
 
58
52
  ```sh
59
53
  rails g graph_weaver:install https://api.example.com/graphql
@@ -63,41 +57,19 @@ which writes the initializer, the `app/graphql` layout, the editor config and th
63
57
  schema dump. **[Getting started](docs/getting_started.md)** walks the production
64
58
  setup end to end.
65
59
 
66
- You then rarely type `rake graph_weaver:generate` again: while the dev server is
67
- up, a `.graphql` edit regenerates and reloads before the next request, the way a
68
- route change does. Run it when you're ready to commit the Ruby.
69
-
70
- A second schema is a second `GraphWeaver.graph` its own queries, output,
71
- client, namespace and scalar registrations — and the same one command generates
72
- and verifies the app: **[more than one
73
- schema](docs/getting_started.md#more-than-one-schema)**.
74
-
75
- Or skip the build step and poke at an API from a console —
76
- anything holding a schema parses, and the module runs on what parsed it:
77
-
78
- ```ruby
79
- api = GraphWeaver.new("https://countries.trevorblades.com/")
80
- CountryQuery = api.parse("queries/country.graphql") # a path or a raw string
81
- CountryQuery.execute!(code: "JP").country&.capital # => "Tokyo"
82
-
83
- api.run!("query { continents { name } }").continents # or no module at all
84
- ```
85
-
86
- The **[examples](examples/)** run that path for real, smallest first: a public API
87
- in 30 lines, a paginated search, the production path against GitHub, and the
60
+ Or skip the build step and poke at an API from a console: `GraphWeaver.new(url)`
61
+ parses a query into a module on the spot, and `run!` executes one without a module
62
+ at all [against a real API](docs/real_world.md). The
63
+ **[examples](examples/)** run that path for real, smallest first: a public API in
64
+ 30 lines, a paginated search, the production path against GitHub, and the
88
65
  federated graph below.
89
66
 
90
67
  ## Precise types are expensive to fake, so it fakes them for you
91
68
 
92
69
  Generation makes result types exact, which makes them tedious to build by hand —
93
70
  and most generators stop there and leave you the fixtures. GraphWeaver ships the
94
- fakes. One line in the spec helper:
95
-
96
- ```ruby
97
- require "graph_weaver/rspec"
98
- ```
99
-
100
- then one tag says what an example runs against:
71
+ fakes. One line in the spec helper (`require "graph_weaver/rspec"`), then one tag
72
+ says what an example runs against:
101
73
 
102
74
  ```ruby
103
75
  it "shows the profile", graphql: :fake do
@@ -113,71 +85,51 @@ No fixture, no stub, no HTTP — and the values are seeded from rspec's own seed
113
85
  `--seed 4242` hands back that same person and a failure reproduces.
114
86
 
115
87
  Random data answers "does this render". When the example is *about* the data, pin
116
- the fields it's about and let the rest stay fabricated:
88
+ the fields it's about `graphql_fake("Person.name" => "Ada")`, or a whole type
89
+ off your factory — and everything else in the selection stays fabricated. Pins are
90
+ schema names, checked and spellchecked, so a typo raises rather than leaving the
91
+ example green against random data.
117
92
 
118
- ```ruby
119
- graphql_fake("Person.name" => "Ada", "Person.pets" => [{ "name" => "Shelby" }, {}])
120
-
121
- person.name # => "Ada"
122
- person.pets.first.name # => "Shelby" — the second pet is still fabricated
123
- person.pets.size # => 2 — a pinned list is as long as you write it
124
- ```
125
-
126
- Keys are schema names — a field, or a whole type: `"Person" => build(:person)`
127
- reads the selected fields off your factory's object and fabricates the rest. They
128
- are checked and spellchecked, so a typo raises instead of leaving the example
129
- green against random data. The tag also picks a *real* client
130
- when you want one: `:in_process` runs your resolvers, `:router` runs them across a
131
- federated graph, and `:wire` serves either at your own endpoint, so the transport
132
- you ship runs too. Field-level failure simulation and record/replay cassettes with
133
- anonymization are in [testing](docs/testing.md).
93
+ The tag also picks a *real* client when you want one: `:in_process` runs your
94
+ resolvers, `:router` runs them across a federated graph, and `:wire` serves either
95
+ at your own endpoint, so the transport you ship runs too. Field-level failure
96
+ simulation and record/replay cassettes with anonymization are in
97
+ [testing](docs/testing.md).
134
98
 
135
99
  ## Federation without a gateway
136
100
 
137
101
  When your app is both a GraphQL client and a subgraph, the local router plans a
138
102
  query across the composed supergraph and runs your **real resolvers** over the
139
- boundary — no gateway process, no node, no sockets. That's
140
- [`examples/federation.rb`](examples/federation.rb), the example that needs no
141
- network. Part of what it prints:
142
-
143
- ```
144
- fetches:
145
- → accounts root fields
146
- → reviews _entities × 1 User
147
- → products _entities × 2 Product
148
- ```
149
-
150
- The trace is the query plan: every node at a level goes in one `_entities` call,
151
- so two products cost one fetch. Anything it can't answer *faithfully* it refuses
152
- at plan time rather than guessing — the example prints one of those too, naming
153
- the coordinate that stopped it and what to rename. And it's diffed against a real
154
- `@apollo/gateway` over the same supergraph:
155
- currently 73 queries identical, 2 refused, 0 wrong
103
+ boundary — no gateway process, no node, no sockets. It prints the plan as it
104
+ fetches, batching every node at a level into one `_entities` call, and anything it
105
+ can't answer *faithfully* it refuses at plan time rather than guessing, naming the
106
+ coordinate that stopped it. It is diffed against a real `@apollo/gateway` over the
107
+ same supergraph: currently 73 queries identical, 2 refused, 0 wrong
156
108
  ([`spec/integration/router_parity_spec.rb`](spec/integration/router_parity_spec.rb)).
157
- See [federation](docs/federation.md).
109
+ [`examples/federation.rb`](examples/federation.rb) runs the whole thing with no
110
+ network; see [federation](docs/federation.md).
158
111
 
159
112
  ## The schema keeps itself honest
160
113
 
161
114
  The lifecycle is rake tasks, not a CI pipeline you assemble yourself:
162
115
  `schema:refresh` re-introspects the committed dump, `schema:diff` names what
163
- changed when whatever that dump came from an endpoint, your own schema class —
164
- has moved past it, `queries:check` names the queries that drift broke and where,
165
- `unused` names the selections your app stopped reading, and `verify` fails when
166
- the checked-in Ruby is stale.
167
- Generation is deterministic same schema and queries, byte-identical files — so
168
- regenerating never shows a diff you didn't earn. See
116
+ changed when whatever that dump came from has moved past it, `queries:check` names
117
+ the queries that drift broke and where, `unused` names the selections your app
118
+ stopped reading, and `verify` fails when the checked-in Ruby is stale. Generation
119
+ is deterministic — same schema and queries, byte-identical files so regenerating
120
+ never shows a diff you didn't earn. See
169
121
  [getting started](docs/getting_started.md#5-verify-in-ci).
170
122
 
171
- **Any release can change what codegen emits**, patch releases included fixing a
172
- generated type is a byte change. So `rake graph_weaver:generate` is part of every
173
- upgrade, and `verify` is what tells you when you've skipped it.
123
+ **Any release can change what codegen emits**, patch releases included. So `rake
124
+ graph_weaver:generate` is part of every upgrade, and `verify` is what tells you
125
+ when you've skipped it.
174
126
 
175
127
  #### Also in the box
176
128
 
177
- - **Queries and mutations** with typed variable kwargs — enums as `T::Enum`s, input objects as `T::Struct`s, required vs optional falling out of nullability and defaults. Results are generated per selection set; an input object has no selection set, so [its whole closure is](docs/generated_modules.md#an-input-object-generates-its-whole-closure)
178
- - **Fragments** (inline, named, type conditions), **unions and interfaces** (member structs, `__typename` dispatch), `@skip`/`@include` nullability
179
- - **Any transport**: in-process execution, a zero-dependency HTTP client, or Faraday with your own middleware plus a composable `Retry` with backoff and jitter
180
- - **Structured errors**: a typed envelope that keeps partial data and extensions, an error hierarchy split by failure site, field-level reports with entity ids, and stale-schema detection
129
+ - **Typed variable kwargs** — enums as `T::Enum`s, input objects as `T::Struct`s, required vs optional falling out of nullability and defaults
130
+ - **Fragments**, **unions and interfaces** (member structs, `__typename` dispatch), `@skip`/`@include` nullability
131
+ - **Any transport** in-process, a zero-dependency HTTP client, or Faraday with your own middleware, plus a composable `Retry`
132
+ - **Structured errors** an envelope that keeps partial data and extensions, a hierarchy split by failure site, field-level reports, stale-schema detection
181
133
 
182
134
  #### Dig deeper
183
135
 
data/docs/alternatives.md CHANGED
@@ -27,12 +27,6 @@ expensive.
27
27
  The honest counter: if your app doesn't use Sorbet, most of that value
28
28
  evaporates, and [graphlient](#graphlient) is the better answer.
29
29
 
30
- This idea was tried once before. `yogurt` generated Sorbet types from GraphQL
31
- documents, shipped two versions 70 minutes apart in November 2020, and stopped —
32
- its own README concedes it lacked named fragments and that the author "probably
33
- got a lot of the decisions wrong".<!-- https://raw.githubusercontent.com/theorygeek/yogurt/master/README.md ; versions 0.1.1 16:52 and 0.2.0 18:02 on 2020-11-26 — https://rubygems.org/api/v1/versions/yogurt.json ; last real commit 29ef902 2020-11-26 -->
34
- So the thesis is unproven, not proven wrong.
35
-
36
30
  ## The table
37
31
 
38
32
  `graphql-ruby` isn't a column because it isn't a client — see
@@ -53,7 +47,7 @@ vendor SDKs actually do.
53
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 |
54
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 |
55
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 |
56
- | **Last release** | v0.7.0, 2026-09-12 | 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 |
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 |
57
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 |
58
52
 
59
53
  ## graphql-client
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,20 +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.
33
30
 
34
- **One entry per request.** The request key is the query, its variables and the
35
- operation name together, and a cassette holds one entry per key: re-recording a
36
- request *replaces* its entry, and a request the file hasn't seen appends one. So
37
- however often `GRAPHWEAVER_RECORD=1` runs, no cassette ends up with two entries
38
- for the same request.
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.
39
37
 
40
38
  Editing a query changes the key, so the re-record writes a new entry and the old
41
39
  one stays behind — a recording of a request nothing sends any more.
42
- `cassettes:check` counts those ("1 not sent by any query module", below); the
43
- way to clear them is to delete the cassette and record it afresh.
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.
44
48
 
45
49
  ## Has a recording gone stale?
46
50
 
@@ -48,16 +52,11 @@ A cassette is the one artifact here recorded from *someone else's* server, and
48
52
  none of the other checks can see it drift: `verify` asks whether the generated
49
53
  Ruby is fresh, `queries:check` whether a query still validates, `schema:diff`
50
54
  whether the server's schema moved. When the recorded *answers* stop fitting the
51
- structs your schema generated — a field that was `Int!` when you recorded and
52
- is `String!` now — nothing notices until a spec dies mid-run on a cast error
53
- naming a struct and nothing else.
54
-
55
- ```sh
56
- rake graph_weaver:cassettes:check
57
- ```
58
-
59
- It replays every recording through the generated modules — no network — so it
60
- 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:
61
60
 
62
61
  ```
63
62
  spec/cassettes/dashboard.yml: 1 stale (3 checked, 1 not sent by any query module)
@@ -65,17 +64,17 @@ spec/cassettes/dashboard.yml: 1 stale (3 checked, 1 not sent by any query module
65
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
66
65
  ```
67
66
 
68
- A recording is matched to the module that sends its query, so one written by
69
- hand is skipped and counted rather than guessed at. Checking **none** of them
70
- fails too: a green run that compared nothing would pass whatever the recordings
71
- said. The fix is a re-record (`GRAPHWEAVER_RECORD=1`, with a live `client:`) —
72
- 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.
73
72
 
74
73
  ## Anonymization
75
74
 
76
- Cassettes hold real responses, so scrub them as they're recorded: real data
77
- never reaches disk, and the caller sees the anonymized response too, so
78
- 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.
79
78
 
80
79
  ```ruby
81
80
  GraphWeaver::Testing.configure do |config|
@@ -94,21 +93,21 @@ preserving everything that makes the recording faithful:
94
93
  | id *relationships* (same original id → same fake id) | the id values themselves |
95
94
 
96
95
  **Every plain string goes**, not the PII-shaped ones — nothing here can tell a
97
- user's name from a product's, so a recorded `"pikachu"` replays as `"name-1"`
98
- and an assertion pinned to it fails. Leave it off for a public, non-sensitive
99
- API, where the real values *are* the point of the cassette.
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.
100
99
 
101
100
  `data` is walked against the schema — which is why it needs one, to know which
102
- values are enums, dates, ids. `errors` and `extensions` have none behind them,
103
- 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
104
103
  string and number is replaced. `path`, `locations` and an error's
105
104
  `extensions.code` are kept, because they describe the request rather than the
106
105
  data — and call sites branch on `code` the way they branch on an enum.
107
106
 
108
107
  **The query and its variables are not anonymized.** They're the key replay
109
108
  matches on, so scrubbing them would make the recording unfindable. A mutation's
110
- input is often the sensitive part, so record with placeholder variables, or
111
- don't record that request.
109
+ input is often the sensitive part, so record with placeholder variables, or don't
110
+ record that request.
112
111
 
113
112
  Recording says so when the bytes it wrote look like a credential:
114
113
 
@@ -118,20 +117,16 @@ cassette is committed as written, so review this one first. …
118
117
  ```
119
118
 
120
119
  It recognizes tokens by shape — a JWT, `AKIA…`, `ghp_…`, `xox…`, `sk_live_…`, a
121
- PEM block, a `Bearer` header — which is every credential that is unmistakable
122
- and nothing else. A password like `hunter2` has no shape, so a quiet run is not
123
- a clean bill of health: **read a cassette before committing it.**
124
-
125
- For cassettes recorded before the flag was on:
126
-
127
- ```sh
128
- rake graph_weaver:cassettes:anonymize # every cassette in cassette_dir, in place
129
- ```
130
-
131
- Anonymization preserves shape, so an anonymized cassette still passes
132
- `cassettes:check` — including a custom scalar, whose replacement is the same
133
- one [`FakeClient`](testing.md#fabricated-data--graphql-fake) would fabricate.
134
- 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
135
130
  `Testing.config.overrides` (`{ "Money" => "12.00" }` — [pins](testing.md#pins)),
136
131
  which the anonymizer reads too; without one, anonymizing refuses rather than
137
132
  writing a value the codec can't read back.
@@ -139,6 +134,6 @@ writing a value the codec can't read back.
139
134
  ## Cassette or FakeClient?
140
135
 
141
136
  [FakeClient](testing.md) needs no recording and is the better default for unit
142
- tests. Reach for a cassette when the *shape* of a real API's answers is the
143
- point — pagination quirks, which union member came back, where that server puts
144
- 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