graph_weaver 0.0.1 → 0.2.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/.yardopts +6 -0
- data/CHANGELOG.md +279 -0
- data/Gemfile.lock +75 -17
- data/Makefile +11 -1
- data/NOTES.md +1 -1
- data/PLAN.md +85 -22
- data/README.md +86 -26
- data/docs/cassettes.md +76 -0
- data/docs/errors.md +134 -0
- data/docs/generated_modules.md +229 -0
- data/docs/getting_started.md +134 -0
- data/docs/logging.md +33 -0
- data/docs/real_world.md +53 -0
- data/docs/scalars.md +183 -0
- data/docs/testing.md +103 -0
- data/docs/transports.md +124 -0
- data/graph_weaver.gemspec +10 -1
- data/lib/graph_weaver/client.rb +200 -0
- data/lib/graph_weaver/codegen/emit.rb +286 -0
- data/lib/graph_weaver/codegen/enum_type.rb +149 -0
- data/lib/graph_weaver/codegen/nodes.rb +356 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +256 -0
- data/lib/graph_weaver/codegen.rb +396 -401
- data/lib/graph_weaver/errors.rb +322 -0
- data/lib/graph_weaver/hints.rb +63 -0
- data/lib/graph_weaver/inflect.rb +19 -0
- data/lib/graph_weaver/logging.rb +41 -0
- data/lib/graph_weaver/railtie.rb +29 -0
- data/lib/graph_weaver/response.rb +55 -0
- data/lib/graph_weaver/retry.rb +97 -0
- data/lib/graph_weaver/rspec.rb +59 -0
- data/lib/graph_weaver/schema_loader.rb +208 -8
- data/lib/graph_weaver/selection.rb +68 -0
- data/lib/graph_weaver/tasks.rb +71 -0
- data/lib/graph_weaver/testing/cassette.rb +224 -0
- data/lib/graph_weaver/testing/failure.rb +109 -0
- data/lib/graph_weaver/testing/fake_client.rb +228 -0
- data/lib/graph_weaver/testing/values.rb +98 -0
- data/lib/graph_weaver/testing.rb +106 -0
- 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 +268 -4
- metadata +132 -2
- data/lib/graph_weaver/http_executor.rb +0 -31
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Source emission: turns the node tree into the generated module text.
|
|
5
|
+
# Mixed into Codegen — methods run with the generator instance state.
|
|
6
|
+
class GraphWeaver::Codegen
|
|
7
|
+
module Emit
|
|
8
|
+
include GraphWeaver::Inflect
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def emit_nested(node, out, indent)
|
|
13
|
+
case node
|
|
14
|
+
when UnionNode then emit_union(node, out, indent)
|
|
15
|
+
when EnumNode then emit_enum(node, out, indent)
|
|
16
|
+
else emit_object(node, out, indent)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def emit_enum(node, out, indent)
|
|
21
|
+
pad = " " * indent
|
|
22
|
+
|
|
23
|
+
out << "#{pad}class #{node.class_name} < T::Enum"
|
|
24
|
+
out << "#{pad} enums do"
|
|
25
|
+
node.values.each do |value|
|
|
26
|
+
out << "#{pad} #{camelize(value.downcase)} = new(#{value.inspect})"
|
|
27
|
+
end
|
|
28
|
+
out << "#{pad} end"
|
|
29
|
+
out << "#{pad}end"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# module-level wire translation tables for an app-mapped enum
|
|
33
|
+
def emit_mapped_enum(node, out, indent)
|
|
34
|
+
pad = " " * indent
|
|
35
|
+
type = node.bare_type
|
|
36
|
+
prefix = node.const_prefix
|
|
37
|
+
|
|
38
|
+
out << "#{pad}# GraphQL enum #{node.graphql_name} <-> #{type} (registered mapping)"
|
|
39
|
+
out << "#{pad}#{prefix}_FROM_WIRE = T.let({"
|
|
40
|
+
node.mapping.each do |wire, member|
|
|
41
|
+
out << "#{pad} #{wire.inspect} => #{type}.deserialize(#{member.serialize.to_s.inspect}),"
|
|
42
|
+
end
|
|
43
|
+
out << "#{pad}}.freeze, T::Hash[String, #{type}])"
|
|
44
|
+
out << "#{pad}#{prefix}_TO_WIRE = T.let(#{prefix}_FROM_WIRE.invert.freeze, T::Hash[#{type}, String])"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def emit_object(node, out, indent)
|
|
48
|
+
pad = " " * indent
|
|
49
|
+
|
|
50
|
+
out << "#{pad}class #{node.class_name} < T::Struct"
|
|
51
|
+
out << "#{pad} extend T::Sig"
|
|
52
|
+
out << "#{pad} include GraphWeaver::Hints"
|
|
53
|
+
node.mixins.each do |mixin|
|
|
54
|
+
out << "#{pad} include #{mixin} # registered for #{node.graphql_type}"
|
|
55
|
+
end
|
|
56
|
+
out << ""
|
|
57
|
+
|
|
58
|
+
node.fields.filter_map { |field| field.node.nested }.each do |child|
|
|
59
|
+
emit_nested(child, out, indent + 1)
|
|
60
|
+
out << ""
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
node.fields.each do |field|
|
|
64
|
+
out << "#{pad} const :#{field.prop}, #{field.node.prop_type}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
out << ""
|
|
68
|
+
out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(#{node.class_name}) }"
|
|
69
|
+
out << "#{pad} def self.from_h(data)"
|
|
70
|
+
out << "#{pad} new("
|
|
71
|
+
node.fields.each do |field|
|
|
72
|
+
out << "#{pad} #{field.prop}: #{field_cast(field)},"
|
|
73
|
+
end
|
|
74
|
+
out << "#{pad} )"
|
|
75
|
+
out << "#{pad} rescue GraphWeaver::TypeError"
|
|
76
|
+
out << "#{pad} raise # already wrapped by a nested struct — keep the innermost context"
|
|
77
|
+
out << "#{pad} rescue TypeError, ArgumentError, KeyError => e"
|
|
78
|
+
out << "#{pad} raise GraphWeaver::TypeError.new(struct: self, error: e)"
|
|
79
|
+
out << "#{pad} end"
|
|
80
|
+
out << "#{pad}end"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def emit_union(node, out, indent)
|
|
84
|
+
pad = " " * indent
|
|
85
|
+
|
|
86
|
+
out << "#{pad}module #{node.class_name}"
|
|
87
|
+
out << "#{pad} extend T::Sig"
|
|
88
|
+
out << ""
|
|
89
|
+
|
|
90
|
+
node.members.each_value do |member|
|
|
91
|
+
emit_object(member, out, indent + 1)
|
|
92
|
+
out << ""
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
member_names = node.members.values.map(&:class_name)
|
|
96
|
+
type_alias = member_names.size == 1 ? member_names.first : "T.any(#{member_names.join(", ")})"
|
|
97
|
+
out << "#{pad} Type = T.type_alias { #{type_alias} }"
|
|
98
|
+
out << ""
|
|
99
|
+
out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(Type) }"
|
|
100
|
+
out << "#{pad} def self.from_h(data)"
|
|
101
|
+
out << "#{pad} case (typename = data.fetch(\"__typename\"))"
|
|
102
|
+
node.members.each do |graphql_name, member|
|
|
103
|
+
out << "#{pad} when #{graphql_name.inspect} then #{member.class_name}.from_h(data)"
|
|
104
|
+
end
|
|
105
|
+
out << "#{pad} else raise GraphWeaver::TypeError.new(struct: self, message: \"unexpected __typename: \#{typename}\")"
|
|
106
|
+
out << "#{pad} end"
|
|
107
|
+
out << "#{pad} end"
|
|
108
|
+
out << "#{pad}end"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def emit_execute(out, variables, flatten: nil)
|
|
112
|
+
out << " @client = T.let(nil, T.untyped)"
|
|
113
|
+
out << ""
|
|
114
|
+
out << " class << self"
|
|
115
|
+
out << " extend T::Sig"
|
|
116
|
+
out << ""
|
|
117
|
+
out << " sig { params(client: T.untyped).void }"
|
|
118
|
+
out << " attr_writer :client"
|
|
119
|
+
out << ""
|
|
120
|
+
out << " # default client (a GraphWeaver::Client or any transport) for"
|
|
121
|
+
out << " # execute: per-module override, else the app default"
|
|
122
|
+
out << " sig { returns(T.untyped) }"
|
|
123
|
+
out << " def client"
|
|
124
|
+
out << " @client || #{@client_const || "GraphWeaver.client!"}"
|
|
125
|
+
out << " end"
|
|
126
|
+
out << " end"
|
|
127
|
+
out << ""
|
|
128
|
+
|
|
129
|
+
# the kwarg surface: the input's fields when flattened, else one
|
|
130
|
+
# kwarg per declared variable — typed identically either way. The
|
|
131
|
+
# per-call client override rides as an optional POSITIONAL arg, so
|
|
132
|
+
# variables keep the entire kwarg namespace (nothing is reserved).
|
|
133
|
+
params = flatten ? flatten.fields.partition(&:required).flatten : variables
|
|
134
|
+
|
|
135
|
+
sig_params = ["client: T.untyped"]
|
|
136
|
+
sig_params += params.map do |param|
|
|
137
|
+
bare = param.node.coerce? ? param.node.coerce_input_type : param.node.bare_type
|
|
138
|
+
kwarg_type = param.required || bare == "T.untyped" ? bare : "T.nilable(#{bare})"
|
|
139
|
+
"#{kwarg_name(param)}: #{kwarg_type}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
kwargs = ["client = nil"]
|
|
143
|
+
kwargs += params.map { |param| param.required ? "#{kwarg_name(param)}:" : "#{kwarg_name(param)}: nil" }
|
|
144
|
+
|
|
145
|
+
# execute returns the full envelope; execute! is the strict shortcut for
|
|
146
|
+
# `execute(...).data!` — the typed result, or a raised QueryError.
|
|
147
|
+
forward = (["client"] + params.map { |param| "#{kwarg_name(param)}: #{kwarg_name(param)}" }).join(", ")
|
|
148
|
+
|
|
149
|
+
if flatten
|
|
150
|
+
out << " # $#{variables.first.wire}'s fields, flattened into kwargs (single input-object variable)"
|
|
151
|
+
end
|
|
152
|
+
out << " sig { params(#{sig_params.join(", ")}).returns(GraphWeaver::Response[Result]) }"
|
|
153
|
+
out << " def self.execute(#{kwargs.join(", ")})"
|
|
154
|
+
|
|
155
|
+
if flatten
|
|
156
|
+
fields = flatten.fields.map { |field| "#{field.prop}:" }.join(", ")
|
|
157
|
+
out << " variables = {"
|
|
158
|
+
out << " #{variables.first.wire.inspect} => #{flatten.class_name}.coerce({ #{fields} }).serialize,"
|
|
159
|
+
out << " }"
|
|
160
|
+
else
|
|
161
|
+
required, optional = variables.partition(&:required)
|
|
162
|
+
if required.empty?
|
|
163
|
+
out << " variables = {}"
|
|
164
|
+
else
|
|
165
|
+
out << " variables = {"
|
|
166
|
+
required.each do |var|
|
|
167
|
+
out << " #{var.wire.inspect} => #{variable_serialize(var)},"
|
|
168
|
+
end
|
|
169
|
+
out << " }"
|
|
170
|
+
end
|
|
171
|
+
optional.each do |var|
|
|
172
|
+
out << " variables[#{var.wire.inspect}] = #{variable_serialize(var)} unless #{var.kwarg}.nil?"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
out << ""
|
|
177
|
+
out << " transport = GraphWeaver.resolve_transport(client || self.client)"
|
|
178
|
+
out << " raw = transport.execute(QUERY, variables: variables).to_h"
|
|
179
|
+
out << " GraphWeaver::Response[Result].new("
|
|
180
|
+
out << " data: (Result.from_h(raw[\"data\"]) if raw[\"data\"]),"
|
|
181
|
+
out << " errors: (raw[\"errors\"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) },"
|
|
182
|
+
out << " extensions: raw[\"extensions\"] || {},"
|
|
183
|
+
out << " )"
|
|
184
|
+
out << " end"
|
|
185
|
+
out << ""
|
|
186
|
+
out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
|
|
187
|
+
out << " def self.execute!(#{kwargs.join(", ")})"
|
|
188
|
+
out << " execute(#{forward}).data!"
|
|
189
|
+
out << " end"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# a kwarg surface entry is a VarDef (.kwarg) or, when flattened, an
|
|
193
|
+
# InputNode::Field (.prop)
|
|
194
|
+
def kwarg_name(param)
|
|
195
|
+
param.respond_to?(:kwarg) ? param.kwarg : param.prop
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def variable_serialize(var)
|
|
199
|
+
value = var.node.coerce? ? var.node.coerce(var.kwarg) : var.kwarg
|
|
200
|
+
var.node.serialize_identity? ? value : var.node.serialize(value, 1)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def field_cast(field)
|
|
204
|
+
node = field.node
|
|
205
|
+
|
|
206
|
+
if node.non_null?
|
|
207
|
+
raw = "data.fetch(#{field.key.inspect})"
|
|
208
|
+
node.identity? ? raw : node.cast(raw, 1)
|
|
209
|
+
else
|
|
210
|
+
raw = "data[#{field.key.inspect}]"
|
|
211
|
+
node.identity? ? raw : "#{raw}&.then { |v1| #{node.cast("v1", 2)} }"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# a module-level T::Struct per input type; serialize builds the wire
|
|
216
|
+
# hash, omitting optional fields left nil
|
|
217
|
+
def emit_input(node, out, indent)
|
|
218
|
+
pad = " " * indent
|
|
219
|
+
|
|
220
|
+
out << "#{pad}class #{node.class_name} < T::Struct"
|
|
221
|
+
out << "#{pad} extend T::Sig"
|
|
222
|
+
out << ""
|
|
223
|
+
node.fields.each do |field|
|
|
224
|
+
default = field.required ? "" : ", default: nil"
|
|
225
|
+
out << "#{pad} const :#{field.prop}, #{field.node.prop_type}#{default}"
|
|
226
|
+
end
|
|
227
|
+
out << ""
|
|
228
|
+
# locals wear the reserved __gw prefix: prop readers are bare method
|
|
229
|
+
# calls here, and a prop named "result" or "value" would otherwise
|
|
230
|
+
# be shadowed — GraphQL reserves __-names, so no field can collide
|
|
231
|
+
out << "#{pad} sig { returns(T::Hash[String, T.untyped]) }"
|
|
232
|
+
out << "#{pad} def serialize"
|
|
233
|
+
out << "#{pad} __gw_result = T.let({}, T::Hash[String, T.untyped])"
|
|
234
|
+
node.fields.each do |field|
|
|
235
|
+
if field.required || field.node.serialize_identity?
|
|
236
|
+
value = field.node.serialize_identity? ? field.prop.to_s : field.node.serialize(field.prop.to_s, 1)
|
|
237
|
+
line = "__gw_result[#{field.wire.inspect}] = #{value}"
|
|
238
|
+
line += " unless #{field.prop}.nil?" unless field.required
|
|
239
|
+
out << "#{pad} #{line}"
|
|
240
|
+
else
|
|
241
|
+
# bind a local so sorbet's flow-sensitivity narrows the nilable
|
|
242
|
+
out << "#{pad} unless (__gw_value = #{field.prop}).nil?"
|
|
243
|
+
out << "#{pad} __gw_result[#{field.wire.inspect}] = #{field.node.serialize("__gw_value", 1)}"
|
|
244
|
+
out << "#{pad} end"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
out << "#{pad} __gw_result"
|
|
248
|
+
out << "#{pad} end"
|
|
249
|
+
out << ""
|
|
250
|
+
out << "#{pad} # serialize, under the conventional name"
|
|
251
|
+
out << "#{pad} sig { returns(T::Hash[String, T.untyped]) }"
|
|
252
|
+
out << "#{pad} def to_h = serialize"
|
|
253
|
+
out << ""
|
|
254
|
+
out << "#{pad} # Build from a plain hash (underscored keys, Symbol or String):"
|
|
255
|
+
out << "#{pad} # enums accept their wire values, nested inputs accept hashes;"
|
|
256
|
+
out << "#{pad} # the struct's types are enforced on construction."
|
|
257
|
+
out << "#{pad} sig { params(value: T.any(#{node.class_name}, T::Hash[T.untyped, T.untyped])).returns(#{node.class_name}) }"
|
|
258
|
+
out << "#{pad} def self.coerce(value)"
|
|
259
|
+
out << "#{pad} return value if value.is_a?(#{node.class_name})"
|
|
260
|
+
out << ""
|
|
261
|
+
out << "#{pad} # a typo'd key must not silently drop off the wire"
|
|
262
|
+
out << "#{pad} GraphWeaver::Hints.validate_keys!(self, value)"
|
|
263
|
+
out << ""
|
|
264
|
+
out << "#{pad} new("
|
|
265
|
+
node.fields.each do |field|
|
|
266
|
+
raw = "value_at(value, :#{field.prop})"
|
|
267
|
+
expr = if field.node.hash_coerce_identity?
|
|
268
|
+
raw
|
|
269
|
+
elsif field.required
|
|
270
|
+
"#{raw}.then { |v1| #{field.node.hash_coerce("v1", 2)} }"
|
|
271
|
+
else
|
|
272
|
+
"#{raw}&.then { |v1| #{field.node.hash_coerce("v1", 2)} }"
|
|
273
|
+
end
|
|
274
|
+
out << "#{pad} #{field.prop}: #{expr},"
|
|
275
|
+
end
|
|
276
|
+
out << "#{pad} )"
|
|
277
|
+
out << "#{pad} end"
|
|
278
|
+
out << ""
|
|
279
|
+
out << "#{pad} sig { params(hash: T::Hash[T.untyped, T.untyped], key: Symbol).returns(T.untyped) }"
|
|
280
|
+
out << "#{pad} private_class_method def self.value_at(hash, key)"
|
|
281
|
+
out << "#{pad} hash.key?(key) ? hash[key] : hash[key.to_s]"
|
|
282
|
+
out << "#{pad} end"
|
|
283
|
+
out << "#{pad}end"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
class GraphWeaver::Codegen
|
|
5
|
+
# How one GraphQL enum maps onto an app-owned T::Enum, so generated
|
|
6
|
+
# code speaks YOUR enum instead of generating one per module:
|
|
7
|
+
#
|
|
8
|
+
# class PetKind < T::Enum
|
|
9
|
+
# enums { Cat = new("cat"); Dog = new("dog") }
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# GraphWeaver.register_enum("Species", PetKind)
|
|
13
|
+
#
|
|
14
|
+
# The wire mapping is inferred by name ("CAT" <-> PetKind::Cat,
|
|
15
|
+
# case/underscore-insensitive against each member's serialized value);
|
|
16
|
+
# map: pins renames explicitly and merges over inference. Every wire
|
|
17
|
+
# value the schema declares must resolve — generation fails naming the
|
|
18
|
+
# gaps — unless fallback: names a member to absorb unknown values
|
|
19
|
+
# (forward-compat for servers that add members; inputs stay strict).
|
|
20
|
+
class EnumType
|
|
21
|
+
attr_reader :graphql_name, :type, :fallback, :requires
|
|
22
|
+
|
|
23
|
+
def initialize(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
24
|
+
@graphql_name = graphql_name.to_s
|
|
25
|
+
unless type.is_a?(Class) && type < T::Enum
|
|
26
|
+
raise ArgumentError, "type: must be a T::Enum subclass, got #{type.inspect}"
|
|
27
|
+
end
|
|
28
|
+
unless type.name
|
|
29
|
+
raise ArgumentError, "type: must be a named constant (anonymous classes can't appear in generated source)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@type = type
|
|
33
|
+
@map = map || {}
|
|
34
|
+
@fallback = fallback
|
|
35
|
+
@requires = Array(requires)
|
|
36
|
+
|
|
37
|
+
if fallback && !type.values.include?(fallback)
|
|
38
|
+
raise ArgumentError, "fallback: must be a #{type} member, got #{fallback.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# wire value => member for every value the schema declares; raises
|
|
43
|
+
# naming the unmappable ones (unless fallback: absorbs them)
|
|
44
|
+
def mapping_for(wire_values)
|
|
45
|
+
mapping = {}
|
|
46
|
+
missing = []
|
|
47
|
+
|
|
48
|
+
wire_values.each do |wire|
|
|
49
|
+
member = @map[wire] || infer(wire)
|
|
50
|
+
member ? mapping[wire] = member : missing << wire
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if missing.any? && !fallback
|
|
54
|
+
raise GraphWeaver::Error,
|
|
55
|
+
"#{type} has no member for #{graphql_name} value(s) #{missing.join(", ")} — " \
|
|
56
|
+
"add them, pin with map:, or absorb with fallback:"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
mapping
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# "CAT" matches serialize "cat"; "NOT_FOUND" matches "not_found"
|
|
65
|
+
def infer(wire)
|
|
66
|
+
@type.values.find { |member| normalize(member.serialize.to_s) == normalize(wire) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalize(value)
|
|
70
|
+
value.downcase.delete("_")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# Map a GraphQL enum onto an app-owned T::Enum (see EnumType); the
|
|
76
|
+
# global default — client.register_enum scopes to one client.
|
|
77
|
+
def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
78
|
+
enum_registry[graphql_name.to_s] = EnumType.new(graphql_name, type, map:, fallback:, requires:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Bulk, inference-only form: register_enums("Species" => PetKind, ...)
|
|
82
|
+
def register_enums(mappings)
|
|
83
|
+
mappings.each { |graphql_name, type| register_enum(graphql_name, type) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def enum_registry
|
|
87
|
+
@enum_registry ||= {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Attach app-owned helper modules to every struct generated from a
|
|
91
|
+
# GraphQL type — the logic stays in your code, generation wires it in:
|
|
92
|
+
#
|
|
93
|
+
# GraphWeaver.register_type("Pet", PetHelpers)
|
|
94
|
+
#
|
|
95
|
+
# Or build the mixin inline — the block is module_eval'd into a fresh
|
|
96
|
+
# module auto-named GraphWeaver::TypeHelpers::<Type>. Handy for quick
|
|
97
|
+
# decoration; srb tc can't see into block-defined methods, so prefer
|
|
98
|
+
# a named module where static checking matters:
|
|
99
|
+
#
|
|
100
|
+
# GraphWeaver.register_type("Pet") do
|
|
101
|
+
# def display_name = "#{name} the pet"
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# Additive: repeated registrations (and client-scoped ones) stack.
|
|
105
|
+
def register_type(graphql_name, *mixins, requires: nil, &block)
|
|
106
|
+
entry = type_registry[graphql_name.to_s] ||= { mixins: [], requires: [] }
|
|
107
|
+
add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def type_registry
|
|
111
|
+
@type_registry ||= {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# shared with Client#register_type: build/validate the mixins and
|
|
115
|
+
# append them to a registry entry
|
|
116
|
+
def add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
117
|
+
mixins = mixins.dup
|
|
118
|
+
mixins << helper_module(graphql_name, block) if block
|
|
119
|
+
|
|
120
|
+
raise ArgumentError, "pass one or more helper modules, or a block" if mixins.empty?
|
|
121
|
+
mixins.each do |mixin|
|
|
122
|
+
unless mixin.is_a?(Module) && mixin.name
|
|
123
|
+
raise ArgumentError, "type helpers must be named modules, got #{mixin.inspect}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
entry[:mixins].concat(mixins)
|
|
128
|
+
entry[:requires].concat(Array(requires))
|
|
129
|
+
entry
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# a block-built mixin needs a name generated files can reference:
|
|
133
|
+
# GraphWeaver::TypeHelpers::Pet (suffixed on re-registration)
|
|
134
|
+
def helper_module(graphql_name, block)
|
|
135
|
+
base = GraphWeaver::Inflect.camelize(graphql_name.to_s)
|
|
136
|
+
name = base
|
|
137
|
+
count = 1
|
|
138
|
+
name = "#{base}V#{count += 1}" while GraphWeaver::TypeHelpers.const_defined?(name, false)
|
|
139
|
+
GraphWeaver::TypeHelpers.const_set(name, Module.new(&block))
|
|
140
|
+
end
|
|
141
|
+
private :helper_module
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
module GraphWeaver
|
|
146
|
+
# Home of block-built type helpers (register_type with a block), which
|
|
147
|
+
# need constant names so generated files can reference them.
|
|
148
|
+
module TypeHelpers; end
|
|
149
|
+
end
|