graph_weaver 0.5.0 → 0.6.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 +537 -0
- data/Gemfile.lock +19 -19
- data/README.md +74 -53
- data/docs/cassettes.md +29 -4
- data/docs/editors.md +3 -1
- data/docs/errors.md +75 -16
- data/docs/federation.md +206 -155
- data/docs/generated_modules.md +223 -166
- data/docs/getting_started.md +106 -82
- data/docs/logging.md +35 -5
- data/docs/scalars.md +119 -24
- data/docs/testing.md +196 -155
- data/docs/transports.md +47 -19
- data/docs/upgrading.md +243 -22
- data/graph_weaver.gemspec +16 -2
- data/lib/generators/graph_weaver/install_generator.rb +31 -16
- data/lib/graph_weaver/client.rb +52 -15
- data/lib/graph_weaver/codegen/aliases.rb +15 -8
- data/lib/graph_weaver/codegen/emit.rb +107 -42
- data/lib/graph_weaver/codegen/enum_type.rb +4 -3
- data/lib/graph_weaver/codegen/nodes.rb +42 -21
- data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
- data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
- data/lib/graph_weaver/codegen.rb +382 -105
- data/lib/graph_weaver/coerce.rb +113 -0
- data/lib/graph_weaver/errors.rb +57 -13
- data/lib/graph_weaver/federation.rb +10 -22
- data/lib/graph_weaver/hints.rb +76 -2
- data/lib/graph_weaver/in_process.rb +11 -8
- data/lib/graph_weaver/inflect.rb +2 -0
- data/lib/graph_weaver/input_struct.rb +115 -12
- data/lib/graph_weaver/internal/overrides.rb +101 -0
- data/lib/graph_weaver/internal/planner.rb +868 -0
- data/lib/graph_weaver/internal/schemas.rb +50 -0
- data/lib/graph_weaver/internal/selection.rb +127 -0
- data/lib/graph_weaver/{testing → internal}/subgraphs.rb +45 -43
- data/lib/graph_weaver/internal/values.rb +181 -0
- data/lib/graph_weaver/internal.rb +206 -0
- data/lib/graph_weaver/logging.rb +108 -20
- data/lib/graph_weaver/parsing.rb +6 -13
- data/lib/graph_weaver/query_module.rb +2 -0
- data/lib/graph_weaver/railtie.rb +113 -14
- data/lib/graph_weaver/representation.rb +30 -2
- data/lib/graph_weaver/response.rb +15 -0
- data/lib/graph_weaver/retry.rb +54 -22
- data/lib/graph_weaver/rspec.rb +63 -18
- data/lib/graph_weaver/schema_diff.rb +293 -0
- data/lib/graph_weaver/schema_loader.rb +126 -35
- data/lib/graph_weaver/tasks.rb +88 -36
- data/lib/graph_weaver/testing/cassette.rb +131 -78
- data/lib/graph_weaver/testing/coverage.rb +11 -15
- data/lib/graph_weaver/testing/failure.rb +14 -8
- data/lib/graph_weaver/testing/fake_client.rb +253 -60
- data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
- data/lib/graph_weaver/testing/router.rb +147 -840
- data/lib/graph_weaver/testing.rb +40 -83
- data/lib/graph_weaver/transport/faraday.rb +1 -1
- data/lib/graph_weaver/transport/http.rb +29 -12
- data/lib/graph_weaver/transport.rb +11 -34
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +221 -118
- metadata +17 -13
- data/CLAUDE.md +0 -161
- data/DECISIONS.md +0 -309
- data/Makefile +0 -23
- data/NOTES.md +0 -182
- data/PLAN.md +0 -115
- data/REVIEW.md +0 -946
- data/lib/graph_weaver/schemas.rb +0 -46
- data/lib/graph_weaver/selection.rb +0 -120
- data/lib/graph_weaver/testing/values.rb +0 -98
|
@@ -34,29 +34,36 @@ class GraphWeaver::Codegen
|
|
|
34
34
|
class ScalarType
|
|
35
35
|
# Inferred (deserialize, serialize) codecs, tried in order; the first
|
|
36
36
|
# whose probe the Ruby type defines as a class method wins, and its
|
|
37
|
-
# serialize is paired with it. Builders take (type_name, expr) => code
|
|
38
|
-
|
|
37
|
+
# serialize is paired with it. Builders take (type_name, expr) => code;
|
|
38
|
+
# `call` is the same serialization run rather than emitted, for the
|
|
39
|
+
# testing harness (see #serialize_value).
|
|
40
|
+
Codec = Struct.new(:probe, :cast, :serialize, :call)
|
|
39
41
|
CODECS = [
|
|
40
42
|
Codec.new(:parse, # Type.parse(wire) <-> value.to_s
|
|
41
43
|
->(type, expr) { "#{type}.parse(#{expr})" },
|
|
42
|
-
->(_type, expr) { "#{expr}.to_s" }
|
|
44
|
+
->(_type, expr) { "#{expr}.to_s" },
|
|
45
|
+
->(_klass, value) { value.to_s }),
|
|
43
46
|
Codec.new(:load, # Type.load(wire) <-> Type.dump(value)
|
|
44
47
|
->(type, expr) { "#{type}.load(#{expr})" },
|
|
45
|
-
->(type, expr) { "#{type}.dump(#{expr})" }
|
|
48
|
+
->(type, expr) { "#{type}.dump(#{expr})" },
|
|
49
|
+
->(klass, value) { klass.dump(value) }),
|
|
46
50
|
].freeze
|
|
47
51
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"Float" =>
|
|
52
|
+
# A scalar with no `cast:` to run input through: its Ruby type is the
|
|
53
|
+
# whole rule, so key on that — a custom scalar registered as a plain
|
|
54
|
+
# String gets the same check. ID is the exception GraphQL itself names
|
|
55
|
+
# (see Coerce.id), matched by GraphQL name in #coercer.
|
|
56
|
+
COERCERS = {
|
|
57
|
+
"Integer" => "integer",
|
|
58
|
+
"Float" => "float",
|
|
59
|
+
"String" => "string",
|
|
60
|
+
"T::Boolean" => "boolean",
|
|
55
61
|
}.freeze
|
|
62
|
+
private_constant :Codec, :CODECS, :COERCERS
|
|
56
63
|
|
|
57
64
|
attr_reader :graphql_name, :type, :requires
|
|
58
65
|
|
|
59
|
-
def initialize(graphql_name, type, cast: nil, serialize: nil, requires: nil
|
|
66
|
+
def initialize(graphql_name, type, cast: nil, serialize: nil, requires: nil)
|
|
60
67
|
@graphql_name = graphql_name.to_s
|
|
61
68
|
@klass = type.is_a?(Module) ? type : nil
|
|
62
69
|
@type = type_name(type)
|
|
@@ -67,52 +74,47 @@ class GraphWeaver::Codegen
|
|
|
67
74
|
codec = @klass && CODECS.find { |c| @klass.respond_to?(c.probe) }
|
|
68
75
|
@cast = normalize_cast(cast, codec&.cast)
|
|
69
76
|
@serialize = normalize_serialize(serialize, codec&.serialize)
|
|
70
|
-
@
|
|
71
|
-
validate_coerce!
|
|
77
|
+
@serialize_value = runtime_serialize(serialize, codec)
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
def cast(expr) = @cast&.call(expr)
|
|
75
81
|
def cast? = !@cast.nil?
|
|
76
82
|
def serialize(expr) = @serialize&.call(expr)
|
|
77
83
|
def serialize? = !@serialize.nil?
|
|
78
|
-
def coerce? =
|
|
84
|
+
def coerce? = !coerce_input("v").nil?
|
|
79
85
|
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def coercion
|
|
87
|
-
return if @coerce == false
|
|
88
|
-
return if @coerce.nil? && !GraphWeaver.auto_coerce
|
|
86
|
+
# #serialize run rather than emitted: the wire value for a Ruby one. The
|
|
87
|
+
# testing harness reads app objects — a Time, a Money — off an object pin
|
|
88
|
+
# and has to write what the server would. A `serialize:` proc builds code
|
|
89
|
+
# and can't be run, so its value passes through and the cast complains.
|
|
90
|
+
def serialize_value(value)
|
|
91
|
+
return value if value.nil? || @serialize_value.nil?
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
@serialize_value.call(value)
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
# The code that normalizes a
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
96
|
+
# The code that normalizes a loose input — a Rails param — into this
|
|
97
|
+
# scalar's Ruby type before it is serialized, or nil for nothing to do.
|
|
98
|
+
# `cast:` is the how: it already knows how to build the Ruby object from
|
|
99
|
+
# a wire value, guarded so an already-typed value passes through. A
|
|
100
|
+
# scalar without one falls back to its Ruby type's check, which is what
|
|
101
|
+
# `.checked(:never)` on the generated sig gives up.
|
|
99
102
|
def coerce_input(expr)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# the accepted Sorbet type for a coercible variable kwarg
|
|
107
|
-
def coerce_type
|
|
108
|
-
case (how = coercion)
|
|
109
|
-
when :parse then "T.any(#{@type}, String)"
|
|
110
|
-
when Conversion then how.input_type
|
|
103
|
+
if cast?
|
|
104
|
+
"(#{expr}.is_a?(#{@type}) ? #{expr} : #{cast(expr)})"
|
|
105
|
+
elsif (fn = coercer)
|
|
106
|
+
"GraphWeaver::Coerce.#{fn}(#{expr})"
|
|
111
107
|
end
|
|
112
108
|
end
|
|
113
109
|
|
|
114
110
|
private
|
|
115
111
|
|
|
112
|
+
def coercer
|
|
113
|
+
return "id" if @graphql_name == "ID" && @type == "String"
|
|
114
|
+
|
|
115
|
+
COERCERS[@type]
|
|
116
|
+
end
|
|
117
|
+
|
|
116
118
|
def type_name(type)
|
|
117
119
|
case type
|
|
118
120
|
when Module
|
|
@@ -148,33 +150,23 @@ class GraphWeaver::Codegen
|
|
|
148
150
|
end
|
|
149
151
|
end
|
|
150
152
|
|
|
153
|
+
# The runnable half of normalize_serialize: a Symbol names a method
|
|
154
|
+
# (:itself included, which is identity either way), an inferred codec
|
|
155
|
+
# knows its own call, and a Proc emits code there is no way to run.
|
|
156
|
+
def runtime_serialize(serialize, codec)
|
|
157
|
+
case serialize
|
|
158
|
+
when Symbol then ->(value) { value.public_send(serialize) }
|
|
159
|
+
when nil then codec && ->(value) { codec.call.call(@klass, value) }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
151
163
|
# With only a type-name string we can't assume the lib is installed at
|
|
152
164
|
# codegen time, so the paths aren't loaded — only shape-checked.
|
|
153
165
|
def normalize_requires(requires)
|
|
154
166
|
GraphWeaver::Codegen.normalize_requires!(requires, load: !@klass.nil?)
|
|
155
167
|
end
|
|
156
|
-
|
|
157
|
-
# coerce: true asks for something the scalar has to know how to do, so
|
|
158
|
-
# refuse a pass-through one now rather than emit a silent no-op.
|
|
159
|
-
def validate_coerce!
|
|
160
|
-
case @coerce
|
|
161
|
-
when false, nil then nil
|
|
162
|
-
when true
|
|
163
|
-
return if coercion
|
|
164
|
-
|
|
165
|
-
raise ArgumentError,
|
|
166
|
-
"coerce: true needs a cast and a serialize (#{@graphql_name} has neither, " \
|
|
167
|
-
"so there is nothing to coerce)"
|
|
168
|
-
else
|
|
169
|
-
raise ArgumentError, "coerce: must be true or false, got #{@coerce.inspect}"
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
168
|
end
|
|
173
169
|
|
|
174
|
-
# Pre-registered rather than user intent (see register_builtin_scalars!), so
|
|
175
|
-
# generation doesn't hold a schema to them.
|
|
176
|
-
BUILTIN_SCALARS = %w[ID String Int Float Boolean Date].freeze
|
|
177
|
-
|
|
178
170
|
class << self
|
|
179
171
|
# requires: is a require path or list of them; each must be a non-empty
|
|
180
172
|
# String (it is emitted verbatim as `require "..."` atop the generated
|
|
@@ -204,18 +196,19 @@ class GraphWeaver::Codegen
|
|
|
204
196
|
# the accepted cast:/serialize:/requires: forms. Later registrations
|
|
205
197
|
# win, so an app can override a built-in (e.g. map Date onto its own
|
|
206
198
|
# type).
|
|
207
|
-
def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil
|
|
199
|
+
def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil)
|
|
208
200
|
scalar_registry[graphql_name.to_s] =
|
|
209
|
-
ScalarType.new(graphql_name, type, cast:, serialize:, requires
|
|
201
|
+
ScalarType.new(graphql_name, type, cast:, serialize:, requires:)
|
|
210
202
|
end
|
|
211
203
|
|
|
212
|
-
# The ScalarType for a scalar
|
|
213
|
-
#
|
|
214
|
-
# scalars
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
204
|
+
# The ScalarType in play for a scalar, most specific first: the
|
|
205
|
+
# `Type.field` registration when `coordinate` names one, then the
|
|
206
|
+
# scalar-name registration. Unknown scalars fall back to an untyped
|
|
207
|
+
# pass-through (T.untyped, no cast) — the prior behavior for scalars
|
|
208
|
+
# outside the table.
|
|
209
|
+
def scalar(graphql_name, coordinate = nil)
|
|
210
|
+
(coordinate && scalar_registry[coordinate.to_s]) ||
|
|
211
|
+
scalar_registry.fetch(graphql_name.to_s) { ScalarType.new(graphql_name, "T.untyped") }
|
|
219
212
|
end
|
|
220
213
|
|
|
221
214
|
def scalar_registry
|
|
@@ -230,32 +223,43 @@ class GraphWeaver::Codegen
|
|
|
230
223
|
end
|
|
231
224
|
|
|
232
225
|
# Drop every custom registration and restore the built-in scalars — the
|
|
233
|
-
# clean slate to reach for between tests, or to undo overrides.
|
|
234
|
-
# the built-ins to coerce loose input? That's GraphWeaver.auto_coerce,
|
|
235
|
-
# resolved at generation time — no re-registering.)
|
|
226
|
+
# clean slate to reach for between tests, or to undo overrides.
|
|
236
227
|
def reset_scalars!
|
|
237
228
|
clear_scalars!
|
|
238
229
|
register_builtin_scalars!
|
|
239
230
|
self
|
|
240
231
|
end
|
|
241
232
|
|
|
242
|
-
# Built-in scalars — pre-registered entries in the one registry.
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
233
|
+
# Built-in scalars — pre-registered entries in the one registry. Most stay
|
|
234
|
+
# pass-through: their Ruby classes (String, Integer) define neither .parse
|
|
235
|
+
# nor .load, so codec inference matches nothing and leaves them identity —
|
|
236
|
+
# which is exactly why we can name them with the real class constants.
|
|
237
|
+
# Float is the exception: JSON has one number type, so a whole Float
|
|
238
|
+
# arrives as `1` from every encoder that drops the trailing zero
|
|
239
|
+
# (graphql-js and Go both do), and Coerce.float widens that without
|
|
240
|
+
# accepting the garbage `.to_f` would silently turn into 0.0. Date
|
|
241
|
+
# deserializes via ISO-8601 (it *does* define .parse, but we want iso8601
|
|
242
|
+
# specifically, so it's explicit). The rest carry no cast and coerce
|
|
243
|
+
# input by their Ruby type — see coerce_input.
|
|
250
244
|
def register_builtin_scalars!
|
|
251
245
|
register_scalar "ID", String
|
|
252
246
|
register_scalar "String", String
|
|
253
247
|
register_scalar "Int", Integer
|
|
254
|
-
register_scalar "Float", Float
|
|
248
|
+
register_scalar "Float", Float, cast: ->(expr) { "GraphWeaver::Coerce.float(#{expr})" }
|
|
255
249
|
register_scalar "Boolean", "T::Boolean"
|
|
256
250
|
register_scalar "Date", Date, cast: :iso8601, serialize: :iso8601, requires: "date"
|
|
257
251
|
end
|
|
252
|
+
private :register_builtin_scalars!
|
|
258
253
|
end
|
|
259
254
|
|
|
255
|
+
# codegen's own record of a registration; users get one back from
|
|
256
|
+
# `.scalar` but never name the class
|
|
257
|
+
private_constant :ScalarType
|
|
258
|
+
|
|
260
259
|
register_builtin_scalars!
|
|
260
|
+
|
|
261
|
+
# Pre-registered rather than user intent, so generation doesn't hold a schema
|
|
262
|
+
# to them (validate_registration! skips these). Read off the registry the line
|
|
263
|
+
# above just filled: a seventh built-in shouldn't have to be named twice.
|
|
264
|
+
BUILTIN_SCALARS = scalar_registry.keys.freeze
|
|
261
265
|
end
|
|
@@ -54,9 +54,7 @@ class GraphWeaver::Codegen
|
|
|
54
54
|
# won't resolve yet — see EnumType for why that's a Rails initializer
|
|
55
55
|
if mixin.is_a?(String)
|
|
56
56
|
raise ArgumentError, "type helpers are the modules themselves, not their names — " \
|
|
57
|
-
"extend_type(#{graphql_name.to_s.inspect}, #{mixin}).
|
|
58
|
-
"resolvable while config/initializers run; register from a " \
|
|
59
|
-
"Rails.application.config.to_prepare block, which generation also runs first."
|
|
57
|
+
"extend_type(#{graphql_name.to_s.inspect}, #{mixin}). #{GraphWeaver::Codegen::AUTOLOAD_HINT}"
|
|
60
58
|
end
|
|
61
59
|
unless mixin.is_a?(Module) && mixin.name
|
|
62
60
|
raise ArgumentError, "type helpers must be named modules, got #{mixin.inspect}"
|
|
@@ -84,6 +82,7 @@ class GraphWeaver::Codegen
|
|
|
84
82
|
# arbitrary text that could inject code
|
|
85
83
|
ALIAS_NAME = /\A[a-zA-Z_]\w*[?!]?\z/
|
|
86
84
|
ALIAS_SEGMENT = /\A[a-zA-Z_]\w*\z/
|
|
85
|
+
private_constant :ALIAS_NAME, :ALIAS_SEGMENT
|
|
87
86
|
|
|
88
87
|
# { accessor => { segments:, optional: } } from a path string (accessor
|
|
89
88
|
# named after the last segment), an array of such, or an { accessor => path }
|