graph_weaver 0.2.2 → 0.4.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 +65 -0
- data/CLAUDE.md +70 -0
- data/Gemfile.lock +21 -21
- data/README.md +2 -0
- data/docs/errors.md +25 -5
- data/docs/federation.md +100 -0
- data/docs/generated_modules.md +7 -4
- data/docs/getting_started.md +52 -0
- data/docs/scalars.md +15 -2
- data/lib/graph_weaver/client.rb +4 -3
- data/lib/graph_weaver/codegen/emit.rb +46 -10
- data/lib/graph_weaver/codegen/enum_type.rb +5 -5
- data/lib/graph_weaver/codegen/nodes.rb +21 -1
- data/lib/graph_weaver/codegen.rb +201 -13
- data/lib/graph_weaver/errors.rb +145 -19
- data/lib/graph_weaver/hints.rb +6 -1
- data/lib/graph_weaver/input_struct.rb +8 -0
- data/lib/graph_weaver/response.rb +2 -2
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +109 -41
- metadata +3 -1
data/lib/graph_weaver/errors.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
@@ -12,16 +12,20 @@ module GraphWeaver
|
|
|
12
12
|
# structured failures to users. One subclass per failure site —
|
|
13
13
|
# {TransportError} (never reached the server), {ServerError} (non-2xx),
|
|
14
14
|
# {QueryError} (GraphQL-level errors), {TypeError} (response wouldn't
|
|
15
|
-
# cast), {ValidationError} (build time) —
|
|
16
|
-
# into #to_h.
|
|
15
|
+
# cast), {InputError} (bad variables), {ValidationError} (build time) —
|
|
16
|
+
# each merging its specifics into #to_h.
|
|
17
17
|
class Error < StandardError
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
18
20
|
# every GraphWeaver error surfaces on the logger too (see
|
|
19
21
|
# GraphWeaver.logger) — construction here means a raise
|
|
22
|
+
sig { params(args: T.untyped).void }
|
|
20
23
|
def initialize(*args)
|
|
21
24
|
super
|
|
22
25
|
GraphWeaver.log(:warn) { "#{self.class.name}: #{message}" }
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
sig { overridable.returns(T::Hash[String, T.untyped]) }
|
|
25
29
|
def to_h
|
|
26
30
|
{ "error" => self.class.name, "message" => message }
|
|
27
31
|
end
|
|
@@ -31,12 +35,17 @@ module GraphWeaver
|
|
|
31
35
|
# TLS handshake, timeout. The original exception is preserved as #cause.
|
|
32
36
|
# Generally retriable.
|
|
33
37
|
class TransportError < Error
|
|
38
|
+
extend T::Sig
|
|
39
|
+
|
|
40
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
|
34
41
|
def to_h
|
|
35
42
|
super.merge("cause" => cause&.class&.name)
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
class << self
|
|
47
|
+
extend T::Sig
|
|
48
|
+
|
|
40
49
|
# The exception classes the bundled transports reclassify as
|
|
41
50
|
# TransportError — network-level failures where the request never
|
|
42
51
|
# reached the server. A mutable Set: each transport contributes its own
|
|
@@ -48,11 +57,16 @@ module GraphWeaver
|
|
|
48
57
|
#
|
|
49
58
|
# SystemCallError covers every Errno::* (connection refused/reset, host
|
|
50
59
|
# unreachable); SocketError covers DNS.
|
|
60
|
+
sig { returns(T::Set[T.class_of(Exception)]) }
|
|
51
61
|
def transport_errors
|
|
52
|
-
@transport_errors ||=
|
|
62
|
+
@transport_errors ||= T.let(
|
|
63
|
+
Set[SocketError, SystemCallError, IOError],
|
|
64
|
+
T.nilable(T::Set[T.class_of(Exception)]),
|
|
65
|
+
)
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
# Add one or more exception classes to the transport-error set.
|
|
69
|
+
sig { params(classes: T.class_of(Exception)).returns(T::Array[T.class_of(Exception)]) }
|
|
56
70
|
def register_transport_error(*classes)
|
|
57
71
|
transport_errors.merge(classes)
|
|
58
72
|
classes
|
|
@@ -63,8 +77,15 @@ module GraphWeaver
|
|
|
63
77
|
# that exploded, a 502 from a proxy, a 401, etc. Distinct from a GraphQL
|
|
64
78
|
# error: we got an HTTP response, it just wasn't success.
|
|
65
79
|
class ServerError < Error
|
|
66
|
-
|
|
80
|
+
extend T::Sig
|
|
67
81
|
|
|
82
|
+
sig { returns(Integer) }
|
|
83
|
+
attr_reader :status
|
|
84
|
+
|
|
85
|
+
sig { returns(T.untyped) }
|
|
86
|
+
attr_reader :body
|
|
87
|
+
|
|
88
|
+
sig { params(status: Integer, body: T.untyped).void }
|
|
68
89
|
def initialize(status:, body: nil)
|
|
69
90
|
@status = status
|
|
70
91
|
@body = body
|
|
@@ -72,6 +93,7 @@ module GraphWeaver
|
|
|
72
93
|
super("HTTP #{status}#{snippet}")
|
|
73
94
|
end
|
|
74
95
|
|
|
96
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
|
75
97
|
def to_h
|
|
76
98
|
super.merge("status" => status)
|
|
77
99
|
end
|
|
@@ -81,16 +103,38 @@ module GraphWeaver
|
|
|
81
103
|
# object (not raised) — the response envelope and QueryError carry these.
|
|
82
104
|
# Match on #code (extensions["code"]) rather than the message string.
|
|
83
105
|
class GraphQLError
|
|
84
|
-
|
|
106
|
+
extend T::Sig
|
|
107
|
+
|
|
108
|
+
sig { returns(String) }
|
|
109
|
+
attr_reader :message
|
|
110
|
+
|
|
111
|
+
sig { returns(T::Array[T.untyped]) }
|
|
112
|
+
attr_reader :locations
|
|
85
113
|
|
|
114
|
+
sig { returns(T.nilable(T::Array[T.untyped])) }
|
|
115
|
+
attr_reader :path
|
|
116
|
+
|
|
117
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
118
|
+
attr_reader :extensions
|
|
119
|
+
|
|
120
|
+
sig do
|
|
121
|
+
params(
|
|
122
|
+
message: String,
|
|
123
|
+
locations: T::Array[T.untyped],
|
|
124
|
+
path: T.nilable(T::Array[T.untyped]),
|
|
125
|
+
extensions: T::Hash[String, T.untyped],
|
|
126
|
+
type: T.nilable(String),
|
|
127
|
+
).void
|
|
128
|
+
end
|
|
86
129
|
def initialize(message:, locations: [], path: nil, extensions: {}, type: nil)
|
|
87
130
|
@message = message
|
|
88
131
|
@locations = locations
|
|
89
132
|
@path = path
|
|
90
133
|
@extensions = extensions
|
|
91
|
-
@error_type = type
|
|
134
|
+
@error_type = T.let(type, T.nilable(String))
|
|
92
135
|
end
|
|
93
136
|
|
|
137
|
+
sig { params(hash: T::Hash[String, T.untyped]).returns(GraphQLError) }
|
|
94
138
|
def self.from_h(hash)
|
|
95
139
|
new(
|
|
96
140
|
message: hash["message"] || "(no message)",
|
|
@@ -105,6 +149,7 @@ module GraphWeaver
|
|
|
105
149
|
# branch on. Read from extensions.code (the Apollo/spec-adjacent
|
|
106
150
|
# convention) or a top-level "type" (GitHub's dialect: NOT_FOUND,
|
|
107
151
|
# FORBIDDEN). nil when the server sent neither.
|
|
152
|
+
sig { returns(T.nilable(String)) }
|
|
108
153
|
def code
|
|
109
154
|
extensions["code"] || @error_type
|
|
110
155
|
end
|
|
@@ -113,11 +158,15 @@ module GraphWeaver
|
|
|
113
158
|
# (unknown field/type/argument). Heuristic by necessity: only Apollo
|
|
114
159
|
# sets a standard code (GRAPHQL_VALIDATION_FAILED); graphql-ruby and
|
|
115
160
|
# GitHub speak in messages.
|
|
116
|
-
VALIDATION_MESSAGE =
|
|
161
|
+
VALIDATION_MESSAGE = T.let(
|
|
162
|
+
/doesn't exist|Cannot query field|Unknown (field|type|argument)|isn't defined|undefined (field|type)/i,
|
|
163
|
+
Regexp,
|
|
164
|
+
)
|
|
117
165
|
|
|
118
166
|
# True when this error looks like the server rejected the query's
|
|
119
167
|
# shape — for a generated module that usually means the schema
|
|
120
168
|
# changed after generation.
|
|
169
|
+
sig { returns(T::Boolean) }
|
|
121
170
|
def validation?
|
|
122
171
|
code == "GRAPHQL_VALIDATION_FAILED" || VALIDATION_MESSAGE.match?(message)
|
|
123
172
|
end
|
|
@@ -126,17 +175,21 @@ module GraphWeaver
|
|
|
126
175
|
# indices stripped — ["people", 3, "email"] => "people.email". The
|
|
127
176
|
# parseable key for grouping/reporting (the raw #path keeps indices).
|
|
128
177
|
# nil for global errors with no path.
|
|
178
|
+
sig { returns(T.nilable(String)) }
|
|
129
179
|
def field
|
|
130
|
-
|
|
180
|
+
p = path
|
|
181
|
+
return unless p
|
|
131
182
|
|
|
132
|
-
named =
|
|
183
|
+
named = p.reject { |seg| seg.is_a?(Integer) || seg.to_s.match?(/\A\d+\z/) }
|
|
133
184
|
named.join(".") unless named.empty?
|
|
134
185
|
end
|
|
135
186
|
|
|
187
|
+
sig { returns(String) }
|
|
136
188
|
def to_s
|
|
137
|
-
loc = locations
|
|
189
|
+
loc = locations.first
|
|
138
190
|
at = loc ? " at #{loc["line"]}:#{loc["column"]}" : ""
|
|
139
|
-
|
|
191
|
+
p = path
|
|
192
|
+
where = p ? " (path: #{p.join(".")})" : ""
|
|
140
193
|
tag = code ? " [#{code}]" : ""
|
|
141
194
|
"#{message}#{at}#{where}#{tag}"
|
|
142
195
|
end
|
|
@@ -144,6 +197,7 @@ module GraphWeaver
|
|
|
144
197
|
# JSON-ready: the problematic field (both forms — #field for grouping,
|
|
145
198
|
# #path with indices for exact location), the machine code, and the
|
|
146
199
|
# server's full extensions.
|
|
200
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
147
201
|
def to_h
|
|
148
202
|
{
|
|
149
203
|
"message" => message,
|
|
@@ -158,15 +212,18 @@ module GraphWeaver
|
|
|
158
212
|
end
|
|
159
213
|
|
|
160
214
|
# Shared filtering over a collection of GraphQLErrors, for surfacing
|
|
161
|
-
# field-level failures programmatically. Host must define #errors.
|
|
215
|
+
# field-level failures programmatically. Host must define #errors / #data.
|
|
162
216
|
module ErrorFiltering
|
|
217
|
+
extend T::Sig
|
|
163
218
|
include Kernel # for sorbet: hosts are Objects
|
|
164
219
|
|
|
165
220
|
# the host's interface, overridden by its attr_readers
|
|
221
|
+
sig { overridable.returns(T::Array[GraphQLError]) }
|
|
166
222
|
def errors
|
|
167
223
|
raise NotImplementedError, "#{self.class} must define #errors"
|
|
168
224
|
end
|
|
169
225
|
|
|
226
|
+
sig { overridable.returns(T.untyped) }
|
|
170
227
|
def data
|
|
171
228
|
raise NotImplementedError, "#{self.class} must define #data"
|
|
172
229
|
end
|
|
@@ -174,15 +231,20 @@ module GraphWeaver
|
|
|
174
231
|
# Errors touching a field path — "user.email" or ["user", "email"];
|
|
175
232
|
# prefix match, so deeper errors count too. List indices appear as
|
|
176
233
|
# path segments ("people.0.email").
|
|
234
|
+
sig { params(path: T.any(String, T::Array[T.untyped])).returns(T::Array[GraphQLError]) }
|
|
177
235
|
def errors_at(path)
|
|
178
236
|
want = (path.is_a?(String) ? path.split(".") : path).map(&:to_s)
|
|
179
|
-
errors.select
|
|
237
|
+
errors.select do |error|
|
|
238
|
+
p = error.path
|
|
239
|
+
p && p.map(&:to_s).first(want.size) == want
|
|
240
|
+
end
|
|
180
241
|
end
|
|
181
242
|
|
|
182
243
|
# True when any error looks like the server rejected the query's
|
|
183
244
|
# shape — the schema has likely changed since the module was
|
|
184
245
|
# generated. Refresh the schema dump and regenerate (rake
|
|
185
246
|
# graph_weaver:schema:refresh && rake graph_weaver:generate).
|
|
247
|
+
sig { returns(T::Boolean) }
|
|
186
248
|
def schema_stale?
|
|
187
249
|
errors.any?(&:validation?)
|
|
188
250
|
end
|
|
@@ -193,25 +255,31 @@ module GraphWeaver
|
|
|
193
255
|
# response.each_error do |field, errors|
|
|
194
256
|
# form.add_error(field, errors.map(&:message))
|
|
195
257
|
# end
|
|
258
|
+
sig { returns(T::Hash[T.nilable(String), T::Array[GraphQLError]]) }
|
|
196
259
|
def errors_by_field
|
|
197
260
|
errors.group_by(&:field)
|
|
198
261
|
end
|
|
199
262
|
|
|
263
|
+
sig do
|
|
264
|
+
params(block: T.proc.params(field: T.nilable(String), errors: T::Array[GraphQLError]).void).void
|
|
265
|
+
end
|
|
200
266
|
def each_error(&block)
|
|
201
|
-
errors_by_field.each(
|
|
267
|
+
errors_by_field.each { |field, errs| block.call(field, errs) }
|
|
202
268
|
end
|
|
203
269
|
|
|
204
270
|
# The id of the record an error points into, resolved by walking the
|
|
205
271
|
# error's path through the (partial) typed data: an error at
|
|
206
272
|
# ["people", 3, "email"] resolves to people[3].id. nil when the data
|
|
207
273
|
# is missing, the path doesn't walk, or the record has no id field.
|
|
274
|
+
sig { params(error: GraphQLError).returns(T.untyped) }
|
|
208
275
|
def entity_id(error)
|
|
209
276
|
# untyped by nature: the walk traverses whatever structs this query
|
|
210
277
|
# generated, reassigning across types at each step
|
|
211
278
|
node = T.let(data, T.untyped)
|
|
212
|
-
|
|
279
|
+
path = error.path
|
|
280
|
+
return unless node && path
|
|
213
281
|
|
|
214
|
-
|
|
282
|
+
(path[0..-2] || []).each do |segment|
|
|
215
283
|
node = if segment.is_a?(Integer) || segment.to_s.match?(/\A\d+\z/)
|
|
216
284
|
node.is_a?(Array) ? node[segment.to_i] : nil
|
|
217
285
|
else
|
|
@@ -230,6 +298,7 @@ module GraphWeaver
|
|
|
230
298
|
#
|
|
231
299
|
# { "people.email" => { "messages" => [...], "codes" => [...],
|
|
232
300
|
# "entity_ids" => ["7", "9"], "errors" => [full to_h...] } }
|
|
301
|
+
sig { returns(T::Hash[T.nilable(String), T::Hash[String, T.untyped]]) }
|
|
233
302
|
def report
|
|
234
303
|
errors_by_field.to_h do |field, field_errors|
|
|
235
304
|
[field, {
|
|
@@ -247,10 +316,25 @@ module GraphWeaver
|
|
|
247
316
|
# Carries the structured errors, any partial data, and top-level
|
|
248
317
|
# extensions (cost/throttle metadata).
|
|
249
318
|
class QueryError < Error
|
|
319
|
+
extend T::Sig
|
|
250
320
|
include ErrorFiltering
|
|
251
321
|
|
|
252
|
-
|
|
322
|
+
sig { override.returns(T::Array[GraphQLError]) }
|
|
323
|
+
attr_reader :errors
|
|
324
|
+
|
|
325
|
+
sig { override.returns(T.untyped) }
|
|
326
|
+
attr_reader :data
|
|
253
327
|
|
|
328
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
329
|
+
attr_reader :extensions
|
|
330
|
+
|
|
331
|
+
sig do
|
|
332
|
+
params(
|
|
333
|
+
errors: T::Array[GraphQLError],
|
|
334
|
+
data: T.untyped,
|
|
335
|
+
extensions: T::Hash[String, T.untyped],
|
|
336
|
+
).void
|
|
337
|
+
end
|
|
254
338
|
def initialize(errors, data: nil, extensions: {})
|
|
255
339
|
@errors = errors
|
|
256
340
|
@data = data
|
|
@@ -259,12 +343,14 @@ module GraphWeaver
|
|
|
259
343
|
end
|
|
260
344
|
|
|
261
345
|
# All non-nil error codes — handy for `codes.include?("THROTTLED")`.
|
|
346
|
+
sig { returns(T::Array[String]) }
|
|
262
347
|
def codes
|
|
263
|
-
errors.
|
|
348
|
+
errors.filter_map(&:code)
|
|
264
349
|
end
|
|
265
350
|
|
|
266
351
|
# The machine side: every error with its path/code/extensions, plus
|
|
267
352
|
# the drift verdict — nest this straight into a JSON response.
|
|
353
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
|
268
354
|
def to_h
|
|
269
355
|
super.merge(
|
|
270
356
|
"schema_stale" => schema_stale?,
|
|
@@ -276,6 +362,7 @@ module GraphWeaver
|
|
|
276
362
|
|
|
277
363
|
private
|
|
278
364
|
|
|
365
|
+
sig { returns(String) }
|
|
279
366
|
def summary
|
|
280
367
|
first = errors.first
|
|
281
368
|
more = errors.size > 1 ? " (and #{errors.size - 1} more)" : ""
|
|
@@ -291,30 +378,69 @@ module GraphWeaver
|
|
|
291
378
|
# #cause carries the original TypeError/KeyError with the offending
|
|
292
379
|
# prop in its message.
|
|
293
380
|
class TypeError < Error
|
|
381
|
+
extend T::Sig
|
|
382
|
+
|
|
383
|
+
sig { returns(T.untyped) }
|
|
294
384
|
attr_reader :struct
|
|
295
385
|
|
|
386
|
+
sig { params(struct: T.untyped, error: T.nilable(Exception), message: T.nilable(String)).void }
|
|
296
387
|
def initialize(struct:, error: nil, message: nil)
|
|
297
388
|
@struct = struct
|
|
298
389
|
super("failed to cast response into #{struct}: #{message || error&.message}")
|
|
299
390
|
end
|
|
300
391
|
|
|
392
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
|
301
393
|
def to_h
|
|
302
394
|
super.merge("struct" => struct.to_s, "cause" => cause&.message)
|
|
303
395
|
end
|
|
304
396
|
end
|
|
305
397
|
|
|
398
|
+
# Raised when the variables passed to a query or mutation can't be built
|
|
399
|
+
# into the generated input structs — an unknown or typo'd input key, a
|
|
400
|
+
# missing required input field, an out-of-range enum, or a wrong-typed
|
|
401
|
+
# field. This is the "the caller's input was invalid" error: rescue it at
|
|
402
|
+
# an API boundary to return a 422. #field names the offending input field
|
|
403
|
+
# when known, #struct the input type being built; the underlying
|
|
404
|
+
# TypeError/KeyError/ArgumentError is preserved as #cause.
|
|
405
|
+
class InputError < Error
|
|
406
|
+
extend T::Sig
|
|
407
|
+
|
|
408
|
+
sig { returns(T.nilable(String)) }
|
|
409
|
+
attr_reader :field
|
|
410
|
+
|
|
411
|
+
sig { returns(T.untyped) }
|
|
412
|
+
attr_reader :struct
|
|
413
|
+
|
|
414
|
+
sig { params(message: String, field: T.nilable(String), struct: T.untyped).void }
|
|
415
|
+
def initialize(message, field: nil, struct: nil)
|
|
416
|
+
@field = field
|
|
417
|
+
@struct = struct
|
|
418
|
+
super(message)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
|
422
|
+
def to_h
|
|
423
|
+
super.merge("field" => field, "struct" => struct&.to_s).compact
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
306
427
|
# Build-time: the query didn't validate against the schema. Carries the
|
|
307
428
|
# structured validation errors (message + line/column) rather than a
|
|
308
429
|
# joined string. Under the Error umbrella like everything else raised
|
|
309
430
|
# here (through 0.1.0 it was an ArgumentError instead).
|
|
310
431
|
class ValidationError < Error
|
|
432
|
+
extend T::Sig
|
|
433
|
+
|
|
434
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
311
435
|
attr_reader :errors
|
|
312
436
|
|
|
437
|
+
sig { params(errors: T::Array[T::Hash[Symbol, T.untyped]]).void }
|
|
313
438
|
def initialize(errors)
|
|
314
439
|
@errors = errors
|
|
315
440
|
super("invalid query: #{errors.map { |e| e[:message] }.join("; ")}")
|
|
316
441
|
end
|
|
317
442
|
|
|
443
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
|
318
444
|
def to_h
|
|
319
445
|
super.merge("errors" => errors.map { |e| e.transform_keys(&:to_s) })
|
|
320
446
|
end
|
data/lib/graph_weaver/hints.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
|
+
require_relative "errors"
|
|
6
7
|
require_relative "inflect"
|
|
7
8
|
|
|
8
9
|
module GraphWeaver
|
|
@@ -32,7 +33,11 @@ module GraphWeaver
|
|
|
32
33
|
end
|
|
33
34
|
suggestion ? "#{key} (did you mean '#{suggestion}'?)" : key
|
|
34
35
|
end
|
|
35
|
-
raise
|
|
36
|
+
raise GraphWeaver::InputError.new(
|
|
37
|
+
"unknown key(s) for #{struct}: #{hints.join(", ")}",
|
|
38
|
+
field: unknown.join(", "),
|
|
39
|
+
struct: struct,
|
|
40
|
+
)
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def method_missing(name, *args, &block)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
|
+
require_relative "errors"
|
|
6
7
|
require_relative "hints"
|
|
7
8
|
|
|
8
9
|
module GraphWeaver
|
|
@@ -53,6 +54,13 @@ module GraphWeaver
|
|
|
53
54
|
raw = value.key?(field.prop) ? value[field.prop] : value[field.prop.to_s]
|
|
54
55
|
[field.prop, raw.nil? || field.coercer.nil? ? raw : field.coercer.call(raw)]
|
|
55
56
|
end)
|
|
57
|
+
rescue GraphWeaver::InputError
|
|
58
|
+
raise # already contextualized by a nested input / enum coercion
|
|
59
|
+
rescue ::TypeError, ::ArgumentError, KeyError => e
|
|
60
|
+
# a wrong-typed field, a missing required field, or an out-of-range
|
|
61
|
+
# enum — surface one branded, structured error for a 422. (`::` so the
|
|
62
|
+
# rescue catches Ruby's TypeError, not GraphWeaver::TypeError.)
|
|
63
|
+
raise GraphWeaver::InputError.new("invalid input for #{self}: #{e.message}", struct: self)
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
66
|
end
|
|
@@ -19,10 +19,10 @@ module GraphWeaver
|
|
|
19
19
|
|
|
20
20
|
Data = type_member
|
|
21
21
|
|
|
22
|
-
sig { returns(T.nilable(Data)) }
|
|
22
|
+
sig { override.returns(T.nilable(Data)) }
|
|
23
23
|
attr_reader :data
|
|
24
24
|
|
|
25
|
-
sig { returns(T::Array[GraphWeaver::GraphQLError]) }
|
|
25
|
+
sig { override.returns(T::Array[GraphWeaver::GraphQLError]) }
|
|
26
26
|
attr_reader :errors
|
|
27
27
|
|
|
28
28
|
sig { returns(T::Hash[String, T.untyped]) }
|
data/lib/graph_weaver/version.rb
CHANGED