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,70 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "time" # Time.httpdate, for Retry-After
5
+
6
+ module GraphWeaver
7
+ module Internal
8
+ # Response headers, as ServerError carries them. HTTP field names are
9
+ # case-insensitive (RFC 9110 §5.1) and the spelling a caller reaches for
10
+ # is the one the server sent ("Retry-After"), while transports store them
11
+ # downcased — so the name is folded on the way in and on the lookups that
12
+ # take one: #[], #fetch, #dig, #key?. Only lookup is forgiving: iteration,
13
+ # #keys and #to_h stay one spelling, which is what a log line and a doc
14
+ # can promise.
15
+ class Headers < Hash
16
+ # tells "no default given" from a default of nil
17
+ MISSING = Object.new
18
+ private_constant :MISSING
19
+
20
+ # A Hash of any casing as one of these; already one, untouched.
21
+ def self.wrap(headers)
22
+ return headers if headers.is_a?(self)
23
+
24
+ headers.each_with_object(new) { |(name, value), out| out.store(fold(name), value) }
25
+ end
26
+
27
+ def self.fold(name) = name.to_s.downcase
28
+
29
+ def [](name) = super(Headers.fold(name))
30
+
31
+ # spelled out rather than forwarded: sorbet can't splat into Hash#fetch's
32
+ # overloads, and a block beats a default there (Ruby warns if given both)
33
+ def fetch(name, default = MISSING, &block)
34
+ folded = Headers.fold(name)
35
+ return super(folded, &block) if block || default.equal?(MISSING)
36
+
37
+ super(folded, default)
38
+ end
39
+
40
+ # Hash#dig reads the slot directly rather than through #[], so the fold
41
+ # has to happen here; a header value is a String, so `rest` can only error
42
+ def dig(name, *rest)
43
+ value = self[name]
44
+ rest.empty? ? value : value&.dig(*rest)
45
+ end
46
+
47
+ # Seconds to wait per the server's Retry-After, which is either a delay
48
+ # in seconds or an HTTP-date. nil when absent or unparseable; a date
49
+ # already past clamps to 0. See RFC 9110 §10.2.3.
50
+ #
51
+ # Here rather than on ServerError because a rate limit reaches a caller
52
+ # two ways — raised, and returned as the envelope a 4xx/5xx WITH a
53
+ # GraphQL errors body makes — and both have to read the one rule.
54
+ def retry_after
55
+ value = self["retry-after"]&.strip
56
+ return if value.nil? || value.empty?
57
+ return value.to_f if value.match?(/\A\d+(\.\d+)?\z/)
58
+
59
+ [Time.httpdate(value) - Time.now, 0.0].max
60
+ rescue ArgumentError
61
+ nil
62
+ end
63
+
64
+ def key?(name) = super(Headers.fold(name))
65
+ alias_method :has_key?, :key?
66
+ alias_method :include?, :key?
67
+ alias_method :member?, :key?
68
+ end
69
+ end
70
+ end
@@ -10,6 +10,10 @@ module GraphWeaver
10
10
  # suite-wide ones while the block that set them is still on the stack,
11
11
  # and a fake built with its own checks them then.
12
12
  module Overrides
13
+ # The key a Hash `list_size:` says its fallback under — every list it
14
+ # doesn't name.
15
+ LIST_SIZE_DEFAULT = "default"
16
+
13
17
  class << self
14
18
  # A pin key names something in the schema: a type ("Money",
15
19
  # "Person"), a "Type.field" coordinate, or a bare field name
@@ -22,6 +26,44 @@ module GraphWeaver
22
26
  end
23
27
  end
24
28
 
29
+ # Whether `key` reads as a reference into the schema rather than a
30
+ # plain word — what tells a pin from an option at a fake's door, where
31
+ # both arrive as the same keywords. A "Type.field" coordinate can only
32
+ # be a pin; a bare word is one when the schema knows it. Whether the
33
+ # reference RESOLVES is validate!'s question, so a coordinate naming
34
+ # no type is still a pin and gets that refusal rather than "unknown
35
+ # option".
36
+ #
37
+ # Casing can't decide it: Hasura's types are lowercase, and
38
+ # `pokemon_v2_pokemon` read as a misspelled option.
39
+ def schema_reference?(schema, key)
40
+ key = key.to_s
41
+ return true if key.include?(".") || key.start_with?("__")
42
+
43
+ !schema.get_type(key).nil? || field_names(schema).include?(key)
44
+ end
45
+
46
+ # Every name a pin may be keyed by — the dictionary a refusal guesses
47
+ # from when a key is neither a pin nor an option.
48
+ def pin_names(schema) = schema.types.keys + field_names(schema)
49
+
50
+ # A Hash `list_size:` sizes one list at a time, keyed the way a pin is
51
+ # minus the bare type name: a type says nothing about how long any one
52
+ # of its fields is.
53
+ def validate_list_size!(schema, list_size)
54
+ return unless list_size.is_a?(Hash)
55
+
56
+ list_size.each do |key, value|
57
+ unless value.is_a?(Integer) || value.is_a?(Range)
58
+ raise GraphWeaver::Error, "list_size: #{key.to_s.inspect} must be an Integer or a " \
59
+ "Range — how long an unbounded list is — got #{value.inspect}"
60
+ end
61
+ next if key.to_s == LIST_SIZE_DEFAULT
62
+
63
+ validate_field_key!(schema, key.to_s, "list_size: key")
64
+ end
65
+ end
66
+
25
67
  # A pin that's a proc is handed the seeded Random when it takes one
26
68
  # and called bare when it doesn't, so a varying pin still reproduces
27
69
  # under `rspec --seed`.
@@ -55,16 +97,36 @@ module GraphWeaver
55
97
 
56
98
  # a leading capital names a type, as it does wherever a pin is written
57
99
  dictionary = type_name.match?(/\A[A-Z]/) ? schema.types.keys : known
58
- bad!(key, "matches no type or field in this schema", dictionary, type_name)
100
+ bad!("override key", key, "matches no type or field in this schema", dictionary, type_name)
101
+ end
102
+
103
+ coordinate!(schema, "override key", key, type_name, field_name)
104
+ end
105
+
106
+ # A key naming one FIELD — a "Type.field" coordinate, or a bare field
107
+ # name matching that field on any type.
108
+ def validate_field_key!(schema, key, label)
109
+ type_name, field_name = key.split(".", 2)
110
+ return if (field_name || type_name).start_with?("__")
111
+
112
+ if field_name.nil?
113
+ known = field_names(schema)
114
+ return if known.include?(type_name)
115
+
116
+ bad!(label, key, "matches no field in this schema", known, type_name)
59
117
  end
60
118
 
119
+ coordinate!(schema, label, key, type_name, field_name)
120
+ end
121
+
122
+ def coordinate!(schema, label, key, type_name, field_name)
61
123
  type = schema.get_type(type_name)
62
124
  unless type.respond_to?(:fields)
63
- bad!(key, "names no object type in this schema", schema.types.keys, type_name)
125
+ bad!(label, key, "names no object type in this schema", schema.types.keys, type_name)
64
126
  end
65
127
  return if type.fields.key?(field_name)
66
128
 
67
- bad!(key, "is not a field of #{type_name}", type.fields.keys, field_name)
129
+ bad!(label, key, "is not a field of #{type_name}", type.fields.keys, field_name)
68
130
  end
69
131
 
70
132
  # A type pin says what every value of that type is, and the fake only
@@ -84,10 +146,10 @@ module GraphWeaver
84
146
  "#{type.graphql_name}, and a pin fabricates a scalar, enum or object: #{advice}"
85
147
  end
86
148
 
87
- def bad!(key, problem, dictionary, term)
149
+ def bad!(label, key, problem, dictionary, term)
88
150
  suggestion = Util.did_you_mean(dictionary, term)
89
151
  hint = suggestion ? " — did you mean '#{suggestion}'?" : ""
90
- raise GraphWeaver::Error, "override key #{key.inspect} #{problem}#{hint}"
152
+ raise GraphWeaver::Error, "#{label} #{key.inspect} #{problem}#{hint}"
91
153
  end
92
154
 
93
155
  # Every output field name in the schema — walked only when a bare key
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "graphql"
5
5
 
6
+ require_relative "selection"
6
7
  require_relative "subgraphs"
7
8
 
8
9
  module GraphWeaver
@@ -155,7 +156,20 @@ module GraphWeaver
155
156
  .map { |error| Wire.graphql_error(error.message, "GRAPHQL_VALIDATION_FAILED") }
156
157
  end
157
158
 
159
+ # @defer/@stream send the rest of the answer in later payloads over a
160
+ # multipart body; this router answers in one. Public and document-wide
161
+ # because the caller has to ask it BEFORE validation — the composed API
162
+ # schema usually doesn't declare the directives, so graphql-ruby's
163
+ # "Directive @defer is not defined" fires first and shadows this.
164
+ def refuse_incremental!(document)
165
+ node = Selection.incremental_directive(document) or return
166
+
167
+ refuse :incremental_delivery,
168
+ "this operation carries @#{node.name}, and the answer would arrive in more than one payload"
169
+ end
170
+
158
171
  def plan(document, operation_name: nil)
172
+ refuse_incremental!(document)
159
173
  operation = pick_operation(document, operation_name)
160
174
  refuse(:operation_type, "this document is a subscription") if
161
175
  operation.operation_type == "subscription"
@@ -178,7 +192,7 @@ module GraphWeaver
178
192
  return plan
179
193
  end
180
194
 
181
- check_interface_objects!(root.graphql_name, selections, fragments) if @interface_objects.any?
195
+ check_reachable!(root.graphql_name, selections, fragments)
182
196
 
183
197
  entry = single_subgraph(root.graphql_name, selections, fragments)
184
198
  if entry
@@ -223,13 +237,14 @@ module GraphWeaver
223
237
 
224
238
  private
225
239
 
226
- # Every type these selections reach, refused if one of them is an
227
- # @interfaceObject: a subgraph resolves the whole interface there, so
228
- # the supergraph records no per-field routing for it and every fetch
229
- # planned against it would be a guess. Asked per query rather than at
230
- # construction one such directive shouldn't cost you the queries
231
- # that never touch the type.
232
- def check_interface_objects!(type_name, selections, fragments, depth = 0)
240
+ # Every position this operation reaches, refused if the supergraph says
241
+ # something about it no plan can honour whatever plan it gets. It runs
242
+ # here, above the verbatim shortcut, because that is the only place
243
+ # every plan passes: a check that lives in {#plan_step} silently skips
244
+ # the query one subgraph answers whole. Asked per query rather than at
245
+ # construction one such directive shouldn't cost you the queries that
246
+ # never touch it.
247
+ def check_reachable!(type_name, selections, fragments, depth = 0)
233
248
  return if depth > MAX_DEPTH
234
249
 
235
250
  selections.each do |node|
@@ -237,18 +252,20 @@ module GraphWeaver
237
252
  when GraphQL::Language::Nodes::Field
238
253
  next if node.name.start_with?("__")
239
254
 
255
+ contextual!(type_name, node)
256
+ progressive_override!(type_name, node)
240
257
  child = raw_child_type(type_name, node.name) or next
241
258
  interface_object!(child, "#{type_name}.#{node.name} returns #{child}")
242
- check_interface_objects!(child, node.selections, fragments, depth + 1)
259
+ check_reachable!(child, node.selections, fragments, depth + 1)
243
260
  when GraphQL::Language::Nodes::InlineFragment
244
261
  condition = node.type&.name || type_name
245
262
  interface_object!(condition, "this operation selects ... on #{condition}")
246
- check_interface_objects!(condition, node.selections, fragments, depth + 1)
263
+ check_reachable!(condition, node.selections, fragments, depth + 1)
247
264
  when GraphQL::Language::Nodes::FragmentSpread
248
265
  fragment = fragments[node.name] or next
249
266
  condition = fragment.type.name
250
267
  interface_object!(condition, "...#{node.name} is on #{condition}")
251
- check_interface_objects!(condition, fragment.selections, fragments, depth + 1)
268
+ check_reachable!(condition, fragment.selections, fragments, depth + 1)
252
269
  end
253
270
  end
254
271
  end
@@ -352,7 +369,6 @@ module GraphWeaver
352
369
 
353
370
  here = step(subgraph, type_name)
354
371
  selections.each do |node|
355
- contextual!(type_name, node)
356
372
  # a subtree that never leaves this subgraph goes over as written:
357
373
  # the boundary rules govern stitching, so they have no business
358
374
  # applying to a query that was never going to cross one
@@ -388,9 +404,9 @@ module GraphWeaver
388
404
 
389
405
  # A @fromContext argument is filled by the GATEWAY, out of a selection on
390
406
  # an ancestor — so a fetch this planner writes leaves it unset and the
391
- # field resolves from nothing. Asked only of a field the planner routes
392
- # itself: a subtree handed to one subgraph whole carries its own context,
393
- # which is why the verbatim path never reaches here.
407
+ # field resolves from nothing. A subgraph's own resolver never fills one
408
+ # either, so handing it the subtree whole doesn't help: this is asked of
409
+ # every field the operation reaches, on every path.
394
410
  def contextual!(type_name, node)
395
411
  names = @table.field(type_name, node.name)&.contextual
396
412
  return if names.nil? || names.empty?
@@ -400,6 +416,20 @@ module GraphWeaver
400
416
  "on its own"
401
417
  end
402
418
 
419
+ # A progressive @override(label:) leaves BOTH subgraphs resolving the
420
+ # field — the label is the rollout rule the gateway evaluates per
421
+ # request to pick between them. A plain @override drops the losing copy
422
+ # at composition, so there is nothing to decide and nothing to refuse;
423
+ # a labelled one is a coin only the gateway can toss.
424
+ def progressive_override!(type_name, node)
425
+ field = @table.field(type_name, node.name)
426
+ label = field&.override_label or return
427
+
428
+ refuse :progressive_override, "#{type_name}.#{node.name} is mid-rollout under " \
429
+ "@override(label: #{label.inspect}) — #{field.graphs.join(" and ")} both resolve it, " \
430
+ "and a local router can't evaluate a rollout percentage"
431
+ end
432
+
403
433
  # The keys a fetch injects are stripped from the answer, so a caller's
404
434
  # alias spelling one is stripped with it — silently, since the two are
405
435
  # then indistinguishable. Asked of what this fetch actually injects
@@ -0,0 +1,49 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module GraphWeaver
5
+ module Internal
6
+ # The verdict a coercion refusal already reached, riding on the plain
7
+ # ::TypeError / ::ArgumentError Coerce raises rather than on a class of
8
+ # its own — so `rescue ::TypeError` still catches it and Coerce keeps
9
+ # complaining the way Kernel#Integer does.
10
+ #
11
+ # The layers that brand a failure (InputStruct.field, Coerce.variable)
12
+ # read it off the exception instead of deriving a kind from the message.
13
+ # A message is prose; reading a verdict back out of one would be a guess,
14
+ # and a wrong `kind` is worse than none — the app will have translated it
15
+ # into a confident sentence.
16
+ module Refusal
17
+ attr_accessor :graph_weaver_kind, :graph_weaver_details
18
+
19
+ class << self
20
+ # Tag an exception with its verdict; returns it, ready to raise.
21
+ def brand(error, kind, **details)
22
+ error.extend(Refusal)
23
+ error.graph_weaver_kind = kind
24
+ error.graph_weaver_details = details
25
+ error
26
+ end
27
+
28
+ # What a raised exception says went wrong, as an InputError kind.
29
+ # Coerce's own refusals carry their verdict; an app's `cast:` or
30
+ # `serialize:` raises whatever it likes, and Ruby's own convention
31
+ # splits the two it raises for a value it can't use: TypeError means
32
+ # the CLASS was wrong, ArgumentError the CONTENT — which is the
33
+ # difference between "send something else" and "fix the text".
34
+ # Anything else is :refused rather than a guess at what it meant.
35
+ def kind_of(error)
36
+ return error.graph_weaver_kind if error.is_a?(Refusal)
37
+
38
+ case error
39
+ when ::TypeError then :type_mismatch
40
+ when ::ArgumentError then :unparseable
41
+ else :refused
42
+ end
43
+ end
44
+
45
+ def details_of(error) = error.is_a?(Refusal) ? error.graph_weaver_details : {}
46
+ end
47
+ end
48
+ end
49
+ end
@@ -27,20 +27,34 @@ module GraphWeaver
27
27
  end
28
28
 
29
29
  # Does this schema carry the coordinate — "Type", or "Type.field"?
30
- def defines?(schema, coordinate)
31
- type_name, field_name = coordinate.split(".", 2)
32
- type = schema.get_type(type_name) or return false
33
- return true unless field_name
34
-
35
- return type.fields.key?(field_name) if type.respond_to?(:fields)
36
- # an input object's members are arguments, not fields
37
- return type.arguments.key?(field_name) if type.respond_to?(:arguments)
30
+ def defines?(schema, coordinate) = !member(schema, coordinate).nil?
38
31
 
39
- false
32
+ # The type this schema gives "Type.field", printed the way SDL prints
33
+ # it — so it compares directly against a supergraph's own spelling.
34
+ # Presence is the cheaper question and answers a different one: a
35
+ # field that is still *there* can have been retyped underneath the
36
+ # composition, which reads as a match until the types are compared.
37
+ def signature(schema, coordinate)
38
+ member = member(schema, coordinate)
39
+ member.type.to_type_signature if member.respond_to?(:type)
40
40
  end
41
41
 
42
42
  private
43
43
 
44
+ # The field (or, on an input object, the argument — its members are
45
+ # arguments, not fields) at "Type.field", or the type itself for a
46
+ # bare coordinate.
47
+ def member(schema, coordinate)
48
+ type_name, field_name = coordinate.split(".", 2)
49
+ type = schema.get_type(type_name) or return
50
+ return type unless field_name
51
+
52
+ return type.fields[field_name] if type.respond_to?(:fields)
53
+ return type.arguments[field_name] if type.respond_to?(:arguments)
54
+
55
+ nil
56
+ end
57
+
44
58
  def descendants(klass)
45
59
  klass.subclasses.flat_map { |subclass| [subclass] + descendants(subclass) }
46
60
  end
@@ -12,6 +12,40 @@ module GraphWeaver
12
12
  module Selection
13
13
  include Kernel # for sorbet: hosts are Objects
14
14
 
15
+ # The directives that ask for the answer in instalments. Neither the
16
+ # generated code nor the local router reads a multipart body, and both
17
+ # have to say so BY NAME: whether the schema in hand declares @defer is
18
+ # graphql-ruby's business, so "Directive @defer is not defined" is an
19
+ # accident that happens to refuse — and it stops happening the day a
20
+ # supergraph @links the defer spec.
21
+ INCREMENTAL = %w[defer stream].freeze
22
+
23
+ # The first @defer/@stream node anywhere in a parsed document, or nil.
24
+ # A module function rather than part of the walk below: the callers ask
25
+ # before they have a schema, an operation, or a host to walk with.
26
+ def self.incremental_directive(node)
27
+ if node.respond_to?(:directives)
28
+ # a spread carries directives and no selections (`...Frag @defer`),
29
+ # so this is asked of every node rather than only of the ones below
30
+ applied = node.directives.find { |d| INCREMENTAL.include?(d.name) }
31
+ return applied if applied
32
+ end
33
+
34
+ children = if node.is_a?(GraphQL::Language::Nodes::Document)
35
+ node.definitions
36
+ elsif node.respond_to?(:selections)
37
+ node.selections
38
+ else
39
+ []
40
+ end
41
+
42
+ children.each do |child|
43
+ nested = incremental_directive(child)
44
+ return nested if nested
45
+ end
46
+ nil
47
+ end
48
+
15
49
  # Every method here becomes an instance method of its host (Codegen,
16
50
  # FakeClient, the cassette Anonymizer) — private so the walk stays the
17
51
  # host's own business rather than part of its API.