graph_weaver 0.5.1 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +432 -0
- data/Gemfile.lock +19 -19
- data/README.md +75 -54
- data/docs/cassettes.md +6 -1
- data/docs/editors.md +3 -1
- data/docs/errors.md +73 -16
- data/docs/federation.md +201 -151
- data/docs/generated_modules.md +222 -165
- data/docs/getting_started.md +106 -82
- data/docs/logging.md +34 -4
- data/docs/real_world.md +4 -4
- data/docs/scalars.md +206 -46
- data/docs/testing.md +191 -151
- data/docs/transports.md +47 -19
- data/docs/upgrading.md +210 -11
- data/lib/generators/graph_weaver/install_generator.rb +16 -1
- data/lib/graph_weaver/client.rb +46 -13
- data/lib/graph_weaver/codegen/aliases.rb +5 -4
- data/lib/graph_weaver/codegen/emit.rb +96 -39
- data/lib/graph_weaver/codegen/enum_type.rb +3 -0
- data/lib/graph_weaver/codegen/nodes.rb +42 -21
- data/lib/graph_weaver/codegen/scalar_type.rb +167 -98
- data/lib/graph_weaver/codegen/type_helpers.rb +1 -0
- data/lib/graph_weaver/codegen.rb +279 -84
- data/lib/graph_weaver/coerce.rb +113 -0
- data/lib/graph_weaver/errors.rb +30 -7
- data/lib/graph_weaver/federation.rb +6 -5
- data/lib/graph_weaver/hints.rb +76 -2
- data/lib/graph_weaver/in_process.rb +11 -8
- data/lib/graph_weaver/inflect.rb +2 -0
- data/lib/graph_weaver/input_struct.rb +115 -12
- data/lib/graph_weaver/internal/overrides.rb +101 -0
- data/lib/graph_weaver/internal/planner.rb +868 -0
- data/lib/graph_weaver/internal/schemas.rb +50 -0
- data/lib/graph_weaver/internal/selection.rb +127 -0
- data/lib/graph_weaver/{testing → internal}/subgraphs.rb +39 -41
- data/lib/graph_weaver/internal/values.rb +184 -0
- data/lib/graph_weaver/internal.rb +206 -0
- data/lib/graph_weaver/logging.rb +108 -20
- data/lib/graph_weaver/parsing.rb +5 -4
- data/lib/graph_weaver/query_module.rb +2 -0
- data/lib/graph_weaver/railtie.rb +113 -14
- data/lib/graph_weaver/representation.rb +30 -2
- data/lib/graph_weaver/response.rb +15 -0
- data/lib/graph_weaver/retry.rb +54 -22
- data/lib/graph_weaver/rspec.rb +50 -11
- data/lib/graph_weaver/schema_diff.rb +293 -0
- data/lib/graph_weaver/schema_loader.rb +96 -29
- data/lib/graph_weaver/tasks.rb +78 -29
- data/lib/graph_weaver/testing/cassette.rb +49 -65
- data/lib/graph_weaver/testing/coverage.rb +5 -4
- data/lib/graph_weaver/testing/failure.rb +10 -6
- data/lib/graph_weaver/testing/fake_client.rb +253 -60
- data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
- data/lib/graph_weaver/testing/router.rb +94 -808
- data/lib/graph_weaver/testing.rb +35 -84
- data/lib/graph_weaver/transport/faraday.rb +1 -1
- data/lib/graph_weaver/transport/http.rb +29 -12
- data/lib/graph_weaver/transport.rb +11 -34
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +203 -119
- metadata +10 -5
- data/lib/graph_weaver/schemas.rb +0 -48
- data/lib/graph_weaver/selection.rb +0 -120
- 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 #{@
|
|
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 #{@
|
|
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 #{@
|
|
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 #{@
|
|
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
|
-
|
|
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" #
|
|
374
|
-
out << "#{pad} raise GraphWeaver::TypeError.new(struct: self,
|
|
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.
|
|
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|
|
|
472
|
+
optional.map { |var|
|
|
473
|
+
flag = omitted[var]
|
|
474
|
+
flag ? "#{var.kwarg}: (#{flag} = true; nil)" : "#{var.kwarg}: nil"
|
|
475
|
+
} + ["client: nil"]
|
|
456
476
|
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,7 +538,7 @@ 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
|
|
541
|
+
out << " raw = GraphWeaver.check_envelope!(response, Result)"
|
|
503
542
|
# errors first: a cast failure is usually a field the server nulled for
|
|
504
543
|
# a reason it stated, and that reason belongs in the raised error
|
|
505
544
|
out << " errors = (raw[\"errors\"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) }"
|
|
@@ -517,21 +556,36 @@ class GraphWeaver::Codegen
|
|
|
517
556
|
out << " end"
|
|
518
557
|
end
|
|
519
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.
|
|
520
563
|
def variable_serialize(var)
|
|
521
|
-
value = var.node.coerce?
|
|
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
|
|
522
570
|
var.node.serialize_identity? ? value : var.node.serialize(value, 1)
|
|
523
571
|
end
|
|
524
572
|
|
|
525
573
|
def field_cast(field)
|
|
526
574
|
node = field.node
|
|
527
575
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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} }"
|
|
535
589
|
end
|
|
536
590
|
|
|
537
591
|
# A module-level T::Struct per input type: typed consts plus a FIELDS
|
|
@@ -548,6 +602,7 @@ class GraphWeaver::Codegen
|
|
|
548
602
|
if node.one_of
|
|
549
603
|
out << "#{pad} # @oneOf: every field is nullable, so exactly-one is checked at runtime"
|
|
550
604
|
out << "#{pad} ONE_OF = T.let(true, T::Boolean)"
|
|
605
|
+
out << "#{pad} private_constant :ONE_OF"
|
|
551
606
|
out << ""
|
|
552
607
|
end
|
|
553
608
|
node.fields.each do |field|
|
|
@@ -568,6 +623,8 @@ class GraphWeaver::Codegen
|
|
|
568
623
|
out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}),"
|
|
569
624
|
end
|
|
570
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"
|
|
571
628
|
out << "#{pad}end"
|
|
572
629
|
end
|
|
573
630
|
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?,
|
|
13
|
-
#
|
|
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
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
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
|
|
67
|
-
# kwargs do
|
|
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, :
|
|
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
|
|
138
|
-
element = @of.
|
|
139
|
-
element = "T.nilable(#{element})" if
|
|
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}
|
|
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
|
-
"
|
|
221
|
+
"GraphWeaver::InputStruct.enum(#{class_name}, #{expr})"
|
|
209
222
|
end
|
|
210
223
|
|
|
211
|
-
def
|
|
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
|
|
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
|
-
"
|
|
268
|
+
"GraphWeaver::InputStruct.mapped_enum(#{@type_name}, #{const_prefix}_FROM_WIRE, #{expr})"
|
|
254
269
|
end
|
|
255
270
|
|
|
256
|
-
def
|
|
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
|
|
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
|