graph_weaver 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/CHANGELOG.md +213 -0
- data/Gemfile.lock +32 -2
- data/Makefile +7 -2
- data/NOTES.md +1 -1
- data/PLAN.md +34 -1
- data/README.md +56 -41
- data/docs/cassettes.md +76 -0
- data/docs/errors.md +37 -9
- data/docs/generated_modules.md +139 -29
- data/docs/getting_started.md +134 -0
- data/docs/logging.md +33 -0
- data/docs/real_world.md +27 -20
- data/docs/scalars.md +122 -8
- data/docs/testing.md +44 -34
- data/docs/transports.md +124 -0
- data/graph_weaver.gemspec +7 -1
- data/lib/graph_weaver/client.rb +200 -0
- data/lib/graph_weaver/codegen/emit.rb +88 -39
- data/lib/graph_weaver/codegen/enum_type.rb +149 -0
- data/lib/graph_weaver/codegen/nodes.rb +109 -9
- data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
- data/lib/graph_weaver/codegen.rb +301 -67
- data/lib/graph_weaver/errors.rb +37 -25
- data/lib/graph_weaver/hints.rb +63 -0
- data/lib/graph_weaver/inflect.rb +2 -1
- data/lib/graph_weaver/logging.rb +41 -0
- data/lib/graph_weaver/railtie.rb +29 -0
- data/lib/graph_weaver/retry.rb +97 -0
- data/lib/graph_weaver/rspec.rb +19 -14
- data/lib/graph_weaver/schema_loader.rb +156 -20
- data/lib/graph_weaver/selection.rb +3 -3
- data/lib/graph_weaver/tasks.rb +71 -0
- data/lib/graph_weaver/testing/cassette.rb +42 -21
- data/lib/graph_weaver/testing/failure.rb +22 -22
- data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
- data/lib/graph_weaver/testing/values.rb +2 -2
- data/lib/graph_weaver/testing.rb +32 -16
- data/lib/graph_weaver/transport/faraday.rb +56 -0
- data/lib/graph_weaver/transport/http.rb +87 -0
- data/lib/graph_weaver/transport.rb +120 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +208 -38
- metadata +73 -5
- data/lib/graph_weaver/faraday_executor.rb +0 -61
- data/lib/graph_weaver/http_executor.rb +0 -44
- data/lib/graph_weaver/testing/rspec.rb +0 -5
|
@@ -29,11 +29,30 @@ class GraphWeaver::Codegen
|
|
|
29
29
|
out << "#{pad}end"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# module-level wire translation tables for an app-mapped enum
|
|
33
|
+
def emit_mapped_enum(node, out, indent)
|
|
34
|
+
pad = " " * indent
|
|
35
|
+
type = node.bare_type
|
|
36
|
+
prefix = node.const_prefix
|
|
37
|
+
|
|
38
|
+
out << "#{pad}# GraphQL enum #{node.graphql_name} <-> #{type} (registered mapping)"
|
|
39
|
+
out << "#{pad}#{prefix}_FROM_WIRE = T.let({"
|
|
40
|
+
node.mapping.each do |wire, member|
|
|
41
|
+
out << "#{pad} #{wire.inspect} => #{type}.deserialize(#{member.serialize.to_s.inspect}),"
|
|
42
|
+
end
|
|
43
|
+
out << "#{pad}}.freeze, T::Hash[String, #{type}])"
|
|
44
|
+
out << "#{pad}#{prefix}_TO_WIRE = T.let(#{prefix}_FROM_WIRE.invert.freeze, T::Hash[#{type}, String])"
|
|
45
|
+
end
|
|
46
|
+
|
|
32
47
|
def emit_object(node, out, indent)
|
|
33
48
|
pad = " " * indent
|
|
34
49
|
|
|
35
50
|
out << "#{pad}class #{node.class_name} < T::Struct"
|
|
36
51
|
out << "#{pad} extend T::Sig"
|
|
52
|
+
out << "#{pad} include GraphWeaver::Hints"
|
|
53
|
+
node.mixins.each do |mixin|
|
|
54
|
+
out << "#{pad} include #{mixin} # registered for #{node.graphql_type}"
|
|
55
|
+
end
|
|
37
56
|
out << ""
|
|
38
57
|
|
|
39
58
|
node.fields.filter_map { |field| field.node.nested }.each do |child|
|
|
@@ -89,56 +108,74 @@ class GraphWeaver::Codegen
|
|
|
89
108
|
out << "#{pad}end"
|
|
90
109
|
end
|
|
91
110
|
|
|
92
|
-
def emit_execute(out, variables)
|
|
93
|
-
out << " @
|
|
111
|
+
def emit_execute(out, variables, flatten: nil)
|
|
112
|
+
out << " @client = T.let(nil, T.untyped)"
|
|
94
113
|
out << ""
|
|
95
114
|
out << " class << self"
|
|
96
115
|
out << " extend T::Sig"
|
|
97
116
|
out << ""
|
|
98
|
-
out << " sig { params(
|
|
99
|
-
out << " attr_writer :
|
|
117
|
+
out << " sig { params(client: T.untyped).void }"
|
|
118
|
+
out << " attr_writer :client"
|
|
100
119
|
out << ""
|
|
101
|
-
out << " # default transport for
|
|
120
|
+
out << " # default client (a GraphWeaver::Client or any transport) for"
|
|
121
|
+
out << " # execute: per-module override, else the app default"
|
|
102
122
|
out << " sig { returns(T.untyped) }"
|
|
103
|
-
out << " def
|
|
104
|
-
out << " @
|
|
123
|
+
out << " def client"
|
|
124
|
+
out << " @client || #{@client_const || "GraphWeaver.client!"}"
|
|
105
125
|
out << " end"
|
|
106
126
|
out << " end"
|
|
107
127
|
out << ""
|
|
108
128
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
# the kwarg surface: the input's fields when flattened, else one
|
|
130
|
+
# kwarg per declared variable — typed identically either way. The
|
|
131
|
+
# per-call client override rides as an optional POSITIONAL arg, so
|
|
132
|
+
# variables keep the entire kwarg namespace (nothing is reserved).
|
|
133
|
+
params = flatten ? flatten.fields.partition(&:required).flatten : variables
|
|
134
|
+
|
|
135
|
+
sig_params = ["client: T.untyped"]
|
|
136
|
+
sig_params += params.map do |param|
|
|
137
|
+
bare = param.node.coerce? ? param.node.coerce_input_type : param.node.bare_type
|
|
138
|
+
kwarg_type = param.required || bare == "T.untyped" ? bare : "T.nilable(#{bare})"
|
|
139
|
+
"#{kwarg_name(param)}: #{kwarg_type}"
|
|
113
140
|
end
|
|
114
|
-
sig_params << "executor: T.untyped"
|
|
115
141
|
|
|
116
|
-
kwargs =
|
|
117
|
-
kwargs
|
|
142
|
+
kwargs = ["client = nil"]
|
|
143
|
+
kwargs += params.map { |param| param.required ? "#{kwarg_name(param)}:" : "#{kwarg_name(param)}: nil" }
|
|
118
144
|
|
|
119
145
|
# execute returns the full envelope; execute! is the strict shortcut for
|
|
120
146
|
# `execute(...).data!` — the typed result, or a raised QueryError.
|
|
121
|
-
forward = (
|
|
147
|
+
forward = (["client"] + params.map { |param| "#{kwarg_name(param)}: #{kwarg_name(param)}" }).join(", ")
|
|
122
148
|
|
|
149
|
+
if flatten
|
|
150
|
+
out << " # $#{variables.first.wire}'s fields, flattened into kwargs (single input-object variable)"
|
|
151
|
+
end
|
|
123
152
|
out << " sig { params(#{sig_params.join(", ")}).returns(GraphWeaver::Response[Result]) }"
|
|
124
153
|
out << " def self.execute(#{kwargs.join(", ")})"
|
|
125
154
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
out << " variables = {}"
|
|
129
|
-
else
|
|
155
|
+
if flatten
|
|
156
|
+
fields = flatten.fields.map { |field| "#{field.prop}:" }.join(", ")
|
|
130
157
|
out << " variables = {"
|
|
131
|
-
|
|
132
|
-
out << " #{var.wire.inspect} => #{variable_serialize(var)},"
|
|
133
|
-
end
|
|
158
|
+
out << " #{variables.first.wire.inspect} => #{flatten.class_name}.coerce({ #{fields} }).serialize,"
|
|
134
159
|
out << " }"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
else
|
|
161
|
+
required, optional = variables.partition(&:required)
|
|
162
|
+
if required.empty?
|
|
163
|
+
out << " variables = {}"
|
|
164
|
+
else
|
|
165
|
+
out << " variables = {"
|
|
166
|
+
required.each do |var|
|
|
167
|
+
out << " #{var.wire.inspect} => #{variable_serialize(var)},"
|
|
168
|
+
end
|
|
169
|
+
out << " }"
|
|
170
|
+
end
|
|
171
|
+
optional.each do |var|
|
|
172
|
+
out << " variables[#{var.wire.inspect}] = #{variable_serialize(var)} unless #{var.kwarg}.nil?"
|
|
173
|
+
end
|
|
138
174
|
end
|
|
139
175
|
|
|
140
176
|
out << ""
|
|
141
|
-
out << "
|
|
177
|
+
out << " transport = GraphWeaver.resolve_transport(client || self.client)"
|
|
178
|
+
out << " raw = transport.execute(QUERY, variables: variables).to_h"
|
|
142
179
|
out << " GraphWeaver::Response[Result].new("
|
|
143
180
|
out << " data: (Result.from_h(raw[\"data\"]) if raw[\"data\"]),"
|
|
144
181
|
out << " errors: (raw[\"errors\"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) },"
|
|
@@ -152,18 +189,17 @@ class GraphWeaver::Codegen
|
|
|
152
189
|
out << " end"
|
|
153
190
|
end
|
|
154
191
|
|
|
192
|
+
# a kwarg surface entry is a VarDef (.kwarg) or, when flattened, an
|
|
193
|
+
# InputNode::Field (.prop)
|
|
194
|
+
def kwarg_name(param)
|
|
195
|
+
param.respond_to?(:kwarg) ? param.kwarg : param.prop
|
|
196
|
+
end
|
|
197
|
+
|
|
155
198
|
def variable_serialize(var)
|
|
156
199
|
value = var.node.coerce? ? var.node.coerce(var.kwarg) : var.kwarg
|
|
157
200
|
var.node.serialize_identity? ? value : var.node.serialize(value, 1)
|
|
158
201
|
end
|
|
159
202
|
|
|
160
|
-
# Structured shape for a schema-validation error: message plus its first
|
|
161
|
-
# source location, so ValidationError#errors is inspectable.
|
|
162
|
-
def validation_detail(error)
|
|
163
|
-
loc = (error.to_h["locations"]&.first if error.respond_to?(:to_h))
|
|
164
|
-
{ message: error.message, line: loc && loc["line"], column: loc && loc["column"] }
|
|
165
|
-
end
|
|
166
|
-
|
|
167
203
|
def field_cast(field)
|
|
168
204
|
node = field.node
|
|
169
205
|
|
|
@@ -189,16 +225,26 @@ class GraphWeaver::Codegen
|
|
|
189
225
|
out << "#{pad} const :#{field.prop}, #{field.node.prop_type}#{default}"
|
|
190
226
|
end
|
|
191
227
|
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
|
|
192
231
|
out << "#{pad} sig { returns(T::Hash[String, T.untyped]) }"
|
|
193
232
|
out << "#{pad} def serialize"
|
|
194
|
-
out << "#{pad}
|
|
233
|
+
out << "#{pad} __gw_result = T.let({}, T::Hash[String, T.untyped])"
|
|
195
234
|
node.fields.each do |field|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
200
246
|
end
|
|
201
|
-
out << "#{pad}
|
|
247
|
+
out << "#{pad} __gw_result"
|
|
202
248
|
out << "#{pad} end"
|
|
203
249
|
out << ""
|
|
204
250
|
out << "#{pad} # serialize, under the conventional name"
|
|
@@ -212,6 +258,9 @@ class GraphWeaver::Codegen
|
|
|
212
258
|
out << "#{pad} def self.coerce(value)"
|
|
213
259
|
out << "#{pad} return value if value.is_a?(#{node.class_name})"
|
|
214
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 << ""
|
|
215
264
|
out << "#{pad} new("
|
|
216
265
|
node.fields.each do |field|
|
|
217
266
|
raw = "value_at(value, :#{field.prop})"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
class GraphWeaver::Codegen
|
|
5
|
+
# How one GraphQL enum maps onto an app-owned T::Enum, so generated
|
|
6
|
+
# code speaks YOUR enum instead of generating one per module:
|
|
7
|
+
#
|
|
8
|
+
# class PetKind < T::Enum
|
|
9
|
+
# enums { Cat = new("cat"); Dog = new("dog") }
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# GraphWeaver.register_enum("Species", PetKind)
|
|
13
|
+
#
|
|
14
|
+
# The wire mapping is inferred by name ("CAT" <-> PetKind::Cat,
|
|
15
|
+
# case/underscore-insensitive against each member's serialized value);
|
|
16
|
+
# map: pins renames explicitly and merges over inference. Every wire
|
|
17
|
+
# value the schema declares must resolve — generation fails naming the
|
|
18
|
+
# gaps — unless fallback: names a member to absorb unknown values
|
|
19
|
+
# (forward-compat for servers that add members; inputs stay strict).
|
|
20
|
+
class EnumType
|
|
21
|
+
attr_reader :graphql_name, :type, :fallback, :requires
|
|
22
|
+
|
|
23
|
+
def initialize(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
24
|
+
@graphql_name = graphql_name.to_s
|
|
25
|
+
unless type.is_a?(Class) && type < T::Enum
|
|
26
|
+
raise ArgumentError, "type: must be a T::Enum subclass, got #{type.inspect}"
|
|
27
|
+
end
|
|
28
|
+
unless type.name
|
|
29
|
+
raise ArgumentError, "type: must be a named constant (anonymous classes can't appear in generated source)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@type = type
|
|
33
|
+
@map = map || {}
|
|
34
|
+
@fallback = fallback
|
|
35
|
+
@requires = Array(requires)
|
|
36
|
+
|
|
37
|
+
if fallback && !type.values.include?(fallback)
|
|
38
|
+
raise ArgumentError, "fallback: must be a #{type} member, got #{fallback.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# wire value => member for every value the schema declares; raises
|
|
43
|
+
# naming the unmappable ones (unless fallback: absorbs them)
|
|
44
|
+
def mapping_for(wire_values)
|
|
45
|
+
mapping = {}
|
|
46
|
+
missing = []
|
|
47
|
+
|
|
48
|
+
wire_values.each do |wire|
|
|
49
|
+
member = @map[wire] || infer(wire)
|
|
50
|
+
member ? mapping[wire] = member : missing << wire
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if missing.any? && !fallback
|
|
54
|
+
raise GraphWeaver::Error,
|
|
55
|
+
"#{type} has no member for #{graphql_name} value(s) #{missing.join(", ")} — " \
|
|
56
|
+
"add them, pin with map:, or absorb with fallback:"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
mapping
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# "CAT" matches serialize "cat"; "NOT_FOUND" matches "not_found"
|
|
65
|
+
def infer(wire)
|
|
66
|
+
@type.values.find { |member| normalize(member.serialize.to_s) == normalize(wire) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalize(value)
|
|
70
|
+
value.downcase.delete("_")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# Map a GraphQL enum onto an app-owned T::Enum (see EnumType); the
|
|
76
|
+
# global default — client.register_enum scopes to one client.
|
|
77
|
+
def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
78
|
+
enum_registry[graphql_name.to_s] = EnumType.new(graphql_name, type, map:, fallback:, requires:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Bulk, inference-only form: register_enums("Species" => PetKind, ...)
|
|
82
|
+
def register_enums(mappings)
|
|
83
|
+
mappings.each { |graphql_name, type| register_enum(graphql_name, type) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def enum_registry
|
|
87
|
+
@enum_registry ||= {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Attach app-owned helper modules to every struct generated from a
|
|
91
|
+
# GraphQL type — the logic stays in your code, generation wires it in:
|
|
92
|
+
#
|
|
93
|
+
# GraphWeaver.register_type("Pet", PetHelpers)
|
|
94
|
+
#
|
|
95
|
+
# Or build the mixin inline — the block is module_eval'd into a fresh
|
|
96
|
+
# module auto-named GraphWeaver::TypeHelpers::<Type>. Handy for quick
|
|
97
|
+
# decoration; srb tc can't see into block-defined methods, so prefer
|
|
98
|
+
# a named module where static checking matters:
|
|
99
|
+
#
|
|
100
|
+
# GraphWeaver.register_type("Pet") do
|
|
101
|
+
# def display_name = "#{name} the pet"
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# Additive: repeated registrations (and client-scoped ones) stack.
|
|
105
|
+
def register_type(graphql_name, *mixins, requires: nil, &block)
|
|
106
|
+
entry = type_registry[graphql_name.to_s] ||= { mixins: [], requires: [] }
|
|
107
|
+
add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def type_registry
|
|
111
|
+
@type_registry ||= {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# shared with Client#register_type: build/validate the mixins and
|
|
115
|
+
# append them to a registry entry
|
|
116
|
+
def add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
117
|
+
mixins = mixins.dup
|
|
118
|
+
mixins << helper_module(graphql_name, block) if block
|
|
119
|
+
|
|
120
|
+
raise ArgumentError, "pass one or more helper modules, or a block" if mixins.empty?
|
|
121
|
+
mixins.each do |mixin|
|
|
122
|
+
unless mixin.is_a?(Module) && mixin.name
|
|
123
|
+
raise ArgumentError, "type helpers must be named modules, got #{mixin.inspect}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
entry[:mixins].concat(mixins)
|
|
128
|
+
entry[:requires].concat(Array(requires))
|
|
129
|
+
entry
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# a block-built mixin needs a name generated files can reference:
|
|
133
|
+
# GraphWeaver::TypeHelpers::Pet (suffixed on re-registration)
|
|
134
|
+
def helper_module(graphql_name, block)
|
|
135
|
+
base = GraphWeaver::Inflect.camelize(graphql_name.to_s)
|
|
136
|
+
name = base
|
|
137
|
+
count = 1
|
|
138
|
+
name = "#{base}V#{count += 1}" while GraphWeaver::TypeHelpers.const_defined?(name, false)
|
|
139
|
+
GraphWeaver::TypeHelpers.const_set(name, Module.new(&block))
|
|
140
|
+
end
|
|
141
|
+
private :helper_module
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
module GraphWeaver
|
|
146
|
+
# Home of block-built type helpers (register_type with a block), which
|
|
147
|
+
# need constant names so generated files can reference them.
|
|
148
|
+
module TypeHelpers; end
|
|
149
|
+
end
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
# cast/serialize code to emit.
|
|
7
7
|
class GraphWeaver::Codegen
|
|
8
8
|
class Scalar
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# takes a resolved ScalarType — the generator picks it from the
|
|
10
|
+
# client-scoped overlay or the global registry
|
|
11
|
+
def initialize(scalar_type)
|
|
12
|
+
@scalar = scalar_type
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def bare_type
|
|
@@ -15,7 +17,9 @@ class GraphWeaver::Codegen
|
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def prop_type
|
|
18
|
-
|
|
20
|
+
# unregistered scalars are already T.untyped — wrapping in
|
|
21
|
+
# T.nilable is redundant and an srb tc error under typed: strict
|
|
22
|
+
bare_type == "T.untyped" ? bare_type : "T.nilable(#{bare_type})"
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def cast(expr, _depth)
|
|
@@ -41,8 +45,13 @@ class GraphWeaver::Codegen
|
|
|
41
45
|
def coerce(expr) = @scalar.coerce_input(expr)
|
|
42
46
|
def coerce_input_type = @scalar.coerce_type
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
# inside input-struct hashes, scalars coerce exactly like variable
|
|
49
|
+
# kwargs do — the registry (incl. GraphWeaver.auto_coerce) decides
|
|
50
|
+
def hash_coerce(expr, _depth)
|
|
51
|
+
@scalar.coerce_input(expr) || expr
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def hash_coerce_identity? = !@scalar.coerce?
|
|
46
55
|
|
|
47
56
|
def non_null? = false
|
|
48
57
|
def nested = nil
|
|
@@ -71,6 +80,8 @@ class GraphWeaver::Codegen
|
|
|
71
80
|
end
|
|
72
81
|
|
|
73
82
|
class List
|
|
83
|
+
attr_reader :of
|
|
84
|
+
|
|
74
85
|
def initialize(of)
|
|
75
86
|
@of = of
|
|
76
87
|
end
|
|
@@ -115,7 +126,7 @@ class GraphWeaver::Codegen
|
|
|
115
126
|
inner = if @of.non_null? || @of.hash_coerce_identity?
|
|
116
127
|
@of.hash_coerce_identity? ? var : @of.hash_coerce(var, depth + 1)
|
|
117
128
|
else
|
|
118
|
-
"#{var}
|
|
129
|
+
"#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
|
|
119
130
|
end
|
|
120
131
|
|
|
121
132
|
"#{expr}.map { |#{var}| #{inner} }"
|
|
@@ -130,10 +141,14 @@ class GraphWeaver::Codegen
|
|
|
130
141
|
Field = Struct.new(:prop, :key, :node)
|
|
131
142
|
|
|
132
143
|
attr_reader :class_name, :fields
|
|
144
|
+
# the GraphQL type this struct was generated from, and any registered
|
|
145
|
+
# helper modules to include (see Codegen.register_type)
|
|
146
|
+
attr_accessor :graphql_type, :mixins
|
|
133
147
|
|
|
134
148
|
def initialize(class_name)
|
|
135
149
|
@class_name = class_name
|
|
136
150
|
@fields = []
|
|
151
|
+
@mixins = []
|
|
137
152
|
end
|
|
138
153
|
|
|
139
154
|
def bare_type = class_name
|
|
@@ -176,18 +191,103 @@ class GraphWeaver::Codegen
|
|
|
176
191
|
end
|
|
177
192
|
|
|
178
193
|
def serialize_identity? = false
|
|
179
|
-
def coerce? = false
|
|
180
194
|
|
|
181
|
-
#
|
|
182
|
-
|
|
195
|
+
# enums always coerce: a kwarg or hash field accepts the T::Enum or
|
|
196
|
+
# its wire value (deserialize raises on anything else)
|
|
197
|
+
def coerce? = true
|
|
198
|
+
|
|
199
|
+
def coerce(expr)
|
|
183
200
|
"(#{expr}.is_a?(#{class_name}) ? #{expr} : #{class_name}.deserialize(#{expr}))"
|
|
184
201
|
end
|
|
185
202
|
|
|
203
|
+
def coerce_input_type = "T.any(#{class_name}, String)"
|
|
204
|
+
def hash_coerce(expr, _depth) = coerce(expr)
|
|
186
205
|
def hash_coerce_identity? = false
|
|
187
206
|
def non_null? = false
|
|
188
207
|
def nested = self
|
|
189
208
|
end
|
|
190
209
|
|
|
210
|
+
# A GraphQL enum mapped onto an app-owned T::Enum (see EnumType): no
|
|
211
|
+
# generated enum class — instead module-level <NAME>_FROM_WIRE /
|
|
212
|
+
# <NAME>_TO_WIRE constants translate at the boundary. fallback: makes
|
|
213
|
+
# casting absorb unknown wire values (inputs stay strict).
|
|
214
|
+
class MappedEnum
|
|
215
|
+
attr_reader :graphql_name, :mapping
|
|
216
|
+
|
|
217
|
+
def initialize(enum_type, wire_values)
|
|
218
|
+
@graphql_name = enum_type.graphql_name
|
|
219
|
+
@type_name = enum_type.type.name
|
|
220
|
+
@fallback = enum_type.fallback
|
|
221
|
+
@mapping = enum_type.mapping_for(wire_values)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def const_prefix = GraphWeaver::Inflect.underscore(@graphql_name).upcase
|
|
225
|
+
def fallback_const = @fallback && "#{@type_name}.deserialize(#{@fallback.serialize.to_s.inspect})"
|
|
226
|
+
|
|
227
|
+
def bare_type = @type_name
|
|
228
|
+
|
|
229
|
+
def prop_type
|
|
230
|
+
"T.nilable(#{bare_type})"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def cast(expr, _depth)
|
|
234
|
+
if @fallback
|
|
235
|
+
"#{const_prefix}_FROM_WIRE.fetch(#{expr}) { #{fallback_const} }"
|
|
236
|
+
else
|
|
237
|
+
"#{const_prefix}_FROM_WIRE.fetch(#{expr})"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def identity? = false
|
|
242
|
+
|
|
243
|
+
def serialize(expr, _depth)
|
|
244
|
+
"#{const_prefix}_TO_WIRE.fetch(#{expr})"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def serialize_identity? = false
|
|
248
|
+
|
|
249
|
+
# kwargs and hash fields accept the member or its wire value; unlike
|
|
250
|
+
# casting, bad input raises even with a fallback (a typo'd input is
|
|
251
|
+
# our bug, not server drift)
|
|
252
|
+
def coerce? = true
|
|
253
|
+
|
|
254
|
+
def coerce(expr)
|
|
255
|
+
"(#{expr}.is_a?(#{@type_name}) ? #{expr} : #{const_prefix}_FROM_WIRE.fetch(#{expr}))"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def coerce_input_type = "T.any(#{@type_name}, String)"
|
|
259
|
+
def hash_coerce(expr, _depth) = coerce(expr)
|
|
260
|
+
def hash_coerce_identity? = false
|
|
261
|
+
def non_null? = false
|
|
262
|
+
def nested = nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# A single-condition narrowing of an abstract field (`... on Pet { ... }`
|
|
266
|
+
# and nothing else): the member struct when the runtime type matches,
|
|
267
|
+
# nil when it doesn't — a non-match's response object carries no
|
|
268
|
+
# matching fields, so the hash arrives empty. Always nilable, whatever
|
|
269
|
+
# the schema's nullability, because narrowing filters.
|
|
270
|
+
class NarrowedNode
|
|
271
|
+
def initialize(of)
|
|
272
|
+
@of = of
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def class_name = @of.class_name
|
|
276
|
+
def bare_type = @of.bare_type
|
|
277
|
+
|
|
278
|
+
def prop_type
|
|
279
|
+
"T.nilable(#{bare_type})"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def cast(expr, depth)
|
|
283
|
+
"(#{expr}.empty? ? nil : #{@of.cast(expr, depth)})"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def identity? = false
|
|
287
|
+
def non_null? = false
|
|
288
|
+
def nested = @of
|
|
289
|
+
end
|
|
290
|
+
|
|
191
291
|
class UnionNode
|
|
192
292
|
attr_reader :class_name, :members # graphql type name => ObjectNode
|
|
193
293
|
|