graph_weaver 0.6.1 → 0.7.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/Gemfile +8 -0
- data/Gemfile.lock +153 -4
- data/README.md +45 -79
- data/docs/alternatives.md +195 -0
- data/docs/cassettes.md +61 -50
- data/docs/editors.md +32 -47
- data/docs/errors.md +360 -103
- data/docs/federation.md +692 -473
- data/docs/generated_modules.md +441 -314
- data/docs/getting_started.md +370 -194
- data/docs/i18n.md +171 -0
- data/docs/logging.md +197 -50
- data/docs/real_world.md +42 -27
- data/docs/scalars.md +307 -176
- data/docs/testing.md +473 -220
- data/docs/transports.md +224 -151
- data/docs/upgrading.md +258 -305
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +19 -3
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +69 -11
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +406 -195
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +284 -46
- data/lib/graph_weaver/federation.rb +129 -27
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +27 -15
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +80 -0
- data/lib/graph_weaver/internal/headers.rb +70 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +249 -14
- data/lib/graph_weaver/log_subscriber.rb +74 -0
- data/lib/graph_weaver/logging.rb +163 -19
- data/lib/graph_weaver/query_module.rb +44 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +45 -13
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +266 -56
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +34 -10
- data/lib/graph_weaver/testing/endpoint.rb +107 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +164 -45
- data/lib/graph_weaver/testing/router.rb +64 -13
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +48 -6
- data/lib/graph_weaver/transport.rb +134 -27
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +495 -106
- metadata +71 -3
- data/CHANGELOG.md +0 -2355
|
@@ -52,15 +52,17 @@ class GraphWeaver::Codegen
|
|
|
52
52
|
->(klass, value) { klass.dump(value) }),
|
|
53
53
|
].freeze
|
|
54
54
|
|
|
55
|
-
# A scalar
|
|
56
|
-
# whole rule, so key on that — a custom scalar registered as a
|
|
57
|
-
# String gets the same check. ID is the exception GraphQL itself
|
|
58
|
-
# (see Coerce.id), matched by GraphQL name in #coercer.
|
|
55
|
+
# A scalar whose registration named no `cast:` of its own: its Ruby type
|
|
56
|
+
# is the whole rule, so key on that — a custom scalar registered as a
|
|
57
|
+
# plain String gets the same check. ID is the exception GraphQL itself
|
|
58
|
+
# names (see Coerce.id), matched by GraphQL name in #coercer.
|
|
59
59
|
COERCERS = {
|
|
60
60
|
"Integer" => "integer",
|
|
61
61
|
"Float" => "float",
|
|
62
62
|
"String" => "string",
|
|
63
63
|
"T::Boolean" => "boolean",
|
|
64
|
+
"Date" => "date",
|
|
65
|
+
"Time" => "time",
|
|
64
66
|
}.freeze
|
|
65
67
|
|
|
66
68
|
# What the library already knows about a Ruby type, so registering one
|
|
@@ -72,11 +74,25 @@ class GraphWeaver::Codegen
|
|
|
72
74
|
# more than the ISO 8601 a Date scalar carries.
|
|
73
75
|
# - the file to require, so the generated source stands alone.
|
|
74
76
|
# Only types whose wire form is unambiguous belong here.
|
|
77
|
+
#
|
|
78
|
+
# `call:` is the runnable twin of a Proc `serialize:`, which builds source
|
|
79
|
+
# and so can't be run — the testing harness needs both (see
|
|
80
|
+
# #serialize_value).
|
|
81
|
+
TIMESTAMP = ->(expr) { "GraphWeaver::Coerce.timestamp(#{expr})" }
|
|
82
|
+
TIMESTAMP_CALL = ->(value) { GraphWeaver::Coerce.timestamp(value) }
|
|
83
|
+
|
|
75
84
|
STDLIB = {
|
|
76
85
|
"BigDecimal" => { serialize: [:to_s, "F"], requires: "bigdecimal" },
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
86
|
+
# JSON has one number type, so a whole Float arrives as `1` from every
|
|
87
|
+
# encoder that drops the trailing zero (graphql-js and Go both do)
|
|
88
|
+
"Float" => { cast: ->(expr) { "GraphWeaver::Coerce.float(#{expr})" } },
|
|
89
|
+
# strftime, not #iso8601: DateTime < Date passes the is_a? guard, and its
|
|
90
|
+
# #iso8601 writes a timestamp where the schema said a date goes
|
|
91
|
+
"Date" => { cast: :iso8601, serialize: [:strftime, "%F"], requires: "date" },
|
|
92
|
+
# #iso8601 takes no precision, so it writes whole seconds and a
|
|
93
|
+
# sub-second timestamp goes back out poorer than it came in
|
|
94
|
+
"Time" => { cast: :parse, serialize: TIMESTAMP, call: TIMESTAMP_CALL, requires: "time" },
|
|
95
|
+
"DateTime" => { cast: :iso8601, serialize: TIMESTAMP, call: TIMESTAMP_CALL, requires: "date" },
|
|
80
96
|
}.freeze
|
|
81
97
|
|
|
82
98
|
# Everything JSON.parse can hand back. A registered type outside this set
|
|
@@ -104,10 +120,13 @@ class GraphWeaver::Codegen
|
|
|
104
120
|
else
|
|
105
121
|
GraphWeaver::Codegen.normalize_requires!(requires, load: !@klass.nil?)
|
|
106
122
|
end
|
|
123
|
+
@cast_given = cast unless cast == :itself
|
|
107
124
|
codec = @klass && CODECS.find { |c| @klass.respond_to?(c.probe) }
|
|
108
125
|
@cast = normalize_cast(cast || known[:cast], codec&.cast || kernel_cast)
|
|
109
126
|
@serialize = normalize_serialize(serialize || known[:serialize], codec&.serialize)
|
|
110
|
-
@serialize_value =
|
|
127
|
+
@serialize_value = (known[:call] if serialize.nil?) ||
|
|
128
|
+
runtime_serialize(serialize || known[:serialize], codec)
|
|
129
|
+
warn_half_a_value_object
|
|
111
130
|
end
|
|
112
131
|
|
|
113
132
|
def cast(expr) = @cast&.call(expr)
|
|
@@ -128,21 +147,74 @@ class GraphWeaver::Codegen
|
|
|
128
147
|
|
|
129
148
|
# The code that normalizes a loose input — a Rails param — into this
|
|
130
149
|
# scalar's Ruby type before it is serialized, or nil for nothing to do.
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
150
|
+
# The Ruby type's own rule in Coerce is the check, which is what
|
|
151
|
+
# `.checked(:never)` on the generated sig gives up. A registration that
|
|
152
|
+
# named its own `cast:` says how to build the Ruby object instead — but
|
|
153
|
+
# the guard in front of it and the verdict when it refuses are still the
|
|
154
|
+
# library's, so both go through Coerce.cast rather than a bare `is_a?`
|
|
155
|
+
# (a DateTime is one of those, and is not a Date on any wire).
|
|
156
|
+
# The schema's name for the scalar travels with the value, so a refusal
|
|
157
|
+
# reports GraphQL vocabulary rather than the Ruby type it maps to —
|
|
158
|
+
# register_scalar("Money", BigDecimal) refuses a Money, not a BigDecimal.
|
|
135
159
|
def coerce_input(expr)
|
|
136
|
-
if
|
|
137
|
-
"
|
|
138
|
-
elsif
|
|
139
|
-
"GraphWeaver::Coerce
|
|
160
|
+
if (fn = coercer)
|
|
161
|
+
"GraphWeaver::Coerce.#{fn}(#{expr}, #{@graphql_name.inspect})"
|
|
162
|
+
elsif cast?
|
|
163
|
+
"GraphWeaver::Coerce.cast(#{@type}, #{expr}, #{@graphql_name.inspect}) { |raw| #{cast("raw")} }"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Why this registration can't read the JSON at `where`, and what to do about
|
|
168
|
+
# it. Two different mistakes land here, so say which: a type: given by name
|
|
169
|
+
# was never probed (there is no class in hand to probe), while a class was
|
|
170
|
+
# probed and matched nothing. Raised by Codegen#refuse_uncastable!, which
|
|
171
|
+
# owns the question of whether the wire could satisfy the prop at all.
|
|
172
|
+
def uncastable_message(where)
|
|
173
|
+
article = GraphWeaver::Internal::Util.article(@type)
|
|
174
|
+
head = "register_scalar(#{@graphql_name.inspect}, #{@klass ? @type : @type.inspect}) has no " \
|
|
175
|
+
"cast, so nothing builds #{article} #{@type} out of the JSON at #{where}"
|
|
176
|
+
if @klass.nil?
|
|
177
|
+
"#{head} — a type: given by name is never probed, since there is no class in hand. Pass " \
|
|
178
|
+
"the class (register_scalar(#{@graphql_name.inspect}, #{@type})) to infer a cast from " \
|
|
179
|
+
"it, or name one yourself (cast: :parse names a class method, " \
|
|
180
|
+
"cast: ->(v) { \"#{@type}.parse(\#{v})\" } emits any expression)"
|
|
181
|
+
else
|
|
182
|
+
"#{head} — #{@type} defines no .parse and no .load, and Kernel has no #{@type} conversion " \
|
|
183
|
+
"function, so there was nothing to infer. Give it a cast (cast: :parse names a class " \
|
|
184
|
+
"method, cast: ->(v) { \"#{@type}.new(\#{v})\" } emits any expression), or register a " \
|
|
185
|
+
"type the wire already parses into"
|
|
140
186
|
end
|
|
141
187
|
end
|
|
142
188
|
|
|
143
189
|
private
|
|
144
190
|
|
|
191
|
+
# A result compares its props with eql?, so that it and #hash agree on what
|
|
192
|
+
# "same" means. A type that leaves eql? at Object's compares by identity,
|
|
193
|
+
# so two results parsed from the same bytes are unequal and useless as hash
|
|
194
|
+
# keys — whether it defined == (the common Ruby idiom, and the leaf itself
|
|
195
|
+
# then compares fine) or no equality at all. Nothing here can fix that;
|
|
196
|
+
# only the type can.
|
|
197
|
+
def warn_half_a_value_object
|
|
198
|
+
# a Module type names a duck the gem never sees an instance of; a T::Enum's
|
|
199
|
+
# values are singletons, so identity already is equality
|
|
200
|
+
return unless @klass.is_a?(Class) && !(@klass < T::Enum) && !defines?(:eql?)
|
|
201
|
+
|
|
202
|
+
GraphWeaver::Internal::Log.log(:warn) do
|
|
203
|
+
"register_scalar(#{@graphql_name.inspect}, #{@type}): #{@type} inherits #eql? and #hash, " \
|
|
204
|
+
"so its instances compare by identity — two results parsed from the same response won't " \
|
|
205
|
+
"be equal and a result won't work as a hash key — define ==, eql? and hash off the same " \
|
|
206
|
+
"values (alias_method :eql?, :== is the usual shortcut)"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def defines?(method)
|
|
211
|
+
![BasicObject, Kernel, Object].include?(@klass.instance_method(method).owner)
|
|
212
|
+
end
|
|
213
|
+
|
|
145
214
|
def coercer
|
|
215
|
+
# a cast: the registration named is the whole rule (:itself asks for
|
|
216
|
+
# no rich object, which is not the same as asking for no check)
|
|
217
|
+
return unless @cast_given.nil?
|
|
146
218
|
return "id" if @graphql_name == "ID" && @type == "String"
|
|
147
219
|
|
|
148
220
|
COERCERS[@type]
|
|
@@ -170,13 +242,30 @@ class GraphWeaver::Codegen
|
|
|
170
242
|
->(type, expr) { "#{type}(#{expr})" }
|
|
171
243
|
end
|
|
172
244
|
|
|
245
|
+
# A Proc here builds SOURCE for the generated file, so a proc that
|
|
246
|
+
# converts a value (`->(v) { v.to_sym }`) can't work — it interpolates to
|
|
247
|
+
# nothing and every response fails far from the registration. Probe it
|
|
248
|
+
# once now, where the message can name the spelling.
|
|
249
|
+
def source_builder!(option, proc)
|
|
250
|
+
probe = proc.arity.zero? ? proc.call : proc.call("v")
|
|
251
|
+
return proc if probe.is_a?(String)
|
|
252
|
+
|
|
253
|
+
raise ArgumentError, "#{option}: a Proc must return the Ruby source to emit — " \
|
|
254
|
+
"e.g. #{option}: ->(v) { \"#{@type}.parse(\#{v})\" } — got #{probe.inspect}; " \
|
|
255
|
+
"a Symbol names a method instead (#{option}: :parse)"
|
|
256
|
+
rescue ArgumentError => e
|
|
257
|
+
raise if e.message.start_with?("#{option}:")
|
|
258
|
+
|
|
259
|
+
raise ArgumentError, "#{option}: a Proc takes one argument, the expression to wrap — #{e.message}"
|
|
260
|
+
end
|
|
261
|
+
|
|
173
262
|
# nil infers via the matched codec; :itself opts out (identity); a
|
|
174
263
|
# Symbol is a class method on the type — Money.parse(expr)
|
|
175
264
|
def normalize_cast(cast, inferred)
|
|
176
265
|
case cast
|
|
177
266
|
when :itself then nil
|
|
178
267
|
when nil then inferred && ->(expr) { inferred.call(@type, expr) }
|
|
179
|
-
when Proc then cast
|
|
268
|
+
when Proc then source_builder!(:cast, cast)
|
|
180
269
|
when Symbol then ->(expr) { "#{@type}.#{cast}(#{expr})" }
|
|
181
270
|
else raise ArgumentError, "cast: must be a Symbol, Proc, :itself, or nil, got #{cast.inspect}"
|
|
182
271
|
end
|
|
@@ -189,7 +278,7 @@ class GraphWeaver::Codegen
|
|
|
189
278
|
case serialize
|
|
190
279
|
when :itself then nil
|
|
191
280
|
when nil then inferred && ->(expr) { inferred.call(@type, expr) }
|
|
192
|
-
when Proc then serialize
|
|
281
|
+
when Proc then source_builder!(:serialize, serialize)
|
|
193
282
|
when Symbol then ->(expr) { "#{expr}.#{serialize}" }
|
|
194
283
|
when Array
|
|
195
284
|
method, *args = serialize
|
|
@@ -238,7 +327,10 @@ class GraphWeaver::Codegen
|
|
|
238
327
|
end
|
|
239
328
|
end
|
|
240
329
|
end
|
|
330
|
+
end
|
|
241
331
|
|
|
332
|
+
# The scalar half of one graph's registrations — see Codegen::Registry.
|
|
333
|
+
class Registry
|
|
242
334
|
# Register (or override) how a GraphQL custom scalar deserializes into
|
|
243
335
|
# a Ruby object and serializes back onto the wire. See ScalarType for
|
|
244
336
|
# the accepted cast:/serialize:/requires: forms. Later registrations
|
|
@@ -284,10 +376,9 @@ class GraphWeaver::Codegen
|
|
|
284
376
|
# The five the spec names stay pass-through: their Ruby classes (String,
|
|
285
377
|
# Integer) define neither .parse nor .load, so inference matches nothing
|
|
286
378
|
# and leaves them identity — which is exactly why we can name them with
|
|
287
|
-
# the real class constants. Float is the exception
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
# without accepting the garbage `.to_f` would silently turn into 0.0.
|
|
379
|
+
# the real class constants. Float is the exception, and its rule lives in
|
|
380
|
+
# STDLIB with the others, so `register_scalar "Ratio", Float` reads the
|
|
381
|
+
# wire exactly as the built-in Float does.
|
|
291
382
|
#
|
|
292
383
|
# The rest are names, not guesses: graphql-ruby ships all but DateTime as
|
|
293
384
|
# its own scalars, and this library runs a graphql-ruby schema in-process.
|
|
@@ -300,7 +391,7 @@ class GraphWeaver::Codegen
|
|
|
300
391
|
register_scalar "ID", String
|
|
301
392
|
register_scalar "String", String
|
|
302
393
|
register_scalar "Int", Integer
|
|
303
|
-
register_scalar "Float", Float
|
|
394
|
+
register_scalar "Float", Float
|
|
304
395
|
register_scalar "Boolean", "T::Boolean"
|
|
305
396
|
register_scalar "Date", Date
|
|
306
397
|
register_scalar "ISO8601Date", Date
|
|
@@ -314,18 +405,20 @@ class GraphWeaver::Codegen
|
|
|
314
405
|
# untyped on purpose: registering it says so, rather than leaving JSON
|
|
315
406
|
# in the "unregistered custom scalars" report every generation
|
|
316
407
|
register_scalar "JSON", "T.untyped"
|
|
408
|
+
# the objects, not the names: a later register_scalar("DateTime", ...)
|
|
409
|
+
# replaces the entry, and that is app intent rather than a pre-registration
|
|
410
|
+
@builtin_entries = scalar_registry.values.freeze
|
|
317
411
|
end
|
|
318
412
|
private :register_builtin_scalars!
|
|
413
|
+
|
|
414
|
+
# Whether this name still holds the entry pre-registration put there.
|
|
415
|
+
def builtin_scalar?(name)
|
|
416
|
+
entry = scalar_registry[name]
|
|
417
|
+
@builtin_entries.any? { |builtin| builtin.equal?(entry) }
|
|
418
|
+
end
|
|
319
419
|
end
|
|
320
420
|
|
|
321
421
|
# codegen's own record of a registration; users get one back from
|
|
322
422
|
# `.scalar` but never name the class
|
|
323
423
|
private_constant :ScalarType
|
|
324
|
-
|
|
325
|
-
register_builtin_scalars!
|
|
326
|
-
|
|
327
|
-
# Pre-registered rather than user intent, so generation doesn't hold a schema
|
|
328
|
-
# to them (validate_registration! skips these). Read off the registry the line
|
|
329
|
-
# above just filled: a seventh built-in shouldn't have to be named twice.
|
|
330
|
-
BUILTIN_SCALARS = scalar_registry.keys.freeze
|
|
331
424
|
end
|
|
@@ -11,16 +11,18 @@
|
|
|
11
11
|
# path is resolved against an actual selection).
|
|
12
12
|
|
|
13
13
|
class GraphWeaver::Codegen
|
|
14
|
-
|
|
14
|
+
# The extend_type half of one graph's registrations — see Codegen::Registry.
|
|
15
|
+
class Registry
|
|
15
16
|
# Attach app-owned helper modules to every struct generated from a
|
|
16
17
|
# GraphQL type — the logic stays in your code, generation wires it in:
|
|
17
18
|
#
|
|
18
19
|
# GraphWeaver.extend_type("Pet", PetHelpers)
|
|
19
20
|
#
|
|
20
21
|
# Or build the mixin inline — the block is module_eval'd into a fresh
|
|
21
|
-
# module
|
|
22
|
-
#
|
|
23
|
-
#
|
|
22
|
+
# module named for where it is written and what it extends:
|
|
23
|
+
# GraphWeaver::TypeHelpers::Pet, or ::Billing::Pet in graph :billing.
|
|
24
|
+
# Handy for quick decoration; srb tc can't see into block-defined methods,
|
|
25
|
+
# so prefer a named module where static checking matters:
|
|
24
26
|
#
|
|
25
27
|
# GraphWeaver.extend_type("Pet") do
|
|
26
28
|
# def display_name = "#{name} the pet"
|
|
@@ -118,19 +120,62 @@ class GraphWeaver::Codegen
|
|
|
118
120
|
# generated files may still name them.
|
|
119
121
|
def reset_type_helpers!
|
|
120
122
|
type_registry.clear
|
|
123
|
+
helper_counts.clear
|
|
121
124
|
self
|
|
122
125
|
end
|
|
123
126
|
|
|
124
|
-
#
|
|
125
|
-
#
|
|
127
|
+
# Which graph's registrations this registry holds — nil for the top-level
|
|
128
|
+
# one. Block-built helpers are named for it, so two graphs extending the
|
|
129
|
+
# same type get two constants (see helper_module).
|
|
130
|
+
attr_accessor :graph_name
|
|
131
|
+
|
|
132
|
+
# A block-built mixin needs a name generated files can reference:
|
|
133
|
+
# GraphWeaver::TypeHelpers::Pet, or ::Billing::Pet in graph :billing (V2,
|
|
134
|
+
# V3… for a second and third block on the same type in the same place).
|
|
135
|
+
#
|
|
136
|
+
# The name is a function of the source and nothing else — where the block
|
|
137
|
+
# is written and what it extends. It gets baked into generated code, so
|
|
138
|
+
# naming it after whichever constants happened to exist made it a function
|
|
139
|
+
# of how many times THIS process had read the registry, and `generate`
|
|
140
|
+
# wrote a name a plain boot never creates.
|
|
126
141
|
def helper_module(graphql_name, block)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
name = "#{
|
|
131
|
-
|
|
142
|
+
namespace = helper_namespace
|
|
143
|
+
type = GraphWeaver::Inflect.camelize(graphql_name.to_s)
|
|
144
|
+
index = (helper_counts[[namespace.name, type]] += 1)
|
|
145
|
+
name = index == 1 ? type : "#{type}V#{index}"
|
|
146
|
+
# reused rather than replaced, so re-declaring the same source (a Rails
|
|
147
|
+
# to_prepare reload) keeps the module already-loaded structs include
|
|
148
|
+
mod = const_under(namespace, name) { Module.new }
|
|
149
|
+
mod.module_eval(&block)
|
|
150
|
+
mod
|
|
132
151
|
end
|
|
133
152
|
private :helper_module
|
|
153
|
+
|
|
154
|
+
# Where this registry's block-built helpers live: under a module named for
|
|
155
|
+
# the graph, so two graphs extending the same type get two constants and
|
|
156
|
+
# neither has to know the other exists.
|
|
157
|
+
def helper_namespace
|
|
158
|
+
return GraphWeaver::TypeHelpers unless graph_name
|
|
159
|
+
|
|
160
|
+
const_under(GraphWeaver::TypeHelpers, GraphWeaver::Inflect.camelize(graph_name.to_s)) { Module.new }
|
|
161
|
+
end
|
|
162
|
+
private :helper_namespace
|
|
163
|
+
|
|
164
|
+
def const_under(namespace, name)
|
|
165
|
+
return namespace.const_get(name, false) if namespace.const_defined?(name, false)
|
|
166
|
+
|
|
167
|
+
namespace.const_set(name, yield)
|
|
168
|
+
end
|
|
169
|
+
private :const_under
|
|
170
|
+
|
|
171
|
+
# How many block-built helpers this registry has already named for a type.
|
|
172
|
+
# Per registry, not per process: a graph's registrations are replayed over
|
|
173
|
+
# a fresh copy of the top-level registry on every read, so the same source
|
|
174
|
+
# counts the same way every time.
|
|
175
|
+
def helper_counts
|
|
176
|
+
@helper_counts ||= Hash.new(0)
|
|
177
|
+
end
|
|
178
|
+
private :helper_counts
|
|
134
179
|
end
|
|
135
180
|
end
|
|
136
181
|
|