graph_weaver 0.5.0 → 0.6.0

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +537 -0
  3. data/Gemfile.lock +19 -19
  4. data/README.md +74 -53
  5. data/docs/cassettes.md +29 -4
  6. data/docs/editors.md +3 -1
  7. data/docs/errors.md +75 -16
  8. data/docs/federation.md +206 -155
  9. data/docs/generated_modules.md +223 -166
  10. data/docs/getting_started.md +106 -82
  11. data/docs/logging.md +35 -5
  12. data/docs/scalars.md +119 -24
  13. data/docs/testing.md +196 -155
  14. data/docs/transports.md +47 -19
  15. data/docs/upgrading.md +243 -22
  16. data/graph_weaver.gemspec +16 -2
  17. data/lib/generators/graph_weaver/install_generator.rb +31 -16
  18. data/lib/graph_weaver/client.rb +52 -15
  19. data/lib/graph_weaver/codegen/aliases.rb +15 -8
  20. data/lib/graph_weaver/codegen/emit.rb +107 -42
  21. data/lib/graph_weaver/codegen/enum_type.rb +4 -3
  22. data/lib/graph_weaver/codegen/nodes.rb +42 -21
  23. data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
  24. data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
  25. data/lib/graph_weaver/codegen.rb +382 -105
  26. data/lib/graph_weaver/coerce.rb +113 -0
  27. data/lib/graph_weaver/errors.rb +57 -13
  28. data/lib/graph_weaver/federation.rb +10 -22
  29. data/lib/graph_weaver/hints.rb +76 -2
  30. data/lib/graph_weaver/in_process.rb +11 -8
  31. data/lib/graph_weaver/inflect.rb +2 -0
  32. data/lib/graph_weaver/input_struct.rb +115 -12
  33. data/lib/graph_weaver/internal/overrides.rb +101 -0
  34. data/lib/graph_weaver/internal/planner.rb +868 -0
  35. data/lib/graph_weaver/internal/schemas.rb +50 -0
  36. data/lib/graph_weaver/internal/selection.rb +127 -0
  37. data/lib/graph_weaver/{testing → internal}/subgraphs.rb +45 -43
  38. data/lib/graph_weaver/internal/values.rb +181 -0
  39. data/lib/graph_weaver/internal.rb +206 -0
  40. data/lib/graph_weaver/logging.rb +108 -20
  41. data/lib/graph_weaver/parsing.rb +6 -13
  42. data/lib/graph_weaver/query_module.rb +2 -0
  43. data/lib/graph_weaver/railtie.rb +113 -14
  44. data/lib/graph_weaver/representation.rb +30 -2
  45. data/lib/graph_weaver/response.rb +15 -0
  46. data/lib/graph_weaver/retry.rb +54 -22
  47. data/lib/graph_weaver/rspec.rb +63 -18
  48. data/lib/graph_weaver/schema_diff.rb +293 -0
  49. data/lib/graph_weaver/schema_loader.rb +126 -35
  50. data/lib/graph_weaver/tasks.rb +88 -36
  51. data/lib/graph_weaver/testing/cassette.rb +131 -78
  52. data/lib/graph_weaver/testing/coverage.rb +11 -15
  53. data/lib/graph_weaver/testing/failure.rb +14 -8
  54. data/lib/graph_weaver/testing/fake_client.rb +253 -60
  55. data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
  56. data/lib/graph_weaver/testing/router.rb +147 -840
  57. data/lib/graph_weaver/testing.rb +40 -83
  58. data/lib/graph_weaver/transport/faraday.rb +1 -1
  59. data/lib/graph_weaver/transport/http.rb +29 -12
  60. data/lib/graph_weaver/transport.rb +11 -34
  61. data/lib/graph_weaver/version.rb +1 -1
  62. data/lib/graph_weaver.rb +221 -118
  63. metadata +17 -13
  64. data/CLAUDE.md +0 -161
  65. data/DECISIONS.md +0 -309
  66. data/Makefile +0 -23
  67. data/NOTES.md +0 -182
  68. data/PLAN.md +0 -115
  69. data/REVIEW.md +0 -946
  70. data/lib/graph_weaver/schemas.rb +0 -46
  71. data/lib/graph_weaver/selection.rb +0 -120
  72. data/lib/graph_weaver/testing/values.rb +0 -98
@@ -1,6 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "../version"
5
+
4
6
  # Source emission: turns the node tree into the generated module text.
5
7
  # Mixed into Codegen — methods run with the generator instance state.
6
8
  class GraphWeaver::Codegen
@@ -137,7 +139,7 @@ class GraphWeaver::Codegen
137
139
  out << "# typed: strict"
138
140
  out << "# frozen_string_literal: true"
139
141
  out << ""
140
- out << "# Generated by GraphWeaver — do not edit. Shared types for this schema —"
142
+ out << "# Generated by GraphWeaver #{GraphWeaver::VERSION} — do not edit. Shared types for this schema —"
141
143
  out << "# input types, enums, and unions hoisted from shared fragments — one file"
142
144
  out << "# per type; query modules alias what they use."
143
145
  out << ""
@@ -146,13 +148,13 @@ class GraphWeaver::Codegen
146
148
  requires.each { |req| out << "require #{req.inspect}" }
147
149
  out << ""
148
150
  end
149
- out << "module #{@module_name}; end"
151
+ out << "module #{@name}; end"
150
152
  out << ""
151
153
  if inputs.any?
152
154
  out << "# runtime-only forward declarations: input types reference each other"
153
155
  out << "# across files, so every constant must exist before any definition loads"
154
156
  out << "# (srb sees only the full bodies)"
155
- out << "module #{@module_name}"
157
+ out << "module #{@name}"
156
158
  out << " eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)"
157
159
  inputs.each { |input| out << " class #{input.class_name} < T::Struct; end" }
158
160
  out << " RUBY"
@@ -180,9 +182,9 @@ class GraphWeaver::Codegen
180
182
  out << "# typed: strict"
181
183
  out << "# frozen_string_literal: true"
182
184
  out << ""
183
- out << "# Generated by GraphWeaver — do not edit."
185
+ out << "# Generated by GraphWeaver #{GraphWeaver::VERSION} — do not edit."
184
186
  out << ""
185
- out << "module #{@module_name}"
187
+ out << "module #{@name}"
186
188
  yield(out)
187
189
  out << "end"
188
190
 
@@ -210,7 +212,7 @@ class GraphWeaver::Codegen
210
212
  out << "# typed: strict"
211
213
  out << "# frozen_string_literal: true"
212
214
  out << ""
213
- out << "# Generated by GraphWeaver — do not edit."
215
+ out << "# Generated by GraphWeaver #{GraphWeaver::VERSION} — do not edit."
214
216
  out << ""
215
217
  requires = @requires.uniq.sort
216
218
  if requires.any?
@@ -223,7 +225,7 @@ class GraphWeaver::Codegen
223
225
  out << "require_relative \"types\""
224
226
  out << ""
225
227
  end
226
- out << "module #{@module_name}"
228
+ out << "module #{@name}"
227
229
  out << " extend T::Sig" << "" if GraphWeaver.extend_t_sig?
228
230
  # a GraphQL block string could contain a bare GRAPHQL line, which
229
231
  # would terminate the heredoc early — pick a delimiter the query
@@ -266,7 +268,10 @@ class GraphWeaver::Codegen
266
268
  out << ""
267
269
  out << " # #{node.graphql_type} #{node.key_fields.map { |set| "@key(fields: #{set.inspect})" }.join(" ")}"
268
270
  sig = node.params.map { |param| "#{param.kwarg}: #{param.type}" }.join(", ")
269
- out << " sig { params(#{sig}).returns(T::Hash[String, T.untyped]) }"
271
+ # .checked(:never), as on execute: an untyped value (a Rails param)
272
+ # reaches the coercion in the body instead of sorbet-runtime's
273
+ # argument check; srb tc still holds typed call sites.
274
+ out << " sig { params(#{sig}).returns(T::Hash[String, T.untyped]).checked(:never) }"
270
275
  kwargs = node.params.map { |param| param.required ? "#{param.kwarg}:" : "#{param.kwarg}: nil" }.join(", ")
271
276
  out << " def self.#{node.method_name}(#{kwargs})"
272
277
  out << " GraphWeaver::Representation.build(#{node.graphql_type.inspect}, {"
@@ -369,9 +374,10 @@ class GraphWeaver::Codegen
369
374
  end
370
375
  out << "#{pad} )"
371
376
  out << "#{pad} rescue GraphWeaver::Error"
372
- out << "#{pad} raise # already branded by a nested struct — keep the innermost context"
373
- out << "#{pad} rescue StandardError => e" # a scalar's cast may raise anything
374
- out << "#{pad} raise GraphWeaver::TypeError.new(struct: self, error: e)"
377
+ out << "#{pad} raise # already branded by a nested struct or leaf — keep the innermost context"
378
+ out << "#{pad} rescue StandardError => e" # sorbet's prop check, mostly
379
+ out << "#{pad} raise GraphWeaver::TypeError.new(struct: self, " \
380
+ "message: GraphWeaver::Hints.cast_message(self, data, e))"
375
381
  out << "#{pad} end"
376
382
 
377
383
  # alias delegators (extend_type alias:) — typed accessors that project a
@@ -430,6 +436,8 @@ class GraphWeaver::Codegen
430
436
  out << ""
431
437
  out << " # the baked default client, resolved on first use"
432
438
  out << " DEFAULT_CLIENT = T.let(-> { #{@client_const} }, T.proc.returns(T.untyped))"
439
+ # QueryModule reads it with const_get, which privacy doesn't block
440
+ out << " private_constant :DEFAULT_CLIENT"
433
441
  end
434
442
  out << ""
435
443
 
@@ -445,23 +453,58 @@ class GraphWeaver::Codegen
445
453
  ordered = required + optional
446
454
 
447
455
  sig_params = ordered.map do |var|
448
- bare = var.node.coerce? ? var.node.coerce_input_type : var.node.bare_type
456
+ bare = var.node.input_type
449
457
  kwarg_type = var.required || bare == "T.untyped" ? bare : "T.nilable(#{bare})"
450
458
  "#{var.kwarg}: #{kwarg_type}"
451
459
  end
452
460
  sig_params << "client: T.untyped"
453
461
 
462
+ # GraphQL tells an absent variable from an explicit null; a Ruby kwarg
463
+ # with a nil default cannot. The default expression runs exactly when
464
+ # the keyword was left out, which is the missing third state — so
465
+ # `execute(bio: nil)` clears a bio and `execute()` leaves it alone.
466
+ # Only where the schema permits null: a non-null variable can't take
467
+ # one, so nil there still means omit and let its default apply.
468
+ omitted = optional.reject { |var| var.node.non_null? }
469
+ .to_h { |var| [var, "#{var.kwarg}_omitted"] }
470
+
454
471
  kwargs = required.map { |var| "#{var.kwarg}:" } +
455
- optional.map { |var| "#{var.kwarg}: nil" } + ["client: nil"]
472
+ optional.map { |var|
473
+ flag = omitted[var]
474
+ flag ? "#{var.kwarg}: (#{flag} = true; nil)" : "#{var.kwarg}: nil"
475
+ } + ["client: nil"]
456
476
 
457
- # execute returns the full envelope; execute! is the strict shortcut for
458
- # `execute(...).data!` — the typed result, or a raised QueryError.
459
- # kwargs forward via hash shorthand (key == value)
460
- forward = (ordered.map { |var| "#{var.kwarg}:" } + ["client:"]).join(", ")
477
+ call = "client_for(client).execute(QUERY, variables:, operation_name: OPERATION_NAME)"
461
478
 
462
- out << " sig { params(#{sig_params.join(", ")}).returns(GraphWeaver::Response[Result]) }"
479
+ # execute returns the full envelope; execute! is the strict shortcut for
480
+ # the typed result, or a raised QueryError.
481
+ out << " # .checked(:never): an untyped value (a Rails param) reaches the coercion below"
482
+ out << " # instead of sorbet-runtime's argument check; srb tc still holds typed call sites."
483
+ out << " sig { params(#{sig_params.join(", ")}).returns(GraphWeaver::Response[Result]).checked(:never) }"
463
484
  out << " def self.execute(#{kwargs.join(", ")})"
485
+ emit_variables(out, required, optional, omitted)
486
+ out << ""
487
+ out << " from_response(#{call})"
488
+ out << " end"
489
+ out << ""
490
+ out << " sig { params(#{sig_params.join(", ")}).returns(Result).checked(:never) }"
491
+ out << " def self.execute!(#{kwargs.join(", ")})"
492
+ if omitted.empty?
493
+ # kwargs forward via hash shorthand (key == value)
494
+ out << " execute(#{(ordered.map { |var| "#{var.kwarg}:" } + ["client:"]).join(", ")}).data!"
495
+ else
496
+ # repeats the variables rather than delegating: which keywords were
497
+ # left out is exactly what a Ruby call can't forward
498
+ emit_variables(out, required, optional, omitted)
499
+ out << ""
500
+ out << " from_response(#{call}).data!"
501
+ end
502
+ out << " end"
503
+ out << ""
504
+ emit_from_response(out)
505
+ end
464
506
 
507
+ def emit_variables(out, required, optional, omitted)
465
508
  if required.empty?
466
509
  out << " variables = {}"
467
510
  else
@@ -472,19 +515,15 @@ class GraphWeaver::Codegen
472
515
  out << " }"
473
516
  end
474
517
  optional.each do |var|
475
- out << " variables[#{var.wire.inspect}] = #{variable_serialize(var)} unless #{var.kwarg}.nil?"
518
+ value = variable_serialize(var)
519
+ if (flag = omitted[var])
520
+ # nil is a value here, so the serializer has to be stepped around
521
+ value = "(#{var.kwarg}.nil? ? nil : #{value})" unless value == var.kwarg
522
+ out << " variables[#{var.wire.inspect}] = #{value} unless #{flag}"
523
+ else
524
+ out << " variables[#{var.wire.inspect}] = #{value} unless #{var.kwarg}.nil?"
525
+ end
476
526
  end
477
-
478
- out << ""
479
- out << " from_response(client_for(client).execute(QUERY, variables:, operation_name: OPERATION_NAME))"
480
- out << " end"
481
- out << ""
482
- out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
483
- out << " def self.execute!(#{kwargs.join(", ")})"
484
- out << " execute(#{forward}).data!"
485
- out << " end"
486
- out << ""
487
- emit_from_response(out)
488
527
  end
489
528
 
490
529
  # The network-free half of execute: deserialize a raw GraphQL response
@@ -499,10 +538,13 @@ class GraphWeaver::Codegen
499
538
  out << " # \"errors\" => ..., \"extensions\" => ...} with wire-cased string keys."
500
539
  out << " sig { params(response: T.untyped).returns(GraphWeaver::Response[Result]) }"
501
540
  out << " def self.from_response(response)"
502
- out << " raw = GraphWeaver.check_envelope!(response.to_h, Result)"
541
+ out << " raw = GraphWeaver.check_envelope!(response, Result)"
542
+ # errors first: a cast failure is usually a field the server nulled for
543
+ # a reason it stated, and that reason belongs in the raised error
544
+ out << " errors = (raw[\"errors\"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) }"
503
545
  out << " GraphWeaver::Response[Result].new("
504
- out << " data: (Result.from_h(raw[\"data\"]) if raw[\"data\"]),"
505
- out << " errors: (raw[\"errors\"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) },"
546
+ out << " data: (GraphWeaver.cast_data(Result, raw[\"data\"], errors) if raw[\"data\"]),"
547
+ out << " errors:,"
506
548
  out << " extensions: raw[\"extensions\"] || {},"
507
549
  out << " )"
508
550
  out << " end"
@@ -514,21 +556,36 @@ class GraphWeaver::Codegen
514
556
  out << " end"
515
557
  end
516
558
 
559
+ # A kwarg's trip onto the wire: normalize whatever arrived into the type
560
+ # the sig promises — the sig itself is `.checked(:never)`, so this is the
561
+ # check — then serialize. Coercion is wrapped so a refusal names the
562
+ # variable and the operation; the value alone locates nothing.
517
563
  def variable_serialize(var)
518
- value = var.node.coerce? ? var.node.coerce(var.kwarg) : var.kwarg
564
+ value = if var.node.coerce?
565
+ "GraphWeaver::Coerce.variable(#{var.wire.inspect}, OPERATION_NAME, #{var.kwarg}) " \
566
+ "{ |v| #{var.node.coerce("v")} }"
567
+ else
568
+ var.kwarg
569
+ end
519
570
  var.node.serialize_identity? ? value : var.node.serialize(value, 1)
520
571
  end
521
572
 
522
573
  def field_cast(field)
523
574
  node = field.node
524
575
 
525
- if node.non_null?
526
- raw = "data.fetch(#{field.key.inspect})"
527
- node.identity? ? raw : node.cast(raw, 1)
528
- else
529
- raw = "data[#{field.key.inspect}]"
530
- node.identity? ? raw : "#{raw}&.then { |v1| #{node.cast("v1", 2)} }"
531
- end
576
+ cast =
577
+ if node.non_null?
578
+ raw = "data.fetch(#{field.key.inspect})"
579
+ node.identity? ? raw : node.cast(raw, 1)
580
+ else
581
+ raw = "data[#{field.key.inspect}]"
582
+ node.identity? ? raw : "#{raw}&.then { |v1| #{node.cast("v1", 2)} }"
583
+ end
584
+ return cast if node.identity? || !node.leaf?
585
+
586
+ # a leaf's cast raises about the value alone ("invalid date"); nothing
587
+ # else in the trace says which of the struct's four dates it was
588
+ "GraphWeaver::Hints.field(self, #{field.key.inspect}) { #{cast} }"
532
589
  end
533
590
 
534
591
  # A module-level T::Struct per input type: typed consts plus a FIELDS
@@ -545,11 +602,17 @@ class GraphWeaver::Codegen
545
602
  if node.one_of
546
603
  out << "#{pad} # @oneOf: every field is nullable, so exactly-one is checked at runtime"
547
604
  out << "#{pad} ONE_OF = T.let(true, T::Boolean)"
605
+ out << "#{pad} private_constant :ONE_OF"
548
606
  out << ""
549
607
  end
550
608
  node.fields.each do |field|
609
+ # A schema default makes the field optional here, so the prop has to
610
+ # admit the nil that omitting it leaves behind — the same widening
611
+ # execute's kwargs already get.
612
+ type = field.node.prop_type
613
+ type = "T.nilable(#{type})" if !field.required && field.node.non_null? && type != "T.untyped"
551
614
  default = field.required ? "" : ", default: nil"
552
- out << "#{pad} const :#{field.prop}, #{field.node.prop_type}#{default}"
615
+ out << "#{pad} const :#{field.prop}, #{type}#{default}"
553
616
  end
554
617
  out << ""
555
618
  out << "#{pad} # (prop, wire, required, serializer, coercer) per field"
@@ -560,6 +623,8 @@ class GraphWeaver::Codegen
560
623
  out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}),"
561
624
  end
562
625
  out << "#{pad} ].freeze, T::Array[GraphWeaver::InputStruct::Field])"
626
+ # InputStruct reads it with const_get, which privacy doesn't block
627
+ out << "#{pad} private_constant :FIELDS"
563
628
  out << "#{pad}end"
564
629
  end
565
630
  end
@@ -27,9 +27,7 @@ class GraphWeaver::Codegen
27
27
  # autoloading is set up after those run. Say where it does resolve.
28
28
  if type.is_a?(String)
29
29
  raise ArgumentError, "type: is the T::Enum itself, not its name — " \
30
- "register_enum(#{@graphql_name.inspect}, #{type}). An autoloaded constant isn't " \
31
- "resolvable while config/initializers run; register from a " \
32
- "Rails.application.config.to_prepare block, which generation also runs first."
30
+ "register_enum(#{@graphql_name.inspect}, #{type}). #{GraphWeaver::Codegen::AUTOLOAD_HINT}"
33
31
  end
34
32
  unless type.is_a?(Class) && type < T::Enum
35
33
  raise ArgumentError, "type: must be a T::Enum subclass, got #{type.inspect}"
@@ -107,4 +105,7 @@ class GraphWeaver::Codegen
107
105
  self
108
106
  end
109
107
  end
108
+
109
+ # codegen's own record of a register_enum mapping
110
+ private_constant :EnumType
110
111
  end
@@ -8,14 +8,23 @@ require "forwardable"
8
8
  # cast/serialize code to emit.
9
9
  class GraphWeaver::Codegen
10
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?,
11
+ # protocol: bare_type, prop_type, cast(expr, depth), identity?, leaf?,
12
+ # serialize(expr, depth), serialize_identity?, input_type, coerce?,
13
+ # coerce(expr), hash_coerce(expr, depth), hash_coerce_identity?,
14
14
  # non_null?, nested.
15
15
  class Node
16
16
  def bare_type = raise(GraphWeaver::Error, "#{self.class} must define bare_type")
17
17
  def prop_type = "T.nilable(#{bare_type})"
18
+ # The Sorbet type an execute kwarg (or input-struct field) accepts. Same
19
+ # as the prop type for anything a caller can only pass as itself; wider
20
+ # where a wire spelling is also accepted (an enum, an input hash).
21
+ # Independent of #coerce: the sig says what typechecks, the body
22
+ # normalizes whatever actually arrives.
23
+ def input_type = bare_type
18
24
  def identity? = false
25
+ # a scalar or enum: its cast is arbitrary code that raises on its own
26
+ # terms, so the field name has to be attached from outside
27
+ def leaf? = false
19
28
  def serialize_identity? = false
20
29
  def coerce? = false
21
30
  def hash_coerce_identity? = false
@@ -48,6 +57,8 @@ class GraphWeaver::Codegen
48
57
  !@scalar.cast?
49
58
  end
50
59
 
60
+ def leaf? = true
61
+
51
62
  def serialize(expr, _depth)
52
63
  @scalar.serialize(expr)
53
64
  end
@@ -56,15 +67,14 @@ class GraphWeaver::Codegen
56
67
  !@scalar.serialize?
57
68
  end
58
69
 
59
- # coercion (coerce: per scalar, or GraphWeaver.auto_coerce for all):
60
- # accept the value or its raw input and normalize before serializing.
61
- # See ScalarType#coercion.
70
+ # An untyped value reaching the kwarg is normalized to the scalar's Ruby
71
+ # type before it is serialized the sig stays narrow. See
72
+ # ScalarType#coerce_input.
62
73
  def coerce? = @scalar.coerce?
63
74
  def coerce(expr) = @scalar.coerce_input(expr)
64
- def coerce_input_type = @scalar.coerce_type
65
75
 
66
- # inside input-struct hashes, scalars coerce exactly like variable
67
- # kwargs do — the registry (incl. GraphWeaver.auto_coerce) decides
76
+ # inside input-struct hashes, scalars coerce exactly as variable
77
+ # kwargs do
68
78
  def hash_coerce(expr, _depth)
69
79
  @scalar.coerce_input(expr) || expr
70
80
  end
@@ -79,8 +89,8 @@ class GraphWeaver::Codegen
79
89
 
80
90
  attr_reader :of
81
91
 
82
- def_delegators :@of, :bare_type, :cast, :identity?, :serialize, :serialize_identity?,
83
- :coerce?, :coerce, :coerce_input_type, :hash_coerce, :hash_coerce_identity?, :nested
92
+ def_delegators :@of, :bare_type, :cast, :identity?, :leaf?, :serialize, :serialize_identity?,
93
+ :input_type, :coerce?, :coerce, :hash_coerce, :hash_coerce_identity?, :nested
84
94
 
85
95
  def initialize(of)
86
96
  @of = of
@@ -113,6 +123,7 @@ class GraphWeaver::Codegen
113
123
  end
114
124
 
115
125
  def identity? = @of.identity?
126
+ def leaf? = @of.leaf?
116
127
 
117
128
  def serialize(expr, depth)
118
129
  var = "v#{depth}"
@@ -134,9 +145,9 @@ class GraphWeaver::Codegen
134
145
  def coerce? = !hash_coerce_identity?
135
146
  def coerce(expr) = hash_coerce(expr, 1)
136
147
 
137
- def coerce_input_type
138
- element = @of.coerce? ? @of.coerce_input_type : @of.prop_type
139
- element = "T.nilable(#{element})" if @of.coerce? && !@of.non_null? && element != "T.untyped"
148
+ def input_type
149
+ element = @of.input_type
150
+ element = "T.nilable(#{element})" if !@of.non_null? && element != "T.untyped"
140
151
  "T::Array[#{element}]"
141
152
  end
142
153
 
@@ -193,22 +204,24 @@ class GraphWeaver::Codegen
193
204
  def bare_type = class_name
194
205
 
195
206
  def cast(expr, _depth)
196
- "#{class_name}.deserialize(#{expr})"
207
+ "GraphWeaver::Hints.enum(#{class_name}, #{expr})"
197
208
  end
198
209
 
199
210
  def serialize(expr, _depth)
200
211
  "#{expr}.serialize"
201
212
  end
202
213
 
214
+ def leaf? = true
215
+
203
216
  # enums always coerce: a kwarg or hash field accepts the T::Enum or
204
217
  # its wire value (deserialize raises on anything else)
205
218
  def coerce? = true
206
219
 
207
220
  def coerce(expr)
208
- "(#{expr}.is_a?(#{class_name}) ? #{expr} : #{class_name}.deserialize(#{expr}))"
221
+ "GraphWeaver::InputStruct.enum(#{class_name}, #{expr})"
209
222
  end
210
223
 
211
- def coerce_input_type = "T.any(#{class_name}, String)"
224
+ def input_type = "T.any(#{class_name}, String)"
212
225
  def hash_coerce(expr, _depth) = coerce(expr)
213
226
  def nested = self
214
227
  end
@@ -236,7 +249,7 @@ class GraphWeaver::Codegen
236
249
  if @fallback
237
250
  "#{const_prefix}_FROM_WIRE.fetch(#{expr}) { #{fallback_const} }"
238
251
  else
239
- "#{const_prefix}_FROM_WIRE.fetch(#{expr})"
252
+ "GraphWeaver::Hints.mapped_enum(#{@type_name}, #{const_prefix}_FROM_WIRE, #{expr})"
240
253
  end
241
254
  end
242
255
 
@@ -244,16 +257,18 @@ class GraphWeaver::Codegen
244
257
  "#{const_prefix}_TO_WIRE.fetch(#{expr})"
245
258
  end
246
259
 
260
+ def leaf? = true
261
+
247
262
  # kwargs and hash fields accept the member or its wire value; unlike
248
263
  # casting, bad input raises even with a fallback (a typo'd input is
249
264
  # our bug, not server drift)
250
265
  def coerce? = true
251
266
 
252
267
  def coerce(expr)
253
- "(#{expr}.is_a?(#{@type_name}) ? #{expr} : #{const_prefix}_FROM_WIRE.fetch(#{expr}))"
268
+ "GraphWeaver::InputStruct.mapped_enum(#{@type_name}, #{const_prefix}_FROM_WIRE, #{expr})"
254
269
  end
255
270
 
256
- def coerce_input_type = "T.any(#{@type_name}, String)"
271
+ def input_type = "T.any(#{@type_name}, String)"
257
272
  def hash_coerce(expr, _depth) = coerce(expr)
258
273
  end
259
274
 
@@ -360,7 +375,7 @@ class GraphWeaver::Codegen
360
375
 
361
376
  def coerce? = true
362
377
  def coerce(expr) = "#{class_name}.coerce(#{expr})"
363
- def coerce_input_type = "T.any(#{class_name}, T::Hash[T.untyped, T.untyped])"
378
+ def input_type = "T.any(#{class_name}, T::Hash[T.untyped, T.untyped])"
364
379
 
365
380
  # building a struct field from a caller-supplied plain hash value
366
381
  def hash_coerce(expr, _depth) = "#{class_name}.coerce(#{expr})"
@@ -391,4 +406,10 @@ class GraphWeaver::Codegen
391
406
  @params = params
392
407
  end
393
408
  end
409
+
410
+ # The IR is codegen's own vocabulary — every node type is reachable only
411
+ # from inside the walk.
412
+ private_constant :Node, :Scalar, :NonNull, :List, :ObjectNode, :EnumNode,
413
+ :MappedEnum, :NarrowedNode, :UnionNode, :UnionRefNode, :InputNode,
414
+ :RepresentationNode
394
415
  end