graph_weaver 0.2.0 → 0.2.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 +43 -0
- data/Gemfile.lock +2 -2
- data/PLAN.md +7 -0
- data/docs/generated_modules.md +43 -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 +246 -56
- 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
|
@@ -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
|
data/lib/graph_weaver/codegen.rb
CHANGED
|
@@ -19,6 +19,7 @@ require "sorbet-runtime"
|
|
|
19
19
|
# codegen/nodes.rb (the typed IR), codegen/emit.rb (source emission);
|
|
20
20
|
# this file holds the public API and the query walk.
|
|
21
21
|
require_relative "hints"
|
|
22
|
+
require_relative "input_struct"
|
|
22
23
|
require_relative "inflect"
|
|
23
24
|
require_relative "selection"
|
|
24
25
|
require_relative "codegen/enum_type"
|
|
@@ -45,9 +46,11 @@ class GraphWeaver::Codegen
|
|
|
45
46
|
# generation stays strict — a checked-in file deserves a deliberate
|
|
46
47
|
# name). scalars:/enums:/types: are client-scoped overlays consulted
|
|
47
48
|
# before the global registries (ScalarType, EnumType, and arrays of
|
|
48
|
-
# mixin modules, each keyed by GraphQL name).
|
|
49
|
+
# mixin modules, each keyed by GraphQL name). inputs_namespace: is the
|
|
50
|
+
# shared-inputs workflow (see GraphWeaver.generate!): variable types
|
|
51
|
+
# live once in that module and the query module aliases what it uses.
|
|
49
52
|
def initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil,
|
|
50
|
-
scalars: nil, enums: nil, types: nil)
|
|
53
|
+
scalars: nil, enums: nil, types: nil, inputs_namespace: nil)
|
|
51
54
|
@schema = schema
|
|
52
55
|
@query = query.strip
|
|
53
56
|
@module_name = module_name
|
|
@@ -55,6 +58,7 @@ class GraphWeaver::Codegen
|
|
|
55
58
|
@scalars = scalars || {}
|
|
56
59
|
@enums = enums || {}
|
|
57
60
|
@types = types || {}
|
|
61
|
+
@inputs_namespace = inputs_namespace
|
|
58
62
|
@client_const = self.class.client_const(client)
|
|
59
63
|
|
|
60
64
|
if client && @client_const.nil?
|
|
@@ -100,6 +104,40 @@ class GraphWeaver::Codegen
|
|
|
100
104
|
mod
|
|
101
105
|
end
|
|
102
106
|
|
|
107
|
+
# The schema-level variable types this query touched, by GraphQL
|
|
108
|
+
# name — the generate! workflow unions these across queries to decide
|
|
109
|
+
# what the shared inputs module must contain.
|
|
110
|
+
def variable_type_names
|
|
111
|
+
{ inputs: @variable_inputs.keys, enums: @variable_enums.keys, mapped: @mapped_enums.keys }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The shared inputs artifact: the named input/enum types — plus
|
|
115
|
+
# everything they transitively reference — emitted once per schema as
|
|
116
|
+
# a manifest (inputs.rb) plus one file per type under inputs/, so a
|
|
117
|
+
# schema migration diffs only the types it touched. Returns
|
|
118
|
+
# { relative_filename => source }.
|
|
119
|
+
def self.generate_inputs(schema:, module_name:, input_types: [], enum_types: [],
|
|
120
|
+
scalars: nil, enums: nil, types: nil)
|
|
121
|
+
codegen = new(schema:, query: "", module_name:, scalars:, enums:, types:)
|
|
122
|
+
codegen.generate_inputs(input_types, enum_types)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def generate_inputs(input_types, enum_types)
|
|
126
|
+
unless @module_name&.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
|
|
127
|
+
raise ArgumentError, "inputs module name must be a constant name, got #{@module_name.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
@variable_enums = {}
|
|
131
|
+
@variable_inputs = {}
|
|
132
|
+
@mapped_enums = {}
|
|
133
|
+
@requires = []
|
|
134
|
+
|
|
135
|
+
enum_types.sort.each { |name| variable_core(@schema.get_type(name)) }
|
|
136
|
+
input_types.sort.each { |name| input_node(@schema.get_type(name)) }
|
|
137
|
+
|
|
138
|
+
emit_inputs_files
|
|
139
|
+
end
|
|
140
|
+
|
|
103
141
|
VarDef = Struct.new(:kwarg, :wire, :node, :required)
|
|
104
142
|
|
|
105
143
|
# Names that cannot appear bare in generated Ruby: keywords aren't
|
|
@@ -164,61 +202,7 @@ class GraphWeaver::Codegen
|
|
|
164
202
|
|
|
165
203
|
root = object_node(root_type, operation.selections, "Result")
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
out << "# typed: strict"
|
|
169
|
-
out << "# frozen_string_literal: true"
|
|
170
|
-
out << ""
|
|
171
|
-
out << "# Generated by GraphWeaver — do not edit."
|
|
172
|
-
out << ""
|
|
173
|
-
requires = @requires.uniq.sort
|
|
174
|
-
if requires.any?
|
|
175
|
-
requires.each { |req| out << "require #{req.inspect}" }
|
|
176
|
-
out << ""
|
|
177
|
-
end
|
|
178
|
-
out << "module #{@module_name}"
|
|
179
|
-
out << " extend T::Sig"
|
|
180
|
-
out << ""
|
|
181
|
-
# a GraphQL block string could contain a bare GRAPHQL line, which
|
|
182
|
-
# would terminate the heredoc early — pick a delimiter the query
|
|
183
|
-
# can't collide with
|
|
184
|
-
delimiter = "GRAPHQL"
|
|
185
|
-
delimiter += "_" while @query.match?(/^\s*#{delimiter}\s*$/)
|
|
186
|
-
out << " QUERY = T.let(<<~'#{delimiter}', String)"
|
|
187
|
-
@query.each_line { |line| out << " #{line}".rstrip }
|
|
188
|
-
out << " #{delimiter}"
|
|
189
|
-
out << ""
|
|
190
|
-
@mapped_enums.each_value do |mapped|
|
|
191
|
-
emit_mapped_enum(mapped, out, 1)
|
|
192
|
-
out << ""
|
|
193
|
-
end
|
|
194
|
-
@variable_enums.each_value do |enum|
|
|
195
|
-
emit_enum(enum, out, 1)
|
|
196
|
-
out << ""
|
|
197
|
-
end
|
|
198
|
-
inputs, cyclic = ordered_inputs
|
|
199
|
-
if cyclic
|
|
200
|
-
# Recursive input types (Hasura bool_exp et al) reference each other,
|
|
201
|
-
# so no definition order satisfies the runtime — forward-declare every
|
|
202
|
-
# class empty, then let the full definitions below reopen with props.
|
|
203
|
-
# eval'd so srb sees only the full bodies (reopening a T::Struct to
|
|
204
|
-
# add props is a static error; adding them at runtime is fine).
|
|
205
|
-
out << " # runtime-only forward declarations: these input types reference"
|
|
206
|
-
out << " # each other, so the full definitions below need the constants"
|
|
207
|
-
out << " eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)"
|
|
208
|
-
inputs.each { |input| out << " class #{input.class_name} < T::Struct; end" }
|
|
209
|
-
out << " RUBY"
|
|
210
|
-
out << ""
|
|
211
|
-
end
|
|
212
|
-
inputs.each do |input|
|
|
213
|
-
emit_input(input, out, 1)
|
|
214
|
-
out << ""
|
|
215
|
-
end
|
|
216
|
-
emit_nested(root, out, 1)
|
|
217
|
-
out << ""
|
|
218
|
-
emit_execute(out, variables, flatten: flatten_input(variables))
|
|
219
|
-
out << "end"
|
|
220
|
-
|
|
221
|
-
out.join("\n") + "\n"
|
|
205
|
+
emit_module(root, variables)
|
|
222
206
|
end
|
|
223
207
|
|
|
224
208
|
private
|
|
@@ -251,20 +235,6 @@ class GraphWeaver::Codegen
|
|
|
251
235
|
end
|
|
252
236
|
end
|
|
253
237
|
|
|
254
|
-
# The Relay convention — an operation whose only variable is a required
|
|
255
|
-
# input object — reads better flattened: the input's fields become
|
|
256
|
-
# execute's kwargs directly, and the wrapping level is rebuilt on the
|
|
257
|
-
# wire. Multi-variable (or nullable-input) operations keep the
|
|
258
|
-
# variable-per-kwarg surface.
|
|
259
|
-
def flatten_input(variables)
|
|
260
|
-
return unless variables.size == 1
|
|
261
|
-
|
|
262
|
-
var = variables.first
|
|
263
|
-
return unless var.required && var.node.is_a?(NonNull)
|
|
264
|
-
|
|
265
|
-
input = var.node.of
|
|
266
|
-
input if input.is_a?(InputNode)
|
|
267
|
-
end
|
|
268
238
|
|
|
269
239
|
# Selection#each_field, collected by result key (codegen groups
|
|
270
240
|
# repeated selections of one field so it can merge them)
|
|
@@ -467,40 +437,7 @@ class GraphWeaver::Codegen
|
|
|
467
437
|
|
|
468
438
|
# The InputNodes a struct's fields reference, through NON_NULL/LIST
|
|
469
439
|
# wrappers — the edges of the input dependency graph.
|
|
470
|
-
def input_references(node)
|
|
471
|
-
node.fields.filter_map do |field|
|
|
472
|
-
child = field.node
|
|
473
|
-
child = child.of while child.respond_to?(:of)
|
|
474
|
-
child if child.is_a?(InputNode)
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
# Input structs in dependency order (referenced types before their
|
|
479
|
-
# referrers) so each emitted const names an already-defined class.
|
|
480
|
-
# Cycles make that impossible — flagged so emission can forward-declare
|
|
481
|
-
# every input class first, then reopen each to add its props.
|
|
482
|
-
def ordered_inputs
|
|
483
|
-
ordered = []
|
|
484
|
-
seen = {} # node => :done | :visiting (bool_exp graphs get big)
|
|
485
|
-
cyclic = T.let(false, T::Boolean)
|
|
486
|
-
|
|
487
|
-
visit = lambda do |node|
|
|
488
|
-
next if seen[node] == :done
|
|
489
|
-
|
|
490
|
-
if seen[node] == :visiting
|
|
491
|
-
cyclic = true
|
|
492
|
-
next
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
seen[node] = :visiting
|
|
496
|
-
input_references(node).each(&visit)
|
|
497
|
-
seen[node] = :done
|
|
498
|
-
ordered << node
|
|
499
|
-
end
|
|
500
|
-
@variable_inputs.each_value(&visit)
|
|
501
440
|
|
|
502
|
-
[ordered, cyclic]
|
|
503
|
-
end
|
|
504
441
|
|
|
505
442
|
# Registered helper-module names for a GraphQL type (additive: global
|
|
506
443
|
# registrations plus this client's), collecting their requires.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require_relative "hints"
|
|
7
|
+
|
|
8
|
+
module GraphWeaver
|
|
9
|
+
# Runtime for generated input structs. Each struct declares its typed
|
|
10
|
+
# consts plus a compact FIELDS table — (prop, wire name, requiredness,
|
|
11
|
+
# serializer, coercer) per field, with the conversions emitted as
|
|
12
|
+
# lambdas — and this module is the loop that drives it. One copy here
|
|
13
|
+
# instead of unrolled methods in every struct, which is the difference
|
|
14
|
+
# between ~2 lines and ~6 lines per field when a Hasura bool_exp pulls
|
|
15
|
+
# hundreds of input types into one module.
|
|
16
|
+
module InputStruct
|
|
17
|
+
include Kernel # for sorbet: hosts are T::Structs
|
|
18
|
+
|
|
19
|
+
# serializer/coercer are code-as-data from the generated file; nil
|
|
20
|
+
# means identity (the wire value passes through untouched)
|
|
21
|
+
Field = Struct.new(:prop, :wire, :required, :serializer, :coercer)
|
|
22
|
+
|
|
23
|
+
def self.included(base)
|
|
24
|
+
base.extend(ClassMethods)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# the wire hash — optional fields left nil stay off the wire
|
|
28
|
+
def serialize
|
|
29
|
+
self.class.const_get(:FIELDS).each_with_object({}) do |field, wire|
|
|
30
|
+
value = public_send(field.prop)
|
|
31
|
+
next if value.nil? && !field.required
|
|
32
|
+
|
|
33
|
+
wire[field.wire] = field.serializer && !value.nil? ? field.serializer.call(value) : value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
alias_method :to_h, :serialize
|
|
37
|
+
|
|
38
|
+
module ClassMethods
|
|
39
|
+
include Kernel
|
|
40
|
+
|
|
41
|
+
# Build from a plain hash (underscored keys, Symbol or String):
|
|
42
|
+
# enums accept their wire values, nested inputs accept hashes; the
|
|
43
|
+
# struct's types are enforced on construction, and unknown keys
|
|
44
|
+
# raise with a spellchecked hint.
|
|
45
|
+
def coerce(value)
|
|
46
|
+
return value if value.is_a?(self)
|
|
47
|
+
|
|
48
|
+
# a typo'd key must not silently drop off the wire
|
|
49
|
+
GraphWeaver::Hints.validate_keys!(self, value)
|
|
50
|
+
|
|
51
|
+
fields = T.unsafe(self).const_get(:FIELDS)
|
|
52
|
+
T.unsafe(self).new(**fields.to_h do |field|
|
|
53
|
+
raw = value.key?(field.prop) ? value[field.prop] : value[field.prop.to_s]
|
|
54
|
+
[field.prop, raw.nil? || field.coercer.nil? ? raw : field.coercer.call(raw)]
|
|
55
|
+
end)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/graph_weaver/railtie.rb
CHANGED
|
@@ -24,6 +24,7 @@ class GraphWeaver::Railtie < Rails::Railtie
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
initializer "graph_weaver.load_generated", after: :load_config_initializers do
|
|
27
|
-
|
|
27
|
+
# entries may be globs, so Dir[] rather than Dir.exist?
|
|
28
|
+
GraphWeaver.load_generated! if GraphWeaver.generated_paths.any? { |path| Dir[path].any? }
|
|
28
29
|
end
|
|
29
30
|
end
|
data/lib/graph_weaver/rspec.rb
CHANGED
|
@@ -8,14 +8,12 @@ require_relative "testing"
|
|
|
8
8
|
#
|
|
9
9
|
# require "graph_weaver/rspec"
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# auto_fake defaults on, so every example runs against a schema-correct
|
|
14
|
-
# fake. Configure to override:
|
|
11
|
+
# Then opt in to per-example fakes (explicit on purpose — silently
|
|
12
|
+
# swapping every example onto a fake is too surprising to be a default):
|
|
15
13
|
#
|
|
16
14
|
# GraphWeaver::Testing.configure do |config|
|
|
17
|
-
# config.
|
|
18
|
-
# config.
|
|
15
|
+
# config.auto_fake = true # every example runs against a fake
|
|
16
|
+
# # config.schema = MySchema # optional: the committed dump auto-locates
|
|
19
17
|
# end
|
|
20
18
|
#
|
|
21
19
|
# What it wires up:
|
data/lib/graph_weaver/testing.rb
CHANGED
|
@@ -49,10 +49,10 @@ module GraphWeaver
|
|
|
49
49
|
@mode = nil # auto
|
|
50
50
|
@schema = nil
|
|
51
51
|
@cassette_dir = "spec/cassettes"
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
@auto_fake =
|
|
52
|
+
# explicit opt-in: swapping every example onto a fake is too
|
|
53
|
+
# surprising to be a default — a little friction beats unexpected
|
|
54
|
+
# behavior (the schema still auto-locates once you opt in)
|
|
55
|
+
@auto_fake = false
|
|
56
56
|
# GRAPHWEAVER_RECORD=1 rspec ... -> Cassette.use re-records
|
|
57
57
|
@record = !ENV["GRAPHWEAVER_RECORD"].to_s.empty?
|
|
58
58
|
# anonymize responses as they're recorded (needs config.schema)
|
data/lib/graph_weaver/version.rb
CHANGED