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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/Gemfile.lock +21 -30
- data/PLAN.md +7 -0
- data/docs/generated_modules.md +78 -4
- data/docs/getting_started.md +8 -6
- data/docs/testing.md +21 -14
- data/lib/graph_weaver/client.rb +3 -2
- data/lib/graph_weaver/codegen/emit.rb +274 -60
- data/lib/graph_weaver/codegen/nodes.rb +35 -80
- data/lib/graph_weaver/codegen.rb +41 -104
- data/lib/graph_weaver/input_struct.rb +59 -0
- data/lib/graph_weaver/railtie.rb +2 -1
- data/lib/graph_weaver/rspec.rb +4 -6
- data/lib/graph_weaver/testing.rb +4 -4
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +103 -22
- metadata +2 -1
|
@@ -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 << "
|
|
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 << "
|
|
187
|
-
out << "
|
|
188
|
-
out << "
|
|
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
|
-
#
|
|
216
|
-
#
|
|
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}
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|