graph_weaver 0.1.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/.yardopts +6 -0
- data/CHANGELOG.md +256 -0
- data/Gemfile.lock +32 -2
- data/Makefile +7 -2
- data/NOTES.md +1 -1
- data/PLAN.md +41 -1
- data/README.md +56 -41
- data/docs/cassettes.md +76 -0
- data/docs/errors.md +37 -9
- data/docs/generated_modules.md +178 -29
- data/docs/getting_started.md +136 -0
- data/docs/logging.md +33 -0
- data/docs/real_world.md +27 -20
- data/docs/scalars.md +122 -8
- data/docs/testing.md +55 -38
- data/docs/transports.md +124 -0
- data/graph_weaver.gemspec +7 -1
- data/lib/graph_weaver/client.rb +201 -0
- data/lib/graph_weaver/codegen/emit.rb +315 -76
- data/lib/graph_weaver/codegen/enum_type.rb +149 -0
- data/lib/graph_weaver/codegen/nodes.rb +123 -68
- data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
- data/lib/graph_weaver/codegen.rb +272 -101
- 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/input_struct.rb +59 -0
- data/lib/graph_weaver/logging.rb +41 -0
- data/lib/graph_weaver/railtie.rb +30 -0
- data/lib/graph_weaver/retry.rb +97 -0
- data/lib/graph_weaver/rspec.rb +17 -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 +31 -15
- 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 +289 -38
- metadata +74 -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
|
@@ -1,13 +1,33 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
# Protocol defaults — subclasses override what differs. The full node
|
|
11
|
+
# protocol: bare_type, prop_type, cast(expr, depth), identity?,
|
|
12
|
+
# serialize(expr, depth), serialize_identity?, coerce?, 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
|
|
27
|
+
# takes a resolved ScalarType — the generator picks it from the
|
|
28
|
+
# client-scoped overlay or the global registry
|
|
29
|
+
def initialize(scalar_type)
|
|
30
|
+
@scalar = scalar_type
|
|
11
31
|
end
|
|
12
32
|
|
|
13
33
|
def bare_type
|
|
@@ -15,7 +35,9 @@ class GraphWeaver::Codegen
|
|
|
15
35
|
end
|
|
16
36
|
|
|
17
37
|
def prop_type
|
|
18
|
-
|
|
38
|
+
# unregistered scalars are already T.untyped — wrapping in
|
|
39
|
+
# T.nilable is redundant and an srb tc error under typed: strict
|
|
40
|
+
bare_type == "T.untyped" ? bare_type : "T.nilable(#{bare_type})"
|
|
19
41
|
end
|
|
20
42
|
|
|
21
43
|
def cast(expr, _depth)
|
|
@@ -41,36 +63,36 @@ class GraphWeaver::Codegen
|
|
|
41
63
|
def coerce(expr) = @scalar.coerce_input(expr)
|
|
42
64
|
def coerce_input_type = @scalar.coerce_type
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
# inside input-struct hashes, scalars coerce exactly like variable
|
|
67
|
+
# kwargs do — the registry (incl. GraphWeaver.auto_coerce) decides
|
|
68
|
+
def hash_coerce(expr, _depth)
|
|
69
|
+
@scalar.coerce_input(expr) || expr
|
|
70
|
+
end
|
|
46
71
|
|
|
47
|
-
def
|
|
48
|
-
def nested = nil
|
|
72
|
+
def hash_coerce_identity? = !@scalar.coerce?
|
|
49
73
|
end
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
# NonNull is its inner node with the nilability stripped — everything
|
|
76
|
+
# else passes through.
|
|
77
|
+
class NonNull < Node
|
|
78
|
+
extend Forwardable
|
|
79
|
+
|
|
52
80
|
attr_reader :of
|
|
53
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
|
+
|
|
54
85
|
def initialize(of)
|
|
55
86
|
@of = of
|
|
56
87
|
end
|
|
57
88
|
|
|
58
|
-
def bare_type = @of.bare_type
|
|
59
89
|
def prop_type = bare_type
|
|
60
|
-
def cast(expr, depth) = @of.cast(expr, depth)
|
|
61
|
-
def identity? = @of.identity?
|
|
62
|
-
def serialize(expr, depth) = @of.serialize(expr, depth)
|
|
63
|
-
def serialize_identity? = @of.serialize_identity?
|
|
64
|
-
def coerce? = @of.coerce?
|
|
65
|
-
def coerce(expr) = @of.coerce(expr)
|
|
66
|
-
def coerce_input_type = @of.coerce_input_type
|
|
67
|
-
def hash_coerce(expr, depth) = @of.hash_coerce(expr, depth)
|
|
68
|
-
def hash_coerce_identity? = @of.hash_coerce_identity?
|
|
69
90
|
def non_null? = true
|
|
70
|
-
def nested = @of.nested
|
|
71
91
|
end
|
|
72
92
|
|
|
73
|
-
class List
|
|
93
|
+
class List < Node
|
|
94
|
+
attr_reader :of
|
|
95
|
+
|
|
74
96
|
def initialize(of)
|
|
75
97
|
@of = of
|
|
76
98
|
end
|
|
@@ -79,10 +101,6 @@ class GraphWeaver::Codegen
|
|
|
79
101
|
"T::Array[#{@of.prop_type}]"
|
|
80
102
|
end
|
|
81
103
|
|
|
82
|
-
def prop_type
|
|
83
|
-
"T.nilable(#{bare_type})"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
104
|
def cast(expr, depth)
|
|
87
105
|
var = "v#{depth}"
|
|
88
106
|
element = if @of.non_null? || @of.identity?
|
|
@@ -108,50 +126,46 @@ class GraphWeaver::Codegen
|
|
|
108
126
|
end
|
|
109
127
|
|
|
110
128
|
def serialize_identity? = @of.serialize_identity?
|
|
111
|
-
def coerce? = false
|
|
112
129
|
|
|
113
130
|
def hash_coerce(expr, depth)
|
|
114
131
|
var = "v#{depth}"
|
|
115
132
|
inner = if @of.non_null? || @of.hash_coerce_identity?
|
|
116
133
|
@of.hash_coerce_identity? ? var : @of.hash_coerce(var, depth + 1)
|
|
117
134
|
else
|
|
118
|
-
"#{var}
|
|
135
|
+
"#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
|
|
119
136
|
end
|
|
120
137
|
|
|
121
138
|
"#{expr}.map { |#{var}| #{inner} }"
|
|
122
139
|
end
|
|
123
140
|
|
|
124
141
|
def hash_coerce_identity? = @of.hash_coerce_identity?
|
|
125
|
-
def non_null? = false
|
|
126
142
|
def nested = @of.nested
|
|
127
143
|
end
|
|
128
144
|
|
|
129
|
-
class ObjectNode
|
|
145
|
+
class ObjectNode < Node
|
|
130
146
|
Field = Struct.new(:prop, :key, :node)
|
|
131
147
|
|
|
132
148
|
attr_reader :class_name, :fields
|
|
149
|
+
# the GraphQL type this struct was generated from, and any registered
|
|
150
|
+
# helper modules to include (see Codegen.register_type)
|
|
151
|
+
attr_accessor :graphql_type, :mixins
|
|
133
152
|
|
|
134
153
|
def initialize(class_name)
|
|
135
154
|
@class_name = class_name
|
|
136
155
|
@fields = []
|
|
156
|
+
@mixins = []
|
|
137
157
|
end
|
|
138
158
|
|
|
139
159
|
def bare_type = class_name
|
|
140
160
|
|
|
141
|
-
def prop_type
|
|
142
|
-
"T.nilable(#{bare_type})"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
161
|
def cast(expr, _depth)
|
|
146
162
|
"#{class_name}.from_h(#{expr})"
|
|
147
163
|
end
|
|
148
164
|
|
|
149
|
-
def identity? = false
|
|
150
|
-
def non_null? = false
|
|
151
165
|
def nested = self
|
|
152
166
|
end
|
|
153
167
|
|
|
154
|
-
class EnumNode
|
|
168
|
+
class EnumNode < Node
|
|
155
169
|
attr_reader :class_name, :values
|
|
156
170
|
|
|
157
171
|
def initialize(class_name, values)
|
|
@@ -161,34 +175,92 @@ class GraphWeaver::Codegen
|
|
|
161
175
|
|
|
162
176
|
def bare_type = class_name
|
|
163
177
|
|
|
164
|
-
def prop_type
|
|
165
|
-
"T.nilable(#{bare_type})"
|
|
166
|
-
end
|
|
167
|
-
|
|
168
178
|
def cast(expr, _depth)
|
|
169
179
|
"#{class_name}.deserialize(#{expr})"
|
|
170
180
|
end
|
|
171
181
|
|
|
172
|
-
def identity? = false
|
|
173
|
-
|
|
174
182
|
def serialize(expr, _depth)
|
|
175
183
|
"#{expr}.serialize"
|
|
176
184
|
end
|
|
177
185
|
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
# enums always coerce: a kwarg or hash field accepts the T::Enum or
|
|
187
|
+
# its wire value (deserialize raises on anything else)
|
|
188
|
+
def coerce? = true
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
def hash_coerce(expr, _depth)
|
|
190
|
+
def coerce(expr)
|
|
183
191
|
"(#{expr}.is_a?(#{class_name}) ? #{expr} : #{class_name}.deserialize(#{expr}))"
|
|
184
192
|
end
|
|
185
193
|
|
|
186
|
-
def
|
|
187
|
-
def
|
|
194
|
+
def coerce_input_type = "T.any(#{class_name}, String)"
|
|
195
|
+
def hash_coerce(expr, _depth) = coerce(expr)
|
|
188
196
|
def nested = self
|
|
189
197
|
end
|
|
190
198
|
|
|
191
|
-
|
|
199
|
+
# A GraphQL enum mapped onto an app-owned T::Enum (see EnumType): no
|
|
200
|
+
# generated enum class — instead module-level <NAME>_FROM_WIRE /
|
|
201
|
+
# <NAME>_TO_WIRE constants translate at the boundary. fallback: makes
|
|
202
|
+
# casting absorb unknown wire values (inputs stay strict).
|
|
203
|
+
class MappedEnum < Node
|
|
204
|
+
attr_reader :graphql_name, :mapping
|
|
205
|
+
|
|
206
|
+
def initialize(enum_type, wire_values)
|
|
207
|
+
@graphql_name = enum_type.graphql_name
|
|
208
|
+
@type_name = enum_type.type.name
|
|
209
|
+
@fallback = enum_type.fallback
|
|
210
|
+
@mapping = enum_type.mapping_for(wire_values)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def const_prefix = GraphWeaver::Inflect.underscore(@graphql_name).upcase
|
|
214
|
+
def fallback_const = @fallback && "#{@type_name}.deserialize(#{@fallback.serialize.to_s.inspect})"
|
|
215
|
+
|
|
216
|
+
def bare_type = @type_name
|
|
217
|
+
|
|
218
|
+
def cast(expr, _depth)
|
|
219
|
+
if @fallback
|
|
220
|
+
"#{const_prefix}_FROM_WIRE.fetch(#{expr}) { #{fallback_const} }"
|
|
221
|
+
else
|
|
222
|
+
"#{const_prefix}_FROM_WIRE.fetch(#{expr})"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def serialize(expr, _depth)
|
|
227
|
+
"#{const_prefix}_TO_WIRE.fetch(#{expr})"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# kwargs and hash fields accept the member or its wire value; unlike
|
|
231
|
+
# casting, bad input raises even with a fallback (a typo'd input is
|
|
232
|
+
# our bug, not server drift)
|
|
233
|
+
def coerce? = true
|
|
234
|
+
|
|
235
|
+
def coerce(expr)
|
|
236
|
+
"(#{expr}.is_a?(#{@type_name}) ? #{expr} : #{const_prefix}_FROM_WIRE.fetch(#{expr}))"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def coerce_input_type = "T.any(#{@type_name}, String)"
|
|
240
|
+
def hash_coerce(expr, _depth) = coerce(expr)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A single-condition narrowing of an abstract field (`... on Pet { ... }`
|
|
244
|
+
# and nothing else): the member struct when the runtime type matches,
|
|
245
|
+
# nil when it doesn't — a non-match's response object carries no
|
|
246
|
+
# matching fields, so the hash arrives empty. Always nilable, whatever
|
|
247
|
+
# the schema's nullability, because narrowing filters.
|
|
248
|
+
class NarrowedNode < Node
|
|
249
|
+
def initialize(of)
|
|
250
|
+
@of = of
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def class_name = @of.class_name
|
|
254
|
+
def bare_type = @of.bare_type
|
|
255
|
+
|
|
256
|
+
def cast(expr, depth)
|
|
257
|
+
"(#{expr}.empty? ? nil : #{@of.cast(expr, depth)})"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def nested = @of
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class UnionNode < Node
|
|
192
264
|
attr_reader :class_name, :members # graphql type name => ObjectNode
|
|
193
265
|
|
|
194
266
|
def initialize(class_name, members)
|
|
@@ -198,16 +270,10 @@ class GraphWeaver::Codegen
|
|
|
198
270
|
|
|
199
271
|
def bare_type = "#{class_name}::Type"
|
|
200
272
|
|
|
201
|
-
def prop_type
|
|
202
|
-
"T.nilable(#{bare_type})"
|
|
203
|
-
end
|
|
204
|
-
|
|
205
273
|
def cast(expr, _depth)
|
|
206
274
|
"#{class_name}.from_h(#{expr})"
|
|
207
275
|
end
|
|
208
276
|
|
|
209
|
-
def identity? = false
|
|
210
|
-
def non_null? = false
|
|
211
277
|
def nested = self
|
|
212
278
|
end
|
|
213
279
|
|
|
@@ -215,7 +281,7 @@ class GraphWeaver::Codegen
|
|
|
215
281
|
# serialize produces the wire hash. Inputs never cast FROM the wire.
|
|
216
282
|
# Joins the coerce protocol so execute kwargs accept plain hashes,
|
|
217
283
|
# normalized (and type-checked) through the generated .coerce.
|
|
218
|
-
class InputNode
|
|
284
|
+
class InputNode < Node
|
|
219
285
|
Field = Struct.new(:prop, :wire, :node, :required)
|
|
220
286
|
|
|
221
287
|
attr_reader :class_name, :fields
|
|
@@ -227,18 +293,12 @@ class GraphWeaver::Codegen
|
|
|
227
293
|
|
|
228
294
|
def bare_type = class_name
|
|
229
295
|
|
|
230
|
-
def prop_type
|
|
231
|
-
"T.nilable(#{bare_type})"
|
|
232
|
-
end
|
|
233
|
-
|
|
234
296
|
def serialize(expr, _depth)
|
|
235
297
|
"#{expr}.serialize"
|
|
236
298
|
end
|
|
237
299
|
|
|
238
|
-
def serialize_identity? = false
|
|
239
|
-
|
|
240
300
|
def cast(_expr, _depth)
|
|
241
|
-
raise
|
|
301
|
+
raise GraphWeaver::Error, "input objects are never cast from responses"
|
|
242
302
|
end
|
|
243
303
|
|
|
244
304
|
def coerce? = true
|
|
@@ -247,10 +307,5 @@ class GraphWeaver::Codegen
|
|
|
247
307
|
|
|
248
308
|
# building a struct field from a caller-supplied plain hash value
|
|
249
309
|
def hash_coerce(expr, _depth) = "#{class_name}.coerce(#{expr})"
|
|
250
|
-
def hash_coerce_identity? = false
|
|
251
|
-
|
|
252
|
-
def identity? = false
|
|
253
|
-
def non_null? = false
|
|
254
|
-
def nested = nil
|
|
255
310
|
end
|
|
256
311
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# typed: true
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
4
6
|
class GraphWeaver::Codegen
|
|
5
7
|
|
|
6
8
|
# How one GraphQL scalar maps to Ruby: the Sorbet prop type, the
|
|
@@ -15,17 +17,17 @@ class GraphWeaver::Codegen
|
|
|
15
17
|
# from the Ruby type when it is a real class, by probing for a known
|
|
16
18
|
# deserializer and pairing its serializer (see CODECS) — so the common
|
|
17
19
|
# case needs no more than a class:
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
+
# type: Money (defines .parse) => Money.parse(expr) / expr.to_s
|
|
21
|
+
# type: Blob (defines .load) => Blob.load(expr) / Blob.dump(expr)
|
|
20
22
|
# Probing the *deserialize* side is deliberate: every object has #to_s,
|
|
21
23
|
# so inferring a serializer off it would wrongly wrap plain types (String,
|
|
22
24
|
# Integer) — pairing off a deserializer the type actually defines avoids
|
|
23
25
|
# that. Override with an explicit value:
|
|
24
26
|
# - a Symbol names a method, so there is no string to misspell:
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
+
# cast: :load => "Blob.load(expr)" (class method on type)
|
|
28
|
+
# serialize: :to_json => "expr.to_json" (instance method)
|
|
27
29
|
# - a Proc handles anything a Symbol can't express:
|
|
28
|
-
#
|
|
30
|
+
# cast: ->(e) { "Money.new(#{e})" }
|
|
29
31
|
# - :itself opts out — force identity pass-through even when a codec
|
|
30
32
|
# would otherwise match (rare)
|
|
31
33
|
# requires: a String or Array of paths emitted as `require`s atop the
|
|
@@ -56,23 +58,44 @@ class GraphWeaver::Codegen
|
|
|
56
58
|
|
|
57
59
|
attr_reader :graphql_name, :type, :requires
|
|
58
60
|
|
|
59
|
-
def initialize(graphql_name, type
|
|
61
|
+
def initialize(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
|
|
60
62
|
@graphql_name = graphql_name.to_s
|
|
61
63
|
@klass = type.is_a?(Module) ? type : nil
|
|
62
64
|
@type = type_name(type)
|
|
65
|
+
# requires: load BEFORE codec probing — the probe method may come
|
|
66
|
+
# from the required file (core Time has no .parse until the "time"
|
|
67
|
+
# stdlib loads)
|
|
68
|
+
@requires = normalize_requires(requires)
|
|
63
69
|
codec = @klass && CODECS.find { |c| @klass.respond_to?(c.probe) }
|
|
64
70
|
@cast = normalize_cast(cast, codec&.cast)
|
|
65
71
|
@serialize = normalize_serialize(serialize, codec&.serialize)
|
|
66
|
-
@requires = normalize_requires(requires)
|
|
67
72
|
@coerce = coerce
|
|
68
73
|
validate_coerce!
|
|
69
74
|
end
|
|
70
75
|
|
|
76
|
+
# conversions applied to the four convertible built-ins when the
|
|
77
|
+
# global GraphWeaver.auto_coerce is on and no explicit coerce: given
|
|
78
|
+
AUTO_CONVERSIONS = {
|
|
79
|
+
"ID" => :to_s, "String" => :to_s, "Int" => :to_i, "Float" => :to_f,
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
71
82
|
def cast(expr) = @cast&.call(expr)
|
|
72
83
|
def cast? = !@cast.nil?
|
|
73
84
|
def serialize(expr) = @serialize&.call(expr)
|
|
74
85
|
def serialize? = !@serialize.nil?
|
|
75
|
-
def coerce? =
|
|
86
|
+
def coerce? = !!effective_coerce
|
|
87
|
+
|
|
88
|
+
# Explicit coerce: always wins (false means never). Left unset, the
|
|
89
|
+
# global GraphWeaver.auto_coerce decides — resolved HERE, at
|
|
90
|
+
# generation time, so registration order doesn't matter: convertible
|
|
91
|
+
# built-ins get their conversion, anything with a full cast/serialize
|
|
92
|
+
# pair gets parse-style coercion.
|
|
93
|
+
def effective_coerce
|
|
94
|
+
return @coerce unless @coerce.nil?
|
|
95
|
+
return false unless GraphWeaver.auto_coerce
|
|
96
|
+
|
|
97
|
+
AUTO_CONVERSIONS.fetch(@graphql_name) { (cast? && serialize?) || nil }
|
|
98
|
+
end
|
|
76
99
|
|
|
77
100
|
# The code that normalizes a variable input before it's serialized. Two
|
|
78
101
|
# shapes: coerce: true parses a raw value into the rich type via the cast
|
|
@@ -82,17 +105,17 @@ class GraphWeaver::Codegen
|
|
|
82
105
|
# afterward, but is identity for the conversion built-ins, so the
|
|
83
106
|
# converted value goes on the wire natively (a Float, not "5.0").
|
|
84
107
|
def coerce_input(expr)
|
|
85
|
-
case
|
|
108
|
+
case effective_coerce
|
|
86
109
|
when true then "(#{expr}.is_a?(#{@type}) ? #{expr} : #{cast(expr)})"
|
|
87
|
-
when Symbol then "#{expr}.#{
|
|
110
|
+
when Symbol then "#{expr}.#{effective_coerce}"
|
|
88
111
|
end
|
|
89
112
|
end
|
|
90
113
|
|
|
91
114
|
# the accepted Sorbet type for a coercible variable kwarg
|
|
92
115
|
def coerce_type
|
|
93
|
-
case
|
|
116
|
+
case effective_coerce
|
|
94
117
|
when true then "T.any(#{@type}, String)"
|
|
95
|
-
when Symbol then CONVERT_INPUTS.fetch(
|
|
118
|
+
when Symbol then CONVERT_INPUTS.fetch(effective_coerce, "T.untyped")
|
|
96
119
|
end
|
|
97
120
|
end
|
|
98
121
|
|
|
@@ -175,9 +198,9 @@ class GraphWeaver::Codegen
|
|
|
175
198
|
# the accepted cast:/serialize:/requires: forms. Later registrations
|
|
176
199
|
# win, so an app can override a built-in (e.g. map Date onto its own
|
|
177
200
|
# type).
|
|
178
|
-
def register_scalar(graphql_name, type
|
|
201
|
+
def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
|
|
179
202
|
scalar_registry[graphql_name.to_s] =
|
|
180
|
-
ScalarType.new(graphql_name, type
|
|
203
|
+
ScalarType.new(graphql_name, type, cast:, serialize:, requires:, coerce:)
|
|
181
204
|
end
|
|
182
205
|
|
|
183
206
|
# The ScalarType for a scalar name; unknown scalars fall back to an
|
|
@@ -185,7 +208,7 @@ class GraphWeaver::Codegen
|
|
|
185
208
|
# scalars outside the table.
|
|
186
209
|
def scalar(graphql_name)
|
|
187
210
|
scalar_registry.fetch(graphql_name.to_s) do
|
|
188
|
-
ScalarType.new(graphql_name,
|
|
211
|
+
ScalarType.new(graphql_name, "T.untyped")
|
|
189
212
|
end
|
|
190
213
|
end
|
|
191
214
|
|
|
@@ -201,14 +224,12 @@ class GraphWeaver::Codegen
|
|
|
201
224
|
end
|
|
202
225
|
|
|
203
226
|
# Drop every custom registration and restore the built-in scalars — the
|
|
204
|
-
# clean slate to reach for between tests, or to undo overrides.
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
# built-in by hand.
|
|
209
|
-
def reset_scalars!(coerce: false)
|
|
227
|
+
# clean slate to reach for between tests, or to undo overrides. (Want
|
|
228
|
+
# the built-ins to coerce loose input? That's GraphWeaver.auto_coerce,
|
|
229
|
+
# resolved at generation time — no re-registering.)
|
|
230
|
+
def reset_scalars!
|
|
210
231
|
clear_scalars!
|
|
211
|
-
register_builtin_scalars!
|
|
232
|
+
register_builtin_scalars!
|
|
212
233
|
self
|
|
213
234
|
end
|
|
214
235
|
|
|
@@ -218,19 +239,16 @@ class GraphWeaver::Codegen
|
|
|
218
239
|
# matches nothing and leaves them identity — which is exactly why we
|
|
219
240
|
# can name them with the real class constants. Date deserializes via
|
|
220
241
|
# ISO-8601 (it *does* define .parse, but we want iso8601 specifically,
|
|
221
|
-
# so it's explicit).
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
register_scalar "
|
|
229
|
-
register_scalar "
|
|
230
|
-
register_scalar "
|
|
231
|
-
register_scalar "Float", type: Float, coerce: (:to_f if coerce)
|
|
232
|
-
register_scalar "Boolean", type: "T::Boolean"
|
|
233
|
-
register_scalar "Date", type: Date, cast: :iso8601, serialize: :iso8601, requires: "date"
|
|
242
|
+
# so it's explicit). Input coercion is a generation-time concern:
|
|
243
|
+
# GraphWeaver.auto_coerce gives the convertible built-ins their
|
|
244
|
+
# conversion (see ScalarType::AUTO_CONVERSIONS).
|
|
245
|
+
def register_builtin_scalars!
|
|
246
|
+
register_scalar "ID", String
|
|
247
|
+
register_scalar "String", String
|
|
248
|
+
register_scalar "Int", Integer
|
|
249
|
+
register_scalar "Float", Float
|
|
250
|
+
register_scalar "Boolean", "T::Boolean"
|
|
251
|
+
register_scalar "Date", Date, cast: :iso8601, serialize: :iso8601, requires: "date"
|
|
234
252
|
end
|
|
235
253
|
end
|
|
236
254
|
|