graph_weaver 0.4.6 → 0.5.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1442 -0
  3. data/Gemfile.lock +23 -23
  4. data/README.md +115 -96
  5. data/docs/cassettes.md +93 -46
  6. data/docs/editors.md +82 -0
  7. data/docs/errors.md +34 -30
  8. data/docs/federation.md +521 -48
  9. data/docs/generated_modules.md +352 -137
  10. data/docs/getting_started.md +237 -67
  11. data/docs/logging.md +35 -6
  12. data/docs/real_world.md +21 -15
  13. data/docs/scalars.md +49 -154
  14. data/docs/testing.md +300 -52
  15. data/docs/transports.md +129 -30
  16. data/docs/upgrading.md +134 -0
  17. data/graph_weaver.gemspec +19 -3
  18. data/lib/generators/graph_weaver/install_generator.rb +259 -0
  19. data/lib/graph_weaver/client.rb +118 -111
  20. data/lib/graph_weaver/codegen/aliases.rb +223 -0
  21. data/lib/graph_weaver/codegen/emit.rb +283 -261
  22. data/lib/graph_weaver/codegen/enum_type.rb +25 -124
  23. data/lib/graph_weaver/codegen/nodes.rb +72 -13
  24. data/lib/graph_weaver/codegen/scalar_type.rb +69 -66
  25. data/lib/graph_weaver/codegen/type_helpers.rb +140 -0
  26. data/lib/graph_weaver/codegen.rb +672 -336
  27. data/lib/graph_weaver/errors.rb +154 -16
  28. data/lib/graph_weaver/federation.rb +259 -0
  29. data/lib/graph_weaver/hints.rb +9 -1
  30. data/lib/graph_weaver/in_process.rb +90 -0
  31. data/lib/graph_weaver/input_struct.rb +14 -2
  32. data/lib/graph_weaver/logging.rb +29 -0
  33. data/lib/graph_weaver/parsing.rb +59 -0
  34. data/lib/graph_weaver/query_module.rb +55 -0
  35. data/lib/graph_weaver/railtie.rb +23 -1
  36. data/lib/graph_weaver/representation.rb +74 -0
  37. data/lib/graph_weaver/response.rb +7 -0
  38. data/lib/graph_weaver/retry.rb +29 -8
  39. data/lib/graph_weaver/rspec.rb +220 -16
  40. data/lib/graph_weaver/schema_loader.rb +819 -60
  41. data/lib/graph_weaver/schemas.rb +48 -0
  42. data/lib/graph_weaver/selection.rb +43 -8
  43. data/lib/graph_weaver/tasks.rb +220 -22
  44. data/lib/graph_weaver/testing/cassette.rb +249 -81
  45. data/lib/graph_weaver/testing/coverage.rb +160 -0
  46. data/lib/graph_weaver/testing/failure.rb +14 -25
  47. data/lib/graph_weaver/testing/fake_client.rb +182 -22
  48. data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
  49. data/lib/graph_weaver/testing/router.rb +1452 -0
  50. data/lib/graph_weaver/testing/subgraphs.rb +134 -0
  51. data/lib/graph_weaver/testing.rb +209 -13
  52. data/lib/graph_weaver/transport/faraday.rb +28 -10
  53. data/lib/graph_weaver/transport/http.rb +99 -36
  54. data/lib/graph_weaver/transport.rb +67 -14
  55. data/lib/graph_weaver/version.rb +1 -1
  56. data/lib/graph_weaver.rb +416 -172
  57. metadata +25 -9
  58. data/CLAUDE.md +0 -69
  59. data/Makefile +0 -23
  60. data/NOTES.md +0 -182
  61. data/PLAN.md +0 -144
data/docs/errors.md CHANGED
@@ -11,6 +11,7 @@ response = PersonQuery.execute(id: "1") # => GraphWeaver::Response[Result]
11
11
  response.data # T.nilable(Result) — typed, present even on partial success
12
12
  response.errors # Array[GraphWeaver::GraphQLError]
13
13
  response.errors? # any top-level errors?
14
+ response.success? # the same question the other way round
14
15
  response.extensions # { "cost" => … } — rides on success too
15
16
  response.data! # the Result, or raise GraphWeaver::QueryError
16
17
  ```
@@ -22,17 +23,29 @@ Every `GraphQLError` exposes `#message`, `#locations`, `#path`, `#extensions`,
22
23
  and `#code` (`extensions["code"]`) — match on the **code**, not the message
23
24
  string (`response.errors.first.code == "THROTTLED"`).
24
25
 
25
- Everything GraphWeaver raises descends from `GraphWeaver::Error`, split by where
26
- it failed:
26
+ Everything GraphWeaver *concludes* descends from `GraphWeaver::Error` a
27
+ transport failure, a rejected query, a response that wouldn't cast, a plan the
28
+ [local router](federation.md) refused, a subgraph map that doesn't add up. The
29
+ subclass says where it failed:
27
30
 
28
31
  | Class | When |
29
32
  |-------|------|
30
33
  | `TransportError` | never reached the server — DNS, connection refused, TLS, timeout |
31
- | `ServerError` | reached it, non-2xx HTTP — `#status`, `#body` |
32
- | `QueryError` | 200 body with top-level GraphQL errors — `#errors`, `#data`, `#extensions`, `#codes` |
34
+ | `ServerError` | reached it, non-2xx HTTP — `#status`, `#body`, `#headers`, `#retry_after`, `#throttled?` |
35
+ | `QueryError` | 200 body with top-level GraphQL errors — `#errors`, `#data`, `#extensions`, `#codes`, `#throttled?` |
33
36
  | `TypeError` | the response wouldn't cast into the generated structs — `#struct`, `#cause` |
34
- | `InputError` | the variables wouldn't build into the generated input structs — unknown/typo'd key, missing required field, out-of-range enum, wrong-typed field — `#field`, `#struct` |
37
+ | `InputError` | the variables wouldn't build into the generated input structs — unknown/typo'd key, missing required field, out-of-range enum, wrong-typed field, wrong number of @oneOf fields — `#field`, `#struct` |
35
38
  | `ValidationError` | build time: the query didn't validate against the schema |
39
+ | `Codegen::Aliases::UnknownSegment` | build time: an [`alias:`](generated_modules.md#flat-accessors-with-alias) path names a field no type here has — a typo, so `optional: true` won't skip it |
40
+ | `ConfigurationError` | setup judged against your schema — which Ruby schema serves which subgraph (`Testing::Router`, `federation:diff`) |
41
+ | `Testing::Unplannable` | the local test router won't plan this operation — `#category`, `#detail` |
42
+ | `Testing::MissingRecording` | a [cassette](cassettes.md) holds no entry for this request — the message prints the variables, and the ones it did record |
43
+
44
+ An argument that is wrong *on its face* raises a plain `ArgumentError` instead
45
+ (`pool_size: must be >= 1`, `cast: must be a Symbol, Proc, :itself, or nil`),
46
+ like any Ruby method — a bug at the call site, not a condition to rescue. The
47
+ line is whether the library had to read your schema to reach the verdict: it
48
+ did for `ConfigurationError`, which is why a spec helper can rescue that one.
36
49
 
37
50
  ```ruby
38
51
  begin
@@ -40,30 +53,22 @@ begin
40
53
  rescue GraphWeaver::TransportError
41
54
  retry # network blip
42
55
  rescue GraphWeaver::ServerError => e
43
- e.status >= 500 ? backoff : raise # retry 5xx; a 4xx is our bug
56
+ e.throttled? || e.status >= 500 ? backoff : raise # a plain 4xx is our bug
44
57
  rescue GraphWeaver::QueryError => e
45
- e.codes.include?("THROTTLED") ? backoff : raise
58
+ e.throttled? ? backoff : raise # the same question, asked of the errors array
46
59
  end
47
60
  ```
48
61
 
49
- Or skip the hand-rolling `Retry` wraps any transport with
50
- configurable retries:
51
-
52
- ```ruby
53
- transport = GraphWeaver::Retry.new(
54
- GraphWeaver::Transport::HTTP.new(url),
55
- tries: 5, # total attempts
56
- backoff: :exponential, # or :linear, or ->(attempt) { seconds }
57
- base: 0.5, max: 30, # seconds, clamped at max:
58
- jitter: true, # randomize each delay by 50-100%
59
- retry_codes: ["THROTTLED"], # also retry GraphQL errors by code
60
- )
61
- ```
62
+ `#throttled?` deliberately spells the same on both: an API may say "slow
63
+ down" with a 429 or with a `THROTTLED` error in a 200 body, and a caller
64
+ shouldn't have to know which. It recognizes the codes the big graphs
65
+ actually send (`GraphWeaver::GraphQLError::THROTTLE_CODES` — Shopify's
66
+ `THROTTLED`, GitHub's `RATE_LIMITED`, and friends); pass that constant to
67
+ `Retry`'s `retry_codes:` instead of hand-writing the strings.
62
68
 
63
- Defaults match the rescue block above: transport failures always retry,
64
- `ServerError` only on 5xx (a 4xx is your bug retrying won't fix it;
65
- override with `retry_if:`), and GraphQL-level codes only when listed in
66
- `retry_codes:`. Exhausting `tries:` re-raises the last error.
69
+ Or skip the hand-rolling: [`Retry`](transports.md#retries) wraps any client and
70
+ already defaults to exactly the policy abovetransport failures always,
71
+ `ServerError` on 5xx plus 408/429, and GraphQL error codes you name.
67
72
 
68
73
  **Top-level scalar variables** fail like any Ruby method call, *outside* the
69
74
  hierarchy on purpose — passing the wrong Ruby type for a scalar kwarg is a
@@ -73,10 +78,9 @@ String"), a missing required one a plain `ArgumentError` ("missing keyword: :id"
73
78
 
74
79
  **Input-object variables** are the caller-input case, so they're *inside* the
75
80
  hierarchy. When you pass an input object as a hash (or struct) it's built
76
- through the generated `coerce`, and any problem there — an unknown/typo'd key, a
77
- missing required field, an out-of-range enum, a wrong-typed field raises
78
- `GraphWeaver::InputError`. That's one rescue point for turning invalid input
79
- into a 422:
81
+ through the generated `coerce`, and anything wrong in there raises
82
+ `GraphWeaver::InputError` one rescue point for turning invalid input into a
83
+ 422:
80
84
 
81
85
  ```ruby
82
86
  rescue GraphWeaver::InputError => e
@@ -94,8 +98,8 @@ Business/validation failures returned *as data* (Shopify-style `userErrors { fie
94
98
  message code }`) aren't errors here — they're just fields you selected, so they
95
99
  deserialize onto `response.data` like anything else and you inspect them there.
96
100
 
97
- The one-shot `GraphWeaver.execute` / `execute!` mirror this: `execute` returns
98
- the envelope, `execute!` the result-or-raise.
101
+ The one-shot `GraphWeaver.run` / `run!` mirror this: `run` returns
102
+ the envelope, `run!` the result-or-raise.
99
103
 
100
104
  ## Extending TransportError
101
105