graph_weaver 0.0.1 → 0.1.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/CHANGELOG.md +66 -0
- data/Gemfile.lock +45 -17
- data/Makefile +6 -1
- data/PLAN.md +52 -22
- data/README.md +62 -17
- data/docs/errors.md +106 -0
- data/docs/generated_modules.md +119 -0
- data/docs/real_world.md +46 -0
- data/docs/scalars.md +69 -0
- data/docs/testing.md +93 -0
- data/graph_weaver.gemspec +3 -0
- data/lib/graph_weaver/codegen/emit.rb +237 -0
- data/lib/graph_weaver/codegen/nodes.rb +256 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +238 -0
- data/lib/graph_weaver/codegen.rb +156 -395
- data/lib/graph_weaver/errors.rb +310 -0
- data/lib/graph_weaver/faraday_executor.rb +61 -0
- data/lib/graph_weaver/http_executor.rb +16 -3
- data/lib/graph_weaver/inflect.rb +18 -0
- data/lib/graph_weaver/response.rb +55 -0
- data/lib/graph_weaver/rspec.rb +54 -0
- data/lib/graph_weaver/schema_loader.rb +73 -9
- data/lib/graph_weaver/selection.rb +68 -0
- data/lib/graph_weaver/testing/cassette.rb +203 -0
- data/lib/graph_weaver/testing/failure.rb +109 -0
- data/lib/graph_weaver/testing/fake_executor.rb +228 -0
- data/lib/graph_weaver/testing/rspec.rb +5 -0
- data/lib/graph_weaver/testing/values.rb +98 -0
- data/lib/graph_weaver/testing.rb +90 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +97 -3
- metadata +63 -1
data/lib/graph_weaver/codegen.rb
CHANGED
|
@@ -1,228 +1,92 @@
|
|
|
1
1
|
# typed: true
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "date"
|
|
4
5
|
require "graphql"
|
|
5
6
|
require "sorbet-runtime"
|
|
6
7
|
|
|
7
8
|
# Generates plain, statically-typecheckable Ruby from a GraphQL query +
|
|
8
9
|
# schema: nested T::Structs, from_h casting code, and a sig'd execute
|
|
9
|
-
# method.
|
|
10
|
-
#
|
|
11
|
-
# result type of each query.
|
|
10
|
+
# method. The output is source on disk, so srb tc sees the exact result
|
|
11
|
+
# type of each query.
|
|
12
12
|
#
|
|
13
13
|
# Supports queries and mutations; plain fields, inline fragments, named
|
|
14
14
|
# fragment spreads (including interface type conditions), union- and
|
|
15
15
|
# interface-typed fields (dispatch on __typename), enums (generated
|
|
16
|
-
# T::Enum), and typed variables (kwargs on execute).
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}.freeze
|
|
28
|
-
|
|
29
|
-
# deserialization for scalars whose wire format differs from their Ruby
|
|
30
|
-
# type; anything absent passes through untouched
|
|
31
|
-
SCALAR_CASTS = {
|
|
32
|
-
"Date" => ->(expr) { "Date.iso8601(#{expr})" },
|
|
33
|
-
}.freeze
|
|
34
|
-
|
|
35
|
-
# the inverse, for serializing variables onto the wire
|
|
36
|
-
SCALAR_SERIALIZERS = {
|
|
37
|
-
"Date" => ->(expr) { "#{expr}.iso8601" },
|
|
38
|
-
}.freeze
|
|
39
|
-
|
|
40
|
-
class Scalar
|
|
41
|
-
def initialize(name)
|
|
42
|
-
@name = name
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def bare_type
|
|
46
|
-
SCALARS.fetch(@name, "T.untyped")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def prop_type
|
|
50
|
-
"T.nilable(#{bare_type})"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def cast(expr, _depth)
|
|
54
|
-
SCALAR_CASTS.fetch(@name) { return expr }.call(expr)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def identity?
|
|
58
|
-
!SCALAR_CASTS.key?(@name)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def serialize(expr, _depth)
|
|
62
|
-
SCALAR_SERIALIZERS.fetch(@name) { return expr }.call(expr)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def serialize_identity?
|
|
66
|
-
!SCALAR_SERIALIZERS.key?(@name)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def non_null? = false
|
|
70
|
-
def nested = nil
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
class NonNull
|
|
74
|
-
def initialize(of)
|
|
75
|
-
@of = of
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def bare_type = @of.bare_type
|
|
79
|
-
def prop_type = bare_type
|
|
80
|
-
def cast(expr, depth) = @of.cast(expr, depth)
|
|
81
|
-
def identity? = @of.identity?
|
|
82
|
-
def serialize(expr, depth) = @of.serialize(expr, depth)
|
|
83
|
-
def serialize_identity? = @of.serialize_identity?
|
|
84
|
-
def non_null? = true
|
|
85
|
-
def nested = @of.nested
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
class List
|
|
89
|
-
def initialize(of)
|
|
90
|
-
@of = of
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def bare_type
|
|
94
|
-
"T::Array[#{@of.prop_type}]"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def prop_type
|
|
98
|
-
"T.nilable(#{bare_type})"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def cast(expr, depth)
|
|
102
|
-
var = "v#{depth}"
|
|
103
|
-
element = if @of.non_null? || @of.identity?
|
|
104
|
-
@of.identity? ? var : @of.cast(var, depth + 1)
|
|
105
|
-
else
|
|
106
|
-
"#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
"#{expr}.map { |#{var}| #{element} }"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def identity? = @of.identity?
|
|
113
|
-
|
|
114
|
-
def serialize(expr, depth)
|
|
115
|
-
var = "v#{depth}"
|
|
116
|
-
element = if @of.non_null? || @of.serialize_identity?
|
|
117
|
-
@of.serialize_identity? ? var : @of.serialize(var, depth + 1)
|
|
118
|
-
else
|
|
119
|
-
"#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
"#{expr}.map { |#{var}| #{element} }"
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def serialize_identity? = @of.serialize_identity?
|
|
126
|
-
def non_null? = false
|
|
127
|
-
def nested = @of.nested
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
class ObjectNode
|
|
131
|
-
Field = Struct.new(:prop, :key, :node)
|
|
132
|
-
|
|
133
|
-
attr_reader :class_name, :fields
|
|
134
|
-
|
|
135
|
-
def initialize(class_name)
|
|
136
|
-
@class_name = class_name
|
|
137
|
-
@fields = []
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def bare_type = class_name
|
|
141
|
-
|
|
142
|
-
def prop_type
|
|
143
|
-
"T.nilable(#{bare_type})"
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def cast(expr, _depth)
|
|
147
|
-
"#{class_name}.from_h(#{expr})"
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def identity? = false
|
|
151
|
-
def non_null? = false
|
|
152
|
-
def nested = self
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
class EnumNode
|
|
156
|
-
attr_reader :class_name, :values
|
|
157
|
-
|
|
158
|
-
def initialize(class_name, values)
|
|
159
|
-
@class_name = class_name
|
|
160
|
-
@values = values
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def bare_type = class_name
|
|
164
|
-
|
|
165
|
-
def prop_type
|
|
166
|
-
"T.nilable(#{bare_type})"
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def cast(expr, _depth)
|
|
170
|
-
"#{class_name}.deserialize(#{expr})"
|
|
171
|
-
end
|
|
16
|
+
# T::Enum), and typed variables (kwargs on execute). Subscriptions are
|
|
17
|
+
# still open.
|
|
18
|
+
#
|
|
19
|
+
# Split across: codegen/scalar_type.rb (the scalar registry),
|
|
20
|
+
# codegen/nodes.rb (the typed IR), codegen/emit.rb (source emission);
|
|
21
|
+
# this file holds the public API and the query walk.
|
|
22
|
+
require_relative "inflect"
|
|
23
|
+
require_relative "selection"
|
|
24
|
+
require_relative "codegen/scalar_type"
|
|
25
|
+
require_relative "codegen/nodes"
|
|
26
|
+
require_relative "codegen/emit"
|
|
172
27
|
|
|
173
|
-
|
|
28
|
+
class GraphWeaver::Codegen
|
|
29
|
+
include GraphWeaver::Inflect
|
|
30
|
+
include GraphWeaver::Selection
|
|
31
|
+
include Emit
|
|
32
|
+
|
|
33
|
+
attr_reader :module_name
|
|
34
|
+
|
|
35
|
+
# An executor is anything responding to `execute(query, variables:)`
|
|
36
|
+
# whose result `to_h`s into {"data" => ..., "errors" => ...}.
|
|
37
|
+
#
|
|
38
|
+
# executor: (a constant, or its name as a string) becomes the generated
|
|
39
|
+
# module's default transport; when omitted, generated code falls back
|
|
40
|
+
# to GraphWeaver.executor. module_name: defaults to the operation's
|
|
41
|
+
# name; default_module_name: is parse's container-scoped fallback (file
|
|
42
|
+
# generation stays strict — a checked-in file deserves a deliberate
|
|
43
|
+
# name).
|
|
44
|
+
def initialize(schema:, query:, module_name: nil, executor: nil, default_module_name: nil)
|
|
45
|
+
@schema = schema
|
|
46
|
+
@query = query.strip
|
|
47
|
+
@module_name = module_name
|
|
48
|
+
@default_module_name = default_module_name
|
|
49
|
+
@executor_const = self.class.executor_const(executor)
|
|
174
50
|
|
|
175
|
-
|
|
176
|
-
|
|
51
|
+
if executor && @executor_const.nil?
|
|
52
|
+
# a live object can't be spelled in generated source — parse can
|
|
53
|
+
# set one via the module's writer, but file generation cannot
|
|
54
|
+
raise ArgumentError, "executor: must be a named constant or String (got #{executor.inspect}); pass live objects to parse"
|
|
177
55
|
end
|
|
178
|
-
|
|
179
|
-
def serialize_identity? = false
|
|
180
|
-
def non_null? = false
|
|
181
|
-
def nested = self
|
|
182
56
|
end
|
|
183
57
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def bare_type = "#{class_name}::Type"
|
|
193
|
-
|
|
194
|
-
def prop_type
|
|
195
|
-
"T.nilable(#{bare_type})"
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def cast(expr, _depth)
|
|
199
|
-
"#{class_name}.from_h(#{expr})"
|
|
58
|
+
# The constant name an executor can be referenced by in generated
|
|
59
|
+
# source — nil when it can't be (live objects, anonymous modules).
|
|
60
|
+
def self.executor_const(executor)
|
|
61
|
+
case executor
|
|
62
|
+
when String then executor
|
|
63
|
+
when Module then executor.name
|
|
200
64
|
end
|
|
201
|
-
|
|
202
|
-
def identity? = false
|
|
203
|
-
def non_null? = false
|
|
204
|
-
def nested = self
|
|
205
65
|
end
|
|
206
66
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# execution, or an HttpExecutor for a remote endpoint. The generated
|
|
211
|
-
# execute also accepts an executor: override at runtime.
|
|
212
|
-
def initialize(schema:, executor_const:, query:, module_name:)
|
|
213
|
-
@schema = schema
|
|
214
|
-
@executor_const = executor_const
|
|
215
|
-
@query = query.strip
|
|
216
|
-
@module_name = module_name
|
|
67
|
+
# one-step shorthand
|
|
68
|
+
def self.generate(schema:, query:, module_name: nil, executor: nil)
|
|
69
|
+
new(schema:, query:, module_name:, executor:).generate
|
|
217
70
|
end
|
|
218
71
|
|
|
219
72
|
# Development convenience: generate + eval in one step, no build
|
|
220
73
|
# artifact or checked-in file. Same runtime semantics as the generated
|
|
221
74
|
# file, but invisible to srb tc — use the build step for static typing.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
75
|
+
# Evaluates into an anonymous container, so no global constants leak;
|
|
76
|
+
# executor: additionally accepts a live object (set via .executor=).
|
|
77
|
+
def self.parse(schema:, query:, module_name: nil, executor: nil)
|
|
78
|
+
executor_const = executor_const(executor)
|
|
79
|
+
|
|
80
|
+
codegen = new(schema:, query:, module_name:, executor: executor_const, default_module_name: "Query")
|
|
81
|
+
source = codegen.generate
|
|
82
|
+
|
|
83
|
+
container = Module.new
|
|
84
|
+
container.module_eval(source, "(graph_weaver)", 1)
|
|
85
|
+
mod = container.const_get(codegen.module_name)
|
|
86
|
+
# live objects (or anonymous modules) can't be referenced from
|
|
87
|
+
# generated source — set them via the module's writer instead
|
|
88
|
+
mod.executor = executor if executor && executor_const.nil?
|
|
89
|
+
mod
|
|
226
90
|
end
|
|
227
91
|
|
|
228
92
|
VarDef = Struct.new(:kwarg, :wire, :node, :required)
|
|
@@ -230,20 +94,26 @@ class GraphWeaver::Codegen
|
|
|
230
94
|
def generate
|
|
231
95
|
errors = @schema.validate(@query)
|
|
232
96
|
if errors.any?
|
|
233
|
-
raise
|
|
97
|
+
raise GraphWeaver::ValidationError.new(errors.map { |e| validation_detail(e) })
|
|
234
98
|
end
|
|
235
99
|
|
|
236
|
-
doc = GraphQL.parse(@query)
|
|
237
|
-
@fragments = doc.definitions
|
|
238
|
-
.grep(GraphQL::Language::Nodes::FragmentDefinition)
|
|
239
|
-
.to_h { |fragment| [fragment.name, fragment] }
|
|
240
100
|
@variable_enums = {}
|
|
101
|
+
@variable_inputs = {}
|
|
102
|
+
@inputs_in_progress = []
|
|
103
|
+
# requires contributed by the custom scalars this query actually uses
|
|
104
|
+
@scalar_requires = []
|
|
105
|
+
|
|
106
|
+
operation = load_operation(@query)
|
|
107
|
+
root_type = operation_root_type(operation)
|
|
241
108
|
|
|
242
|
-
operation
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
109
|
+
@module_name ||= operation.name || @default_module_name
|
|
110
|
+
unless @module_name
|
|
111
|
+
raise ArgumentError, "module_name: required for anonymous operations"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# generated source is eval'd by parse — never let a name inject code
|
|
115
|
+
unless @module_name.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
|
|
116
|
+
raise ArgumentError, "module_name: must be a constant name, got #{@module_name.inspect}"
|
|
247
117
|
end
|
|
248
118
|
|
|
249
119
|
variables = operation.variables.map do |var|
|
|
@@ -262,17 +132,31 @@ class GraphWeaver::Codegen
|
|
|
262
132
|
out << ""
|
|
263
133
|
out << "# Generated by GraphWeaver — do not edit."
|
|
264
134
|
out << ""
|
|
135
|
+
requires = @scalar_requires.uniq.sort
|
|
136
|
+
if requires.any?
|
|
137
|
+
requires.each { |req| out << "require #{req.inspect}" }
|
|
138
|
+
out << ""
|
|
139
|
+
end
|
|
265
140
|
out << "module #{@module_name}"
|
|
266
141
|
out << " extend T::Sig"
|
|
267
142
|
out << ""
|
|
268
|
-
|
|
143
|
+
# a GraphQL block string could contain a bare GRAPHQL line, which
|
|
144
|
+
# would terminate the heredoc early — pick a delimiter the query
|
|
145
|
+
# can't collide with
|
|
146
|
+
delimiter = "GRAPHQL"
|
|
147
|
+
delimiter += "_" while @query.match?(/^\s*#{delimiter}\s*$/)
|
|
148
|
+
out << " QUERY = T.let(<<~'#{delimiter}', String)"
|
|
269
149
|
@query.each_line { |line| out << " #{line}".rstrip }
|
|
270
|
-
out << "
|
|
150
|
+
out << " #{delimiter}"
|
|
271
151
|
out << ""
|
|
272
152
|
@variable_enums.each_value do |enum|
|
|
273
153
|
emit_enum(enum, out, 1)
|
|
274
154
|
out << ""
|
|
275
155
|
end
|
|
156
|
+
@variable_inputs.each_value do |input|
|
|
157
|
+
emit_input(input, out, 1)
|
|
158
|
+
out << ""
|
|
159
|
+
end
|
|
276
160
|
emit_nested(root, out, 1)
|
|
277
161
|
out << ""
|
|
278
162
|
emit_execute(out, variables)
|
|
@@ -283,53 +167,14 @@ class GraphWeaver::Codegen
|
|
|
283
167
|
|
|
284
168
|
private
|
|
285
169
|
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
def
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def camelize(name)
|
|
293
|
-
name.split("_").map { |part| "#{part[0].upcase}#{part[1..]}" }.join
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Flatten a selection set as seen by `type`: plain fields collect
|
|
297
|
-
# directly; inline fragments and named spreads recurse when their type
|
|
298
|
-
# condition matches (exact name match — interface conditions are out of
|
|
299
|
-
# scope for this spike).
|
|
300
|
-
def gather(type, selections, out = {})
|
|
301
|
-
selections.each do |selection|
|
|
302
|
-
case selection
|
|
303
|
-
when GraphQL::Language::Nodes::Field
|
|
304
|
-
(out[selection.alias || selection.name] ||= []) << selection
|
|
305
|
-
when GraphQL::Language::Nodes::InlineFragment
|
|
306
|
-
gather(type, selection.selections, out) if applies?(selection.type&.name, type)
|
|
307
|
-
when GraphQL::Language::Nodes::FragmentSpread
|
|
308
|
-
fragment = @fragments.fetch(selection.name) do
|
|
309
|
-
raise ArgumentError, "unknown fragment: #{selection.name}"
|
|
310
|
-
end
|
|
311
|
-
gather(type, fragment.selections, out) if applies?(fragment.type.name, type)
|
|
312
|
-
else
|
|
313
|
-
raise NotImplementedError, "unsupported selection: #{selection.class}"
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
|
|
170
|
+
# Selection#each_field, collected by result key (codegen groups
|
|
171
|
+
# repeated selections of one field so it can merge them)
|
|
172
|
+
def gather(type, selections)
|
|
173
|
+
out = {}
|
|
174
|
+
each_field(type, selections) { |key, node| (out[key] ||= []) << node }
|
|
317
175
|
out
|
|
318
176
|
end
|
|
319
177
|
|
|
320
|
-
# A fragment's type condition applies when it names this type exactly,
|
|
321
|
-
# or an interface/union this type belongs to (`... on Named { ... }`).
|
|
322
|
-
def applies?(condition, type)
|
|
323
|
-
return true if condition.nil? || condition == type.graphql_name
|
|
324
|
-
|
|
325
|
-
condition_type = @schema.get_type(condition)
|
|
326
|
-
return false unless condition_type
|
|
327
|
-
|
|
328
|
-
kind = condition_type.kind.name
|
|
329
|
-
(kind == "INTERFACE" || kind == "UNION") &&
|
|
330
|
-
@schema.possible_types(condition_type).include?(type)
|
|
331
|
-
end
|
|
332
|
-
|
|
333
178
|
def object_node(type, selections, class_name)
|
|
334
179
|
node = ObjectNode.new(class_name)
|
|
335
180
|
taken = [class_name]
|
|
@@ -357,12 +202,18 @@ class GraphWeaver::Codegen
|
|
|
357
202
|
# (SDL round-trips reorder values alphabetically)
|
|
358
203
|
type_ref(field_type) { EnumNode.new(name, core.values.keys.sort) }
|
|
359
204
|
when "SCALAR"
|
|
360
|
-
type_ref(field_type) {
|
|
205
|
+
type_ref(field_type) { scalar_node(core.graphql_name) }
|
|
361
206
|
else
|
|
362
207
|
raise NotImplementedError, "unsupported kind: #{core.kind.name}"
|
|
363
208
|
end
|
|
364
209
|
end
|
|
365
210
|
|
|
211
|
+
# a field under @skip/@include may be absent from the response no
|
|
212
|
+
# matter what the schema says — its type must admit nil
|
|
213
|
+
if field_nodes.any? { |n| n.directives.any? { |d| %w[skip include].include?(d.name) } }
|
|
214
|
+
child = child.of if child.is_a?(NonNull)
|
|
215
|
+
end
|
|
216
|
+
|
|
366
217
|
node.fields << ObjectNode::Field.new(prop, key, child)
|
|
367
218
|
end
|
|
368
219
|
|
|
@@ -395,21 +246,59 @@ class GraphWeaver::Codegen
|
|
|
395
246
|
when GraphQL::Language::Nodes::ListType
|
|
396
247
|
List.new(ast_type_ref(ast_type.of_type))
|
|
397
248
|
when GraphQL::Language::Nodes::TypeName
|
|
398
|
-
|
|
399
|
-
case core.kind.name
|
|
400
|
-
when "SCALAR"
|
|
401
|
-
Scalar.new(core.graphql_name)
|
|
402
|
-
when "ENUM"
|
|
403
|
-
@variable_enums[core.graphql_name] ||=
|
|
404
|
-
EnumNode.new(core.graphql_name, core.values.keys.sort)
|
|
405
|
-
else
|
|
406
|
-
raise NotImplementedError, "unsupported variable kind: #{core.kind.name}"
|
|
407
|
-
end
|
|
249
|
+
variable_core(@schema.get_type(ast_type.name))
|
|
408
250
|
else
|
|
409
251
|
raise NotImplementedError, "unsupported type node: #{ast_type.class}"
|
|
410
252
|
end
|
|
411
253
|
end
|
|
412
254
|
|
|
255
|
+
# the input-side core kinds a variable (or input-object field) can have
|
|
256
|
+
def variable_core(core)
|
|
257
|
+
case core.kind.name
|
|
258
|
+
when "SCALAR"
|
|
259
|
+
scalar_node(core.graphql_name)
|
|
260
|
+
when "ENUM"
|
|
261
|
+
@variable_enums[core.graphql_name] ||=
|
|
262
|
+
EnumNode.new(core.graphql_name, core.values.keys.sort)
|
|
263
|
+
when "INPUT_OBJECT"
|
|
264
|
+
input_node(core)
|
|
265
|
+
else
|
|
266
|
+
raise NotImplementedError, "unsupported variable kind: #{core.kind.name}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# A module-level T::Struct per input type, with a serialize method
|
|
271
|
+
# producing the wire hash. Registered once per type; nested inputs
|
|
272
|
+
# register their dependencies first (insertion order = emission order).
|
|
273
|
+
def input_node(core)
|
|
274
|
+
return @variable_inputs[core.graphql_name] if @variable_inputs.key?(core.graphql_name)
|
|
275
|
+
|
|
276
|
+
if @inputs_in_progress.include?(core.graphql_name)
|
|
277
|
+
raise NotImplementedError, "recursive input type: #{core.graphql_name}"
|
|
278
|
+
end
|
|
279
|
+
@inputs_in_progress << core.graphql_name
|
|
280
|
+
|
|
281
|
+
node = InputNode.new(core.graphql_name)
|
|
282
|
+
# sorted so output is deterministic across schema sources
|
|
283
|
+
core.arguments.values.sort_by(&:graphql_name).each do |argument|
|
|
284
|
+
child = type_ref(argument.type) { variable_core(unwrap(argument.type)) }
|
|
285
|
+
required = child.non_null? && !argument.default_value?
|
|
286
|
+
node.fields << InputNode::Field.new(
|
|
287
|
+
underscore(argument.graphql_name), argument.graphql_name, child, required
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
@inputs_in_progress.delete(core.graphql_name)
|
|
292
|
+
@variable_inputs[core.graphql_name] = node
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# A Scalar node, recording any requires its registered type needs so the
|
|
296
|
+
# generated file can require them (collected across the whole query).
|
|
297
|
+
def scalar_node(name)
|
|
298
|
+
@scalar_requires.concat(GraphWeaver::Codegen.scalar(name).requires)
|
|
299
|
+
Scalar.new(name)
|
|
300
|
+
end
|
|
301
|
+
|
|
413
302
|
# rebuild the NON_NULL/LIST wrappers around the core node
|
|
414
303
|
def type_ref(type, &core)
|
|
415
304
|
case type.kind.name
|
|
@@ -436,132 +325,4 @@ class GraphWeaver::Codegen
|
|
|
436
325
|
candidate
|
|
437
326
|
end
|
|
438
327
|
|
|
439
|
-
def emit_nested(node, out, indent)
|
|
440
|
-
case node
|
|
441
|
-
when UnionNode then emit_union(node, out, indent)
|
|
442
|
-
when EnumNode then emit_enum(node, out, indent)
|
|
443
|
-
else emit_object(node, out, indent)
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
def emit_enum(node, out, indent)
|
|
448
|
-
pad = " " * indent
|
|
449
|
-
|
|
450
|
-
out << "#{pad}class #{node.class_name} < T::Enum"
|
|
451
|
-
out << "#{pad} enums do"
|
|
452
|
-
node.values.each do |value|
|
|
453
|
-
out << "#{pad} #{camelize(value.downcase)} = new(#{value.inspect})"
|
|
454
|
-
end
|
|
455
|
-
out << "#{pad} end"
|
|
456
|
-
out << "#{pad}end"
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def emit_object(node, out, indent)
|
|
460
|
-
pad = " " * indent
|
|
461
|
-
|
|
462
|
-
out << "#{pad}class #{node.class_name} < T::Struct"
|
|
463
|
-
out << "#{pad} extend T::Sig"
|
|
464
|
-
out << ""
|
|
465
|
-
|
|
466
|
-
node.fields.filter_map { |field| field.node.nested }.each do |child|
|
|
467
|
-
emit_nested(child, out, indent + 1)
|
|
468
|
-
out << ""
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
node.fields.each do |field|
|
|
472
|
-
out << "#{pad} const :#{field.prop}, #{field.node.prop_type}"
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
out << ""
|
|
476
|
-
out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(#{node.class_name}) }"
|
|
477
|
-
out << "#{pad} def self.from_h(data)"
|
|
478
|
-
out << "#{pad} new("
|
|
479
|
-
node.fields.each do |field|
|
|
480
|
-
out << "#{pad} #{field.prop}: #{field_cast(field)},"
|
|
481
|
-
end
|
|
482
|
-
out << "#{pad} )"
|
|
483
|
-
out << "#{pad} end"
|
|
484
|
-
out << "#{pad}end"
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
def emit_union(node, out, indent)
|
|
488
|
-
pad = " " * indent
|
|
489
|
-
|
|
490
|
-
out << "#{pad}module #{node.class_name}"
|
|
491
|
-
out << "#{pad} extend T::Sig"
|
|
492
|
-
out << ""
|
|
493
|
-
|
|
494
|
-
node.members.each_value do |member|
|
|
495
|
-
emit_object(member, out, indent + 1)
|
|
496
|
-
out << ""
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
member_names = node.members.values.map(&:class_name)
|
|
500
|
-
type_alias = member_names.size == 1 ? member_names.first : "T.any(#{member_names.join(", ")})"
|
|
501
|
-
out << "#{pad} Type = T.type_alias { #{type_alias} }"
|
|
502
|
-
out << ""
|
|
503
|
-
out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(Type) }"
|
|
504
|
-
out << "#{pad} def self.from_h(data)"
|
|
505
|
-
out << "#{pad} case (typename = data.fetch(\"__typename\"))"
|
|
506
|
-
node.members.each do |graphql_name, member|
|
|
507
|
-
out << "#{pad} when #{graphql_name.inspect} then #{member.class_name}.from_h(data)"
|
|
508
|
-
end
|
|
509
|
-
out << "#{pad} else raise \"unexpected __typename: \#{typename}\""
|
|
510
|
-
out << "#{pad} end"
|
|
511
|
-
out << "#{pad} end"
|
|
512
|
-
out << "#{pad}end"
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
def emit_execute(out, variables)
|
|
516
|
-
sig_params = variables.map do |var|
|
|
517
|
-
kwarg_type = var.required ? var.node.prop_type : "T.nilable(#{var.node.bare_type})"
|
|
518
|
-
"#{var.kwarg}: #{kwarg_type}"
|
|
519
|
-
end
|
|
520
|
-
sig_params << "executor: T.untyped"
|
|
521
|
-
|
|
522
|
-
kwargs = variables.map { |var| var.required ? "#{var.kwarg}:" : "#{var.kwarg}: nil" }
|
|
523
|
-
kwargs << "executor: #{@executor_const}"
|
|
524
|
-
|
|
525
|
-
out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
|
|
526
|
-
out << " def self.execute(#{kwargs.join(", ")})"
|
|
527
|
-
|
|
528
|
-
required, optional = variables.partition(&:required)
|
|
529
|
-
if required.empty?
|
|
530
|
-
out << " variables = {}"
|
|
531
|
-
else
|
|
532
|
-
out << " variables = {"
|
|
533
|
-
required.each do |var|
|
|
534
|
-
out << " #{var.wire.inspect} => #{variable_serialize(var)},"
|
|
535
|
-
end
|
|
536
|
-
out << " }"
|
|
537
|
-
end
|
|
538
|
-
optional.each do |var|
|
|
539
|
-
out << " variables[#{var.wire.inspect}] = #{variable_serialize(var)} unless #{var.kwarg}.nil?"
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
out << ""
|
|
543
|
-
out << " result = executor.execute(QUERY, variables: variables).to_h"
|
|
544
|
-
out << " if (errors = result[\"errors\"])"
|
|
545
|
-
out << " raise \"query failed: \#{errors.inspect}\""
|
|
546
|
-
out << " end"
|
|
547
|
-
out << ""
|
|
548
|
-
out << " Result.from_h(result.fetch(\"data\"))"
|
|
549
|
-
out << " end"
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
def variable_serialize(var)
|
|
553
|
-
var.node.serialize_identity? ? var.kwarg : var.node.serialize(var.kwarg, 1)
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
def field_cast(field)
|
|
557
|
-
node = field.node
|
|
558
|
-
|
|
559
|
-
if node.non_null?
|
|
560
|
-
raw = "data.fetch(#{field.key.inspect})"
|
|
561
|
-
node.identity? ? raw : node.cast(raw, 1)
|
|
562
|
-
else
|
|
563
|
-
raw = "data[#{field.key.inspect}]"
|
|
564
|
-
node.identity? ? raw : "#{raw}&.then { |v1| #{node.cast("v1", 2)} }"
|
|
565
|
-
end
|
|
566
|
-
end
|
|
567
328
|
end
|