graph_weaver 0.2.0 → 0.2.2

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.
@@ -5,10 +5,244 @@
5
5
  # Mixed into Codegen — methods run with the generator instance state.
6
6
  class GraphWeaver::Codegen
7
7
  module Emit
8
+ include Kernel # for sorbet: hosts are Objects
8
9
  include GraphWeaver::Inflect
9
10
 
10
11
  private
11
12
 
13
+ # The Relay convention — an operation whose only variable is a required
14
+ # input object — reads better flattened: the input's fields become
15
+ # execute's kwargs directly, and the wrapping level is rebuilt on the
16
+ # wire. Multi-variable (or nullable-input) operations keep the
17
+ # variable-per-kwarg surface.
18
+ def flatten_input(variables)
19
+ return unless variables.size == 1
20
+
21
+ var = variables.first
22
+ return unless var.required && var.node.is_a?(NonNull)
23
+
24
+ input = var.node.of
25
+ input if input.is_a?(InputNode)
26
+ end
27
+
28
+ def input_references(node)
29
+ node.fields.filter_map do |field|
30
+ child = field.node
31
+ child = child.of while child.respond_to?(:of)
32
+ child if child.is_a?(InputNode)
33
+ end
34
+ end
35
+
36
+ # Input structs in dependency order (referenced types before their
37
+ # referrers) so each emitted const names an already-defined class.
38
+ # Cycles make that impossible — flagged so emission can forward-declare
39
+ # every input class first, then reopen each to add its props.
40
+ def ordered_inputs
41
+ ordered = []
42
+ seen = {} # node => :done | :visiting (bool_exp graphs get big)
43
+ cyclic = T.let(false, T::Boolean)
44
+
45
+ visit = lambda do |node|
46
+ next if seen[node] == :done
47
+
48
+ if seen[node] == :visiting
49
+ cyclic = true
50
+ next
51
+ end
52
+
53
+ seen[node] = :visiting
54
+ input_references(node).each(&visit)
55
+ seen[node] = :done
56
+ ordered << node
57
+ end
58
+ @variable_inputs.each_value(&visit)
59
+
60
+ [ordered, cyclic]
61
+ end
62
+
63
+ # Mapped-enum tables, generated enums, and input structs
64
+ # (dependency-ordered, forward-declared when cyclic) — inline in the
65
+ # module that needs them, or once in the shared inputs module.
66
+ def emit_variable_types(out)
67
+ @mapped_enums.each_value do |mapped|
68
+ emit_mapped_enum(mapped, out, 1)
69
+ out << ""
70
+ end
71
+ @variable_enums.each_value do |enum|
72
+ emit_enum(enum, out, 1)
73
+ out << ""
74
+ end
75
+ inputs, cyclic = ordered_inputs
76
+ if cyclic
77
+ # Recursive input types (Hasura bool_exp et al) reference each other,
78
+ # so no definition order satisfies the runtime — forward-declare every
79
+ # class empty, then let the full definitions below reopen with props.
80
+ # eval'd so srb sees only the full bodies (reopening a T::Struct to
81
+ # add props is a static error; adding them at runtime is fine).
82
+ out << " # runtime-only forward declarations: these input types reference"
83
+ out << " # each other, so the full definitions below need the constants"
84
+ out << " eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)"
85
+ inputs.each { |input| out << " class #{input.class_name} < T::Struct; end" }
86
+ out << " RUBY"
87
+ out << ""
88
+ end
89
+ inputs.each do |input|
90
+ emit_input(input, out, 1)
91
+ out << ""
92
+ end
93
+ end
94
+
95
+ # In the shared-inputs workflow the variable types live once in the
96
+ # inputs module; the query module aliases what it uses, so
97
+ # AdoptQuery::AdoptionInput stays a real constant — and shared types
98
+ # keep ONE identity across every module that touches them.
99
+ # Only the shared names THIS module's emission references: variable
100
+ # root types (and, when flattened, the root input's field types)
101
+ # plus every mapped-enum table (result casting reads them too).
102
+ # Nested types stay un-aliased — they live in the inputs module.
103
+ def shared_alias_names(variables, flatten)
104
+ nodes = variables.map(&:node)
105
+ nodes += flatten.fields.map(&:node) if flatten
106
+
107
+ names = @mapped_enums.each_value.flat_map { |m| ["#{m.const_prefix}_FROM_WIRE", "#{m.const_prefix}_TO_WIRE"] }
108
+ nodes.each do |wrapped|
109
+ node = T.let(wrapped, T.untyped)
110
+ node = node.of while node.is_a?(NonNull) || node.is_a?(List)
111
+ case node
112
+ when EnumNode, InputNode then names << node.class_name
113
+ end
114
+ end
115
+ names.uniq
116
+ end
117
+
118
+ def emit_shared_aliases(out, names)
119
+ return if names.empty?
120
+
121
+ names.each { |name| out << " #{name} = #{@inputs_namespace}::#{name}" }
122
+ out << ""
123
+ end
124
+
125
+ # The shared inputs artifact as files: inputs.rb (the manifest —
126
+ # requires, forward declarations for every struct so definition
127
+ # order never matters, then one require per type file) plus
128
+ # inputs/<type>.rb per enum/mapped-table/input struct.
129
+ def emit_inputs_files
130
+ files = {}
131
+ enum_files = []
132
+ struct_files = []
133
+
134
+ @mapped_enums.each do |graphql_name, mapped|
135
+ enum_files << inputs_file(files, graphql_name) { |out| emit_mapped_enum(mapped, out, 1) }
136
+ end
137
+ @variable_enums.each do |graphql_name, enum|
138
+ enum_files << inputs_file(files, graphql_name) { |out| emit_enum(enum, out, 1) }
139
+ end
140
+ inputs, = ordered_inputs
141
+ inputs.each do |input|
142
+ struct_files << inputs_file(files, input.class_name) { |out| emit_input(input, out, 1) }
143
+ end
144
+
145
+ manifest = []
146
+ manifest << "# typed: strict"
147
+ manifest << "# frozen_string_literal: true"
148
+ manifest << ""
149
+ manifest << "# Generated by GraphWeaver — do not edit. Shared variable types for"
150
+ manifest << "# this schema, one file per type; query modules alias what they use."
151
+ manifest << ""
152
+ requires = @requires.uniq.sort
153
+ if requires.any?
154
+ requires.each { |req| manifest << "require #{req.inspect}" }
155
+ manifest << ""
156
+ end
157
+ manifest << "module #{@module_name}; end"
158
+ manifest << ""
159
+ enum_files.sort.each { |file| manifest << "require_relative #{file.delete_suffix(".rb").inspect}" }
160
+ if inputs.any?
161
+ manifest << ""
162
+ manifest << "# runtime-only forward declarations: input types reference each"
163
+ manifest << "# other across files, so every constant must exist before any"
164
+ manifest << "# definition loads (srb sees only the full bodies)"
165
+ manifest << "module #{@module_name}"
166
+ manifest << " eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)"
167
+ inputs.each { |input| manifest << " class #{input.class_name} < T::Struct; end" }
168
+ manifest << " RUBY"
169
+ manifest << "end"
170
+ manifest << ""
171
+ struct_files.sort.each { |file| manifest << "require_relative #{file.delete_suffix(".rb").inspect}" }
172
+ end
173
+
174
+ files["inputs.rb"] = manifest.join("\n") + "\n"
175
+ files
176
+ end
177
+
178
+ # one type per file, wrapped in the namespace so bare sibling
179
+ # references resolve lexically
180
+ def inputs_file(files, name)
181
+ out = []
182
+ out << "# typed: strict"
183
+ out << "# frozen_string_literal: true"
184
+ out << ""
185
+ out << "# Generated by GraphWeaver — do not edit."
186
+ out << ""
187
+ out << "module #{@module_name}"
188
+ yield(out)
189
+ out << "end"
190
+
191
+ file = "inputs/#{GraphWeaver::Inflect.underscore(name)}.rb"
192
+ files[file] = out.join("\n") + "\n"
193
+ file
194
+ end
195
+
196
+ # The whole generated file: header, requires, the QUERY heredoc,
197
+ # enum tables, input structs (dependency-ordered, forward-declared
198
+ # when cyclic), the Result tree, and execute — assembled from the
199
+ # generator's walked state.
200
+ def emit_module(root, variables)
201
+ flatten = flatten_input(variables)
202
+ aliases = @inputs_namespace ? shared_alias_names(variables, flatten) : []
203
+
204
+ out = []
205
+ out << "# typed: strict"
206
+ out << "# frozen_string_literal: true"
207
+ out << ""
208
+ out << "# Generated by GraphWeaver — do not edit."
209
+ out << ""
210
+ requires = @requires.uniq.sort
211
+ if requires.any?
212
+ requires.each { |req| out << "require #{req.inspect}" }
213
+ out << ""
214
+ end
215
+ if aliases.any?
216
+ # the aliases below need the shared module loaded (same directory
217
+ # by the generate! convention)
218
+ out << "require_relative \"inputs\""
219
+ out << ""
220
+ end
221
+ out << "module #{@module_name}"
222
+ out << " extend T::Sig"
223
+ out << ""
224
+ # a GraphQL block string could contain a bare GRAPHQL line, which
225
+ # would terminate the heredoc early — pick a delimiter the query
226
+ # can't collide with
227
+ delimiter = "GRAPHQL"
228
+ delimiter += "_" while @query.match?(/^\s*#{delimiter}\s*$/)
229
+ out << " QUERY = T.let(<<~'#{delimiter}', String)"
230
+ @query.each_line { |line| out << " #{line}".rstrip }
231
+ out << " #{delimiter}"
232
+ out << ""
233
+ if @inputs_namespace
234
+ emit_shared_aliases(out, aliases)
235
+ else
236
+ emit_variable_types(out)
237
+ end
238
+ emit_nested(root, out, 1)
239
+ out << ""
240
+ emit_execute(out, variables, flatten:)
241
+ out << "end"
242
+
243
+ out.join("\n") + "\n"
244
+ end
245
+
12
246
  def emit_nested(node, out, indent)
13
247
  case node
14
248
  when UnionNode then emit_union(node, out, indent)
@@ -175,7 +409,30 @@ class GraphWeaver::Codegen
175
409
 
176
410
  out << ""
177
411
  out << " transport = GraphWeaver.resolve_transport(client || self.client)"
178
- out << " raw = transport.execute(QUERY, variables: variables).to_h"
412
+ out << " from_response(transport.execute(QUERY, variables: variables))"
413
+ out << " end"
414
+ out << ""
415
+ out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
416
+ out << " def self.execute!(#{kwargs.join(", ")})"
417
+ out << " execute(#{forward}).data!"
418
+ out << " end"
419
+ out << ""
420
+ emit_from_response(out)
421
+ end
422
+
423
+ # The network-free half of execute: deserialize a raw GraphQL response
424
+ # into the typed envelope. For responses fetched by any other client —
425
+ # execute delegates here after the transport call. Accepts the response
426
+ # hash (or anything with #to_h, e.g. a schema result): {"data" => ...,
427
+ # "errors" => ..., "extensions" => ...} with wire-cased string keys.
428
+ def emit_from_response(out)
429
+ out << " # Deserialize a raw GraphQL response into the typed envelope — the"
430
+ out << " # network-free half of execute, for responses fetched by any client."
431
+ out << " # Takes the response hash (or anything with #to_h): {\"data\" => ...,"
432
+ out << " # \"errors\" => ..., \"extensions\" => ...} with wire-cased string keys."
433
+ out << " sig { params(response: T.untyped).returns(GraphWeaver::Response[Result]) }"
434
+ out << " def self.from_response(response)"
435
+ out << " raw = response.to_h"
179
436
  out << " GraphWeaver::Response[Result].new("
180
437
  out << " data: (Result.from_h(raw[\"data\"]) if raw[\"data\"]),"
181
438
  out << " errors: (raw[\"errors\"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) },"
@@ -183,9 +440,10 @@ class GraphWeaver::Codegen
183
440
  out << " )"
184
441
  out << " end"
185
442
  out << ""
186
- out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
187
- out << " def self.execute!(#{kwargs.join(", ")})"
188
- out << " execute(#{forward}).data!"
443
+ out << " # from_response + data! — the typed result, or a raised QueryError."
444
+ out << " sig { params(response: T.untyped).returns(Result) }"
445
+ out << " def self.from_response!(response)"
446
+ out << " from_response(response).data!"
189
447
  out << " end"
190
448
  end
191
449
 
@@ -212,74 +470,30 @@ class GraphWeaver::Codegen
212
470
  end
213
471
  end
214
472
 
215
- # a module-level T::Struct per input type; serialize builds the wire
216
- # hash, omitting optional fields left nil
473
+ # A module-level T::Struct per input type: typed consts plus a FIELDS
474
+ # table the GraphWeaver::InputStruct runtime drives — serialize/to_h/
475
+ # coerce live once in the gem, not unrolled per struct (bool_exp
476
+ # schemas pull hundreds of inputs into one module).
217
477
  def emit_input(node, out, indent)
218
478
  pad = " " * indent
219
479
 
220
480
  out << "#{pad}class #{node.class_name} < T::Struct"
221
- out << "#{pad} extend T::Sig"
481
+ out << "#{pad} include GraphWeaver::InputStruct"
482
+ out << "#{pad} extend GraphWeaver::InputStruct::ClassMethods"
222
483
  out << ""
223
484
  node.fields.each do |field|
224
485
  default = field.required ? "" : ", default: nil"
225
486
  out << "#{pad} const :#{field.prop}, #{field.node.prop_type}#{default}"
226
487
  end
227
488
  out << ""
228
- # locals wear the reserved __gw prefix: prop readers are bare method
229
- # calls here, and a prop named "result" or "value" would otherwise
230
- # be shadowed — GraphQL reserves __-names, so no field can collide
231
- out << "#{pad} sig { returns(T::Hash[String, T.untyped]) }"
232
- out << "#{pad} def serialize"
233
- out << "#{pad} __gw_result = T.let({}, T::Hash[String, T.untyped])"
234
- node.fields.each do |field|
235
- if field.required || field.node.serialize_identity?
236
- value = field.node.serialize_identity? ? field.prop.to_s : field.node.serialize(field.prop.to_s, 1)
237
- line = "__gw_result[#{field.wire.inspect}] = #{value}"
238
- line += " unless #{field.prop}.nil?" unless field.required
239
- out << "#{pad} #{line}"
240
- else
241
- # bind a local so sorbet's flow-sensitivity narrows the nilable
242
- out << "#{pad} unless (__gw_value = #{field.prop}).nil?"
243
- out << "#{pad} __gw_result[#{field.wire.inspect}] = #{field.node.serialize("__gw_value", 1)}"
244
- out << "#{pad} end"
245
- end
246
- end
247
- out << "#{pad} __gw_result"
248
- out << "#{pad} end"
249
- out << ""
250
- out << "#{pad} # serialize, under the conventional name"
251
- out << "#{pad} sig { returns(T::Hash[String, T.untyped]) }"
252
- out << "#{pad} def to_h = serialize"
253
- out << ""
254
- out << "#{pad} # Build from a plain hash (underscored keys, Symbol or String):"
255
- out << "#{pad} # enums accept their wire values, nested inputs accept hashes;"
256
- out << "#{pad} # the struct's types are enforced on construction."
257
- out << "#{pad} sig { params(value: T.any(#{node.class_name}, T::Hash[T.untyped, T.untyped])).returns(#{node.class_name}) }"
258
- out << "#{pad} def self.coerce(value)"
259
- out << "#{pad} return value if value.is_a?(#{node.class_name})"
260
- out << ""
261
- out << "#{pad} # a typo'd key must not silently drop off the wire"
262
- out << "#{pad} GraphWeaver::Hints.validate_keys!(self, value)"
263
- out << ""
264
- out << "#{pad} new("
489
+ out << "#{pad} # (prop, wire, required, serializer, coercer) per field"
490
+ out << "#{pad} FIELDS = T.let(["
265
491
  node.fields.each do |field|
266
- raw = "value_at(value, :#{field.prop})"
267
- expr = if field.node.hash_coerce_identity?
268
- raw
269
- elsif field.required
270
- "#{raw}.then { |v1| #{field.node.hash_coerce("v1", 2)} }"
271
- else
272
- "#{raw}&.then { |v1| #{field.node.hash_coerce("v1", 2)} }"
273
- end
274
- out << "#{pad} #{field.prop}: #{expr},"
492
+ serializer = field.node.serialize_identity? ? "nil" : "->(v) { #{field.node.serialize("v", 1)} }"
493
+ coercer = field.node.hash_coerce_identity? ? "nil" : "->(v) { #{field.node.hash_coerce("v", 1)} }"
494
+ out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}),"
275
495
  end
276
- out << "#{pad} )"
277
- out << "#{pad} end"
278
- out << ""
279
- out << "#{pad} sig { params(hash: T::Hash[T.untyped, T.untyped], key: Symbol).returns(T.untyped) }"
280
- out << "#{pad} private_class_method def self.value_at(hash, key)"
281
- out << "#{pad} hash.key?(key) ? hash[key] : hash[key.to_s]"
282
- out << "#{pad} end"
496
+ out << "#{pad} ].freeze, T::Array[GraphWeaver::InputStruct::Field])"
283
497
  out << "#{pad}end"
284
498
  end
285
499
  end
@@ -1,11 +1,29 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "forwardable"
5
+
4
6
  # The typed intermediate representation of a query selection: one node
5
7
  # per GraphQL type shape, each knowing its Sorbet prop type and the
6
8
  # cast/serialize code to emit.
7
9
  class GraphWeaver::Codegen
8
- class Scalar
10
+ # Protocol defaults — subclasses override what differs. The full node
11
+ # protocol: bare_type, prop_type, cast(expr, depth), identity?,
12
+ # serialize(expr, depth), serialize_identity?, coerce?, coerce(expr),
13
+ # coerce_input_type, hash_coerce(expr, depth), hash_coerce_identity?,
14
+ # non_null?, nested.
15
+ class Node
16
+ def bare_type = raise(GraphWeaver::Error, "#{self.class} must define bare_type")
17
+ def prop_type = "T.nilable(#{bare_type})"
18
+ def identity? = false
19
+ def serialize_identity? = false
20
+ def coerce? = false
21
+ def hash_coerce_identity? = false
22
+ def non_null? = false
23
+ def nested = nil
24
+ end
25
+
26
+ class Scalar < Node
9
27
  # takes a resolved ScalarType — the generator picks it from the
10
28
  # client-scoped overlay or the global registry
11
29
  def initialize(scalar_type)
@@ -52,34 +70,27 @@ class GraphWeaver::Codegen
52
70
  end
53
71
 
54
72
  def hash_coerce_identity? = !@scalar.coerce?
55
-
56
- def non_null? = false
57
- def nested = nil
58
73
  end
59
74
 
60
- class NonNull
75
+ # NonNull is its inner node with the nilability stripped — everything
76
+ # else passes through.
77
+ class NonNull < Node
78
+ extend Forwardable
79
+
61
80
  attr_reader :of
62
81
 
82
+ def_delegators :@of, :bare_type, :cast, :identity?, :serialize, :serialize_identity?,
83
+ :coerce?, :coerce, :coerce_input_type, :hash_coerce, :hash_coerce_identity?, :nested
84
+
63
85
  def initialize(of)
64
86
  @of = of
65
87
  end
66
88
 
67
- def bare_type = @of.bare_type
68
89
  def prop_type = bare_type
69
- def cast(expr, depth) = @of.cast(expr, depth)
70
- def identity? = @of.identity?
71
- def serialize(expr, depth) = @of.serialize(expr, depth)
72
- def serialize_identity? = @of.serialize_identity?
73
- def coerce? = @of.coerce?
74
- def coerce(expr) = @of.coerce(expr)
75
- def coerce_input_type = @of.coerce_input_type
76
- def hash_coerce(expr, depth) = @of.hash_coerce(expr, depth)
77
- def hash_coerce_identity? = @of.hash_coerce_identity?
78
90
  def non_null? = true
79
- def nested = @of.nested
80
91
  end
81
92
 
82
- class List
93
+ class List < Node
83
94
  attr_reader :of
84
95
 
85
96
  def initialize(of)
@@ -90,10 +101,6 @@ class GraphWeaver::Codegen
90
101
  "T::Array[#{@of.prop_type}]"
91
102
  end
92
103
 
93
- def prop_type
94
- "T.nilable(#{bare_type})"
95
- end
96
-
97
104
  def cast(expr, depth)
98
105
  var = "v#{depth}"
99
106
  element = if @of.non_null? || @of.identity?
@@ -119,7 +126,6 @@ class GraphWeaver::Codegen
119
126
  end
120
127
 
121
128
  def serialize_identity? = @of.serialize_identity?
122
- def coerce? = false
123
129
 
124
130
  def hash_coerce(expr, depth)
125
131
  var = "v#{depth}"
@@ -133,11 +139,10 @@ class GraphWeaver::Codegen
133
139
  end
134
140
 
135
141
  def hash_coerce_identity? = @of.hash_coerce_identity?
136
- def non_null? = false
137
142
  def nested = @of.nested
138
143
  end
139
144
 
140
- class ObjectNode
145
+ class ObjectNode < Node
141
146
  Field = Struct.new(:prop, :key, :node)
142
147
 
143
148
  attr_reader :class_name, :fields
@@ -153,20 +158,14 @@ class GraphWeaver::Codegen
153
158
 
154
159
  def bare_type = class_name
155
160
 
156
- def prop_type
157
- "T.nilable(#{bare_type})"
158
- end
159
-
160
161
  def cast(expr, _depth)
161
162
  "#{class_name}.from_h(#{expr})"
162
163
  end
163
164
 
164
- def identity? = false
165
- def non_null? = false
166
165
  def nested = self
167
166
  end
168
167
 
169
- class EnumNode
168
+ class EnumNode < Node
170
169
  attr_reader :class_name, :values
171
170
 
172
171
  def initialize(class_name, values)
@@ -176,22 +175,14 @@ class GraphWeaver::Codegen
176
175
 
177
176
  def bare_type = class_name
178
177
 
179
- def prop_type
180
- "T.nilable(#{bare_type})"
181
- end
182
-
183
178
  def cast(expr, _depth)
184
179
  "#{class_name}.deserialize(#{expr})"
185
180
  end
186
181
 
187
- def identity? = false
188
-
189
182
  def serialize(expr, _depth)
190
183
  "#{expr}.serialize"
191
184
  end
192
185
 
193
- def serialize_identity? = false
194
-
195
186
  # enums always coerce: a kwarg or hash field accepts the T::Enum or
196
187
  # its wire value (deserialize raises on anything else)
197
188
  def coerce? = true
@@ -202,8 +193,6 @@ class GraphWeaver::Codegen
202
193
 
203
194
  def coerce_input_type = "T.any(#{class_name}, String)"
204
195
  def hash_coerce(expr, _depth) = coerce(expr)
205
- def hash_coerce_identity? = false
206
- def non_null? = false
207
196
  def nested = self
208
197
  end
209
198
 
@@ -211,7 +200,7 @@ class GraphWeaver::Codegen
211
200
  # generated enum class — instead module-level <NAME>_FROM_WIRE /
212
201
  # <NAME>_TO_WIRE constants translate at the boundary. fallback: makes
213
202
  # casting absorb unknown wire values (inputs stay strict).
214
- class MappedEnum
203
+ class MappedEnum < Node
215
204
  attr_reader :graphql_name, :mapping
216
205
 
217
206
  def initialize(enum_type, wire_values)
@@ -226,10 +215,6 @@ class GraphWeaver::Codegen
226
215
 
227
216
  def bare_type = @type_name
228
217
 
229
- def prop_type
230
- "T.nilable(#{bare_type})"
231
- end
232
-
233
218
  def cast(expr, _depth)
234
219
  if @fallback
235
220
  "#{const_prefix}_FROM_WIRE.fetch(#{expr}) { #{fallback_const} }"
@@ -238,14 +223,10 @@ class GraphWeaver::Codegen
238
223
  end
239
224
  end
240
225
 
241
- def identity? = false
242
-
243
226
  def serialize(expr, _depth)
244
227
  "#{const_prefix}_TO_WIRE.fetch(#{expr})"
245
228
  end
246
229
 
247
- def serialize_identity? = false
248
-
249
230
  # kwargs and hash fields accept the member or its wire value; unlike
250
231
  # casting, bad input raises even with a fallback (a typo'd input is
251
232
  # our bug, not server drift)
@@ -257,9 +238,6 @@ class GraphWeaver::Codegen
257
238
 
258
239
  def coerce_input_type = "T.any(#{@type_name}, String)"
259
240
  def hash_coerce(expr, _depth) = coerce(expr)
260
- def hash_coerce_identity? = false
261
- def non_null? = false
262
- def nested = nil
263
241
  end
264
242
 
265
243
  # A single-condition narrowing of an abstract field (`... on Pet { ... }`
@@ -267,7 +245,7 @@ class GraphWeaver::Codegen
267
245
  # nil when it doesn't — a non-match's response object carries no
268
246
  # matching fields, so the hash arrives empty. Always nilable, whatever
269
247
  # the schema's nullability, because narrowing filters.
270
- class NarrowedNode
248
+ class NarrowedNode < Node
271
249
  def initialize(of)
272
250
  @of = of
273
251
  end
@@ -275,20 +253,14 @@ class GraphWeaver::Codegen
275
253
  def class_name = @of.class_name
276
254
  def bare_type = @of.bare_type
277
255
 
278
- def prop_type
279
- "T.nilable(#{bare_type})"
280
- end
281
-
282
256
  def cast(expr, depth)
283
257
  "(#{expr}.empty? ? nil : #{@of.cast(expr, depth)})"
284
258
  end
285
259
 
286
- def identity? = false
287
- def non_null? = false
288
260
  def nested = @of
289
261
  end
290
262
 
291
- class UnionNode
263
+ class UnionNode < Node
292
264
  attr_reader :class_name, :members # graphql type name => ObjectNode
293
265
 
294
266
  def initialize(class_name, members)
@@ -298,16 +270,10 @@ class GraphWeaver::Codegen
298
270
 
299
271
  def bare_type = "#{class_name}::Type"
300
272
 
301
- def prop_type
302
- "T.nilable(#{bare_type})"
303
- end
304
-
305
273
  def cast(expr, _depth)
306
274
  "#{class_name}.from_h(#{expr})"
307
275
  end
308
276
 
309
- def identity? = false
310
- def non_null? = false
311
277
  def nested = self
312
278
  end
313
279
 
@@ -315,7 +281,7 @@ class GraphWeaver::Codegen
315
281
  # serialize produces the wire hash. Inputs never cast FROM the wire.
316
282
  # Joins the coerce protocol so execute kwargs accept plain hashes,
317
283
  # normalized (and type-checked) through the generated .coerce.
318
- class InputNode
284
+ class InputNode < Node
319
285
  Field = Struct.new(:prop, :wire, :node, :required)
320
286
 
321
287
  attr_reader :class_name, :fields
@@ -327,18 +293,12 @@ class GraphWeaver::Codegen
327
293
 
328
294
  def bare_type = class_name
329
295
 
330
- def prop_type
331
- "T.nilable(#{bare_type})"
332
- end
333
-
334
296
  def serialize(expr, _depth)
335
297
  "#{expr}.serialize"
336
298
  end
337
299
 
338
- def serialize_identity? = false
339
-
340
300
  def cast(_expr, _depth)
341
- raise NotImplementedError, "input objects are never cast from responses"
301
+ raise GraphWeaver::Error, "input objects are never cast from responses"
342
302
  end
343
303
 
344
304
  def coerce? = true
@@ -347,10 +307,5 @@ class GraphWeaver::Codegen
347
307
 
348
308
  # building a struct field from a caller-supplied plain hash value
349
309
  def hash_coerce(expr, _depth) = "#{class_name}.coerce(#{expr})"
350
- def hash_coerce_identity? = false
351
-
352
- def identity? = false
353
- def non_null? = false
354
- def nested = nil
355
310
  end
356
311
  end