graph_weaver 0.7.1 → 0.7.3

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.
@@ -7,16 +7,25 @@
7
7
  # rake_tasks block, so graph_weaver:* tasks appear with no Rakefile
8
8
  # edit. (Outside Rails there is no task-discovery hook — add
9
9
  # `require "graph_weaver/tasks"` to your Rakefile.)
10
- # - generated modules: required at boot once every registration has run —
11
- # both the initializer kind and the to_prepare kind, since a generated
12
- # file `include`s the type helper it was generated with and that
13
- # constant must resolve. load_generated! stays idempotent, so calling
14
- # it yourself too is harmless.
10
+ # - generated modules: required once every registration has run — both the
11
+ # initializer kind and the to_prepare kind, since a generated file
12
+ # `include`s the type helper it was generated with and that constant must
13
+ # resolve. load_generated! stays idempotent, so calling it yourself too is
14
+ # harmless.
15
15
  # - Zeitwerk: the generated directory is hidden from it, since the
16
16
  # default one lives under app/ and its files define top-level
17
17
  # constants.
18
18
  # - watch mode: in development, editing a .graphql regenerates before the
19
19
  # next request, the way editing a route or a locale takes effect.
20
+ #
21
+ # All of that is hung off Rails' lifecycle HOOKS — before_initialize,
22
+ # after_initialize, the reloader — rather than off initializers with
23
+ # `after:`/`before:` edges naming other railties' initializers. Rails
24
+ # topologically sorts every railtie's initializers together, so such an edge
25
+ # constrains the whole app's boot order and tsort is free to satisfy it by
26
+ # moving initializers that aren't ours: a real app failed to boot with
27
+ # graph_weaver in the Gemfile and every one of these bodies neutered. A hook
28
+ # has a fixed place in boot and adds no edge. See DECISIONS.md.
20
29
  class GraphWeaver::Railtie < Rails::Railtie
21
30
  # config.graph_weaver.watch — false to never regenerate during a request.
22
31
  # Default: development only. Every other setting is a top-level one, and an
@@ -62,94 +71,87 @@ class GraphWeaver::Railtie < Rails::Railtie
62
71
  config.graph_weaver = Options.new
63
72
 
64
73
  class << self
65
- # The file watcher, so the to_prepare block below can ask it whether a
66
- # query changed. nil when not watching.
74
+ # The file watcher, so the prepare hook below can ask it whether a query
75
+ # changed. nil when not watching.
67
76
  attr_accessor :watcher
68
77
 
69
- # What ignore_generated actually hid, resolved. Zeitwerk only reads its
78
+ # What has been hidden from Zeitwerk, resolved. Zeitwerk only reads its
70
79
  # ignore list at setup, so anything that arrives later isn't hidden by
71
80
  # calling ignore again — check_generated_ignored! refuses instead.
72
81
  attr_accessor :ignored_dirs
82
+
83
+ # Whether hiding an output right now would actually hide it: false until
84
+ # the initializer below has swept (it picks up anything declared so far),
85
+ # and false again from the moment Zeitwerk sets the main autoloader up.
86
+ attr_accessor :hiding_outputs
73
87
  end
74
88
 
75
89
  rake_tasks do
76
90
  require "graph_weaver/tasks"
77
91
  end
78
92
 
79
- # The two auto-wires an app gets for free, and the only two it can turn off
80
- # by assigning nil. They are declared FIRST, and `before:
81
- # :load_config_initializers`, and both of those are load-bearing:
93
+ # ONE initializer, and it declares no `after:` see the note on the class.
94
+ # The `before:` that is left is inert: it only says "not after Zeitwerk's
95
+ # setup", and this is emitted at its own place in the railties block anyway,
96
+ # so nothing moves. It records the deadline.
82
97
  #
83
- # Rails gives an initializer an implicit `after:` of the previous one in the
84
- # same railtie (Initializable#initializer: `opts[:after] ||=
85
- # initializers.last&.name`). Declared after ignore_generated, which is
86
- # `after: :load_config_initializers`, these two inherited that position —
87
- # they ran AFTER config/initializers, so the `if nil?` fallback overwrote an
88
- # app that had just said nil and the documented PII opt-out did nothing,
89
- # silently, while queries and variables kept reaching a debug Rails.logger.
90
- # Adding the `before:` without moving them is a TSort::Cyclic at boot, since
91
- # the implicit `after:` would then point through ignore_generated.
92
- #
93
- # Rails.logger, unless the app already chose one (set
94
- # GraphWeaver.logger = nil in an initializer to silence)
95
- initializer "graph_weaver.logger", before: :load_config_initializers do
96
- GraphWeaver.logger = Rails.logger if GraphWeaver.logger.nil?
98
+ # What it sweeps is what config/application.rb configured. A graph declared
99
+ # in config/initializers is later than this and hides its own output as it is
100
+ # declared (ignore_output!) which is where that knowledge arrives, and so
101
+ # needs no edge to wait for.
102
+ initializer "graph_weaver.ignore_generated", before: :setup_main_autoloader do
103
+ GraphWeaver::Railtie.hide_generated!
97
104
  end
98
105
 
99
- # An APM sees every GraphQL call without the app configuring anything:
100
- # the ActiveSupport::Notifications adapter from docs/logging.md, plus the
101
- # LogSubscriber that turns its event into one line. Measured at ~4.5µs per
102
- # execution all told (0.13µs of that ActiveSupport::Notifications itself
103
- # with nothing subscribed; the rest is its Event machinery) — 0.05% of a
104
- # 10ms round trip, so there is nothing to weigh.
105
- #
106
- # An instrumenter the app set is never replaced: assigned before this (in
107
- # config/application.rb) the nil check leaves it, and config/initializers now
108
- # genuinely runs later, so one assigned there wins on its own — including
109
- # `GraphWeaver.instrumenter = nil` to opt out.
110
- initializer "graph_weaver.instrumentation", before: :load_config_initializers do
111
- next unless defined?(ActiveSupport::Notifications)
112
-
113
- if GraphWeaver.instrumenter.nil?
114
- GraphWeaver.instrumenter = lambda do |event, payload, &block|
115
- ActiveSupport::Notifications.instrument(event, payload, &block)
116
- end
106
+ # generated/person_query.rb defines ::PersonQuery, but Zeitwerk infers
107
+ # Generated::PersonQuery from the path and app/graphql/generated is inside
108
+ # an autoload root by default, so eager loading raised "uninitialized
109
+ # constant Generated::PersonQuery" in production while development (lazy) was
110
+ # fine. prepare_generated! requires them instead.
111
+ def self.hide_generated!
112
+ self.ignored_dirs = []
113
+ self.hiding_outputs = true
114
+ GraphWeaver::Internal::Util.generated_dirs.each { |path| ignore_output!(path) }
115
+
116
+ # Zeitwerk reads its ignore list once, at setup, and nothing in Rails
117
+ # announces that moment — Zeitwerk itself does. With no such signal, shut
118
+ # the window here: an ignore registered after setup hides nothing, and
119
+ # check_generated_ignored! has to be the one to say so.
120
+ main = Rails.autoloaders.main if Rails.autoloaders.respond_to?(:main)
121
+ if main.respond_to?(:on_setup)
122
+ main.on_setup { GraphWeaver::Railtie.hiding_outputs = false }
123
+ else
124
+ self.hiding_outputs = false
117
125
  end
118
-
119
- # ActiveSupport::LogSubscriber is one of ActiveSupport's own eager
120
- # autoloads, so naming it is enough — no require of theirs needed
121
- require "graph_weaver/log_subscriber"
122
- # idempotent — Subscriber.add_event_subscriber skips a pattern it already has
123
- GraphWeaver::LogSubscriber.attach_to :graph_weaver
124
126
  end
125
127
 
126
- # generated/person_query.rb defines ::PersonQuery, but Zeitwerk infers
127
- # Generated::PersonQuery from the path and app/graphql/generated is
128
- # inside an autoload root by default, so eager loading raised
129
- # "uninitialized constant Generated::PersonQuery" in production while
130
- # development (lazy) was fine. load_generated! below requires them.
128
+ # Hide one output from Zeitwerk, the moment it is named — a graph declared in
129
+ # config/initializers says where it writes long after the sweep above ran.
130
+ # Waiting for it instead is what `after: :load_config_initializers` used to
131
+ # buy, at the cost of the whole app's boot order.
131
132
  #
132
- # after: :load_config_initializers as well as before Zeitwerk's setup a
133
- # graph's output: is only known once the app has declared its graphs, and
134
- # with only the `before:` constraint this ran ~20 initializers too early, so
135
- # an output outside the conventional glob was eager loaded on top of
136
- # load_generated! and died on a redefined enum.
137
- initializer "graph_weaver.ignore_generated",
138
- after: :load_config_initializers, before: :setup_main_autoloader do
139
- # patterns, not paths generated_paths may be globs, and Zeitwerk
140
- # expands its own at setup (which is what this runs before)
141
- dirs = GraphWeaver::Internal::Util.generated_dirs.map { GraphWeaver::Railtie.autoload_path(_1) }
142
- GraphWeaver::Railtie.check_autoload_once!(dirs)
143
- GraphWeaver::Railtie.ignored_dirs = dirs
144
- Rails.autoloaders.each { |loader| dirs.each { |path| loader.ignore(path) } }
133
+ # Outside the window this is a no-op on purpose: before it, the sweep will
134
+ # pick the path up; after it, calling Zeitwerk's `ignore` would only teach
135
+ # check_generated_ignored! a lie.
136
+ def self.ignore_output!(path)
137
+ return unless hiding_outputs
138
+
139
+ # patterns, not paths — generated_paths may be globs, and Zeitwerk expands
140
+ # its own at setup, which is what the window stays open until
141
+ dir = autoload_path(path)
142
+ return if ignored_dirs.include?(dir)
143
+
144
+ check_autoload_once!([dir])
145
+ ignored_dirs << dir
146
+ Rails.autoloaders.each { |loader| loader.ignore(dir) }
145
147
  end
146
148
 
147
- # The `once` autoloader is set up in bootstrap, and Zeitwerk reads its ignore
148
- # list only at setup so the `loader.ignore` above, which runs after
149
- # config/initializers, hides nothing from it however the output is spelled.
150
- # The generic advice ("name it in GraphWeaver.generated_paths from
151
- # config/initializers") produced byte-identical output for this one, so it
152
- # gets its own refusal, naming the place that is still early enough.
149
+ # The `once` autoloader is set up in bootstrap, before any of this — and
150
+ # Zeitwerk reads its ignore list only at setup, so `loader.ignore` hides
151
+ # nothing from it however the output is spelled. The generic advice ("name it
152
+ # in GraphWeaver.generated_paths from config/initializers") produced
153
+ # byte-identical output for this one, so it gets its own refusal, naming the
154
+ # place that is still early enough.
153
155
  def self.check_autoload_once!(dirs)
154
156
  return unless Rails.respond_to?(:autoloaders) && Rails.autoloaders.respond_to?(:once)
155
157
 
@@ -232,13 +234,64 @@ class GraphWeaver::Railtie < Rails::Railtie
232
234
  end
233
235
  end
234
236
 
237
+ # The two auto-wires an app gets for free, and the only two it can turn off
238
+ # by assigning nil.
239
+ #
240
+ # before_initialize runs in Rails' own :bootstrap_hook — after
241
+ # :initialize_logger and before the first railtie initializer, so the default
242
+ # is in place for anything that boots, and config/initializers still runs
243
+ # later and still wins. That timing used to be a `before:
244
+ # :load_config_initializers` edge, and getting it wrong shipped: declared
245
+ # without one, these ran AFTER config/initializers, so the `if nil?` fallback
246
+ # overwrote an app that had just said nil — the documented PII opt-out did
247
+ # nothing, silently, while queries and variables kept reaching a debug
248
+ # Rails.logger.
249
+ config.before_initialize { GraphWeaver::Railtie.default_logger! }
250
+
251
+ # Rails.logger, unless the app already chose one (set GraphWeaver.logger =
252
+ # nil in an initializer to silence)
253
+ def self.default_logger!
254
+ GraphWeaver.logger = Rails.logger if GraphWeaver.logger.nil?
255
+ end
256
+
257
+ # An APM sees every GraphQL call without the app configuring anything:
258
+ # the ActiveSupport::Notifications adapter from docs/logging.md, plus the
259
+ # LogSubscriber that turns its event into one line. Measured at ~4.5µs per
260
+ # execution all told (0.13µs of that ActiveSupport::Notifications itself
261
+ # with nothing subscribed; the rest is its Event machinery) — 0.05% of a
262
+ # 10ms round trip, so there is nothing to weigh.
263
+ #
264
+ # An instrumenter the app set is never replaced: assigned before this (in
265
+ # config/application.rb) the nil check leaves it, and config/initializers runs
266
+ # later, so one assigned there wins on its own — including
267
+ # `GraphWeaver.instrumenter = nil` to opt out.
268
+ config.before_initialize { GraphWeaver::Railtie.default_instrumenter! }
269
+
270
+ def self.default_instrumenter!
271
+ return unless defined?(ActiveSupport::Notifications)
272
+
273
+ if GraphWeaver.instrumenter.nil?
274
+ GraphWeaver.instrumenter = lambda do |event, payload, &block|
275
+ ActiveSupport::Notifications.instrument(event, payload, &block)
276
+ end
277
+ end
278
+
279
+ # ActiveSupport::LogSubscriber is one of ActiveSupport's own eager
280
+ # autoloads, so naming it is enough — no require of theirs needed
281
+ require "graph_weaver/log_subscriber"
282
+ # idempotent — Subscriber.add_event_subscriber skips a pattern it already has
283
+ GraphWeaver::LogSubscriber.attach_to :graph_weaver
284
+ end
285
+
235
286
  # The app already declared what is sensitive, so variables logged at debug
236
287
  # honour the same list as its request logs — including the Procs and dotted
237
- # paths only ParameterFilter understands. after: :load_config_initializers,
238
- # since filter_parameter_logging.rb is where an app adds to it.
239
- initializer "graph_weaver.filter_parameters", after: :load_config_initializers do |app|
288
+ # paths only ParameterFilter understands. after_initialize, since
289
+ # filter_parameter_logging.rb is where an app adds to it.
290
+ config.after_initialize { |app| GraphWeaver::Railtie.adopt_filter_parameters!(app) }
291
+
292
+ def self.adopt_filter_parameters!(app)
240
293
  filters = app.config.filter_parameters
241
- next if filters.empty? || GraphWeaver.filter_parameters != GraphWeaver::DEFAULT_FILTER_PARAMETERS
294
+ return if filters.empty? || GraphWeaver.filter_parameters != GraphWeaver::DEFAULT_FILTER_PARAMETERS
242
295
 
243
296
  GraphWeaver.filter_parameters = ActiveSupport::ParameterFilter.new(filters)
244
297
  end
@@ -246,41 +299,31 @@ class GraphWeaver::Railtie < Rails::Railtie
246
299
  # Watch mode. A .graphql edit should reach the next request the way a route
247
300
  # or a locale change does, so the query directories and the schema dump
248
301
  # become one of Rails' own reloaders: a change there alone triggers a reload
249
- # cycle, and the to_prepare below regenerates before it loads. Off with
302
+ # cycle, and the prepare hook below regenerates before it loads. Off with
250
303
  #
251
304
  # config.graph_weaver.watch = false
252
305
  #
253
- # after: :load_config_initializers that's where an app moves
254
- # queries_paths, and the finisher that reads app.reloaders runs later still.
255
- #
256
- # to_prepare, not the initializer itself: a graph declared from one of those
257
- # (what the docs say to do when its block names an autoloaded constant) isn't
258
- # declared until every initializer has run, and a watcher built before it
259
- # watched the default queries_paths — an edit to that graph's .graphql
260
- # silently never regenerated. to_prepare blocks run in registration order and
261
- # an app registers its own during :load_config_initializers, which this is
262
- # `after:`, so every graph is in by the time this runs; app.reloaders is read
263
- # per request, so joining it this late still counts. Once, though: a dev
264
- # reload re-runs to_prepare, and a second watcher is a second reloader over
265
- # the same files.
266
- initializer "graph_weaver.watch", after: :load_config_initializers do |app|
267
- watched = false
268
- app.config.to_prepare do
269
- next if watched
270
-
271
- watched = true
272
- GraphWeaver::Railtie.watch!(app)
273
- end
274
- end
306
+ # after_initialize, because every graph has to be declared before the watcher
307
+ # is built: a graph declared from a to_prepare block (what the docs say to do
308
+ # when its block names an autoloaded constant) isn't declared until the
309
+ # prepare callbacks have run, and a watcher built before it watched the
310
+ # default queries_paths an edit to that graph's .graphql silently never
311
+ # regenerated. app.reloaders is read per request, so joining it this late
312
+ # still counts.
313
+ config.after_initialize { |app| GraphWeaver::Railtie.watch!(app) }
275
314
 
276
315
  # Registers the watcher, and says so: this is the one thing GraphWeaver does
277
316
  # that writes a checked-in file outside a rake task. Returns it, or nil when
278
317
  # nothing is being watched.
279
318
  def self.watch!(app)
319
+ # exactly one watcher per app, however many times this is called — a second
320
+ # one is a second reloader polling the same files
321
+ app.reloaders.delete(watcher) if watcher
322
+
280
323
  watch = app.config.graph_weaver.watch
281
324
  watch = Rails.env.development? if watch.nil?
282
- # with reloading off nothing re-runs to_prepare, so a watcher could only
283
- # promise something it can't do
325
+ # with reloading off nothing re-runs the prepare hook, so a watcher could
326
+ # only promise something it can't do
284
327
  return self.watcher = nil unless watch && app.config.reloading_enabled?
285
328
 
286
329
  # a directory that doesn't exist yet is still watched — FileUpdateChecker
@@ -321,51 +364,63 @@ class GraphWeaver::Railtie < Rails::Railtie
321
364
  end
322
365
 
323
366
  # A generated file `include`s the type helper it was generated with, so it
324
- # can't load until that constant resolves — and both Zeitwerk's setup and
325
- # the app's own to_prepare blocks (where extend_type/register_enum are told
326
- # to register, Codegen::AUTOLOAD_HINT) happen after config/initializers.
327
- # to_prepare, not `after:` a finisher initializer: naming one there makes
328
- # tsort hoist it ahead of the app's own config/initializers. Re-running on
329
- # each dev reload is free require is idempotent and picks up a module
330
- # generated since boot.
331
- initializer "graph_weaver.load_generated", after: :load_config_initializers do |app|
332
- app.config.to_prepare do
333
- # The graph_weaver tasks write these files and need none of them loaded.
334
- # Loading them would let a stale one block its own repair: a dropped
335
- # extend_type leaves a dangling include, and generate depends on
336
- # :environment, so boot failed before the task that would regenerate it.
337
- next if GraphWeaver.skip_generated_load
338
-
339
- # the app's own to_prepare blocks have run by now, so this is the first
340
- # point that sees every graph and the last before one of them loads
341
- GraphWeaver::Railtie.check_generated_ignored!
342
-
343
- # Regenerate first, then load — and here rather than in the watcher's own
344
- # to_run, so an extend_type or register_enum the app registers in its own
345
- # to_prepare is already in place (that block was registered at
346
- # :load_config_initializers, so it has run by now). A run that
347
- # regenerated has already reloaded what it wrote.
348
- next if GraphWeaver::Railtie.watcher&.execute_if_updated
349
-
350
- # entries may be globs, so Dir[] rather than Dir.exist?
351
- generated = GraphWeaver::Internal::Util.generated_dirs.any? do |dir|
352
- Dir[GraphWeaver::Internal::Util.resolve(dir)].any?
353
- end
354
- next unless generated
355
-
356
- # `require` no-ops on a file it has already read — which is what we want,
357
- # except when the constant that file defined is gone. A graph's
358
- # `namespace:` is normally a module Zeitwerk owns (app/graphql/accounts/
359
- # implies Accounts), and unloading it on a dev reload takes the generated
360
- # module nested inside it with it; require then restores nothing and every
361
- # request 500s on "uninitialized constant Accounts::PersonQuery" until a
362
- # .graphql edit happens to trigger the watcher. An un-namespaced module
363
- # defines a top-level constant Zeitwerk never manages, so it survives.
364
- if GraphWeaver.graphs.any?(&:namespace)
365
- GraphWeaver.reload_generated!
366
- else
367
- GraphWeaver.load_generated!
368
- end
367
+ # can't load until that constant resolves — and both Zeitwerk's setup and the
368
+ # app's own to_prepare blocks (where extend_type/register_enum are told to
369
+ # register, Codegen::AUTOLOAD_HINT) happen after config/initializers.
370
+ # after_initialize is past all of them, and past the watcher above.
371
+ #
372
+ # Except where the app eager loads: an eager-loaded class may name a
373
+ # generated constant in its class body, and Zeitwerk can't resolve one (the
374
+ # directory is ignored, by design), so the modules have to be in place before
375
+ # Rails' :eager_load! rather than after it. before_eager_load is that point,
376
+ # and Rails only runs it when config.eager_load is on which is exactly when
377
+ # after_initialize must not do the work a second time.
378
+ config.before_eager_load { GraphWeaver::Railtie.prepare_generated! }
379
+
380
+ config.after_initialize do |app|
381
+ GraphWeaver::Railtie.prepare_generated! unless app.config.eager_load
382
+ # and again on every dev reload, which picks up a module generated since
383
+ # boot and re-requires one whose namespace Zeitwerk just unloaded. On the
384
+ # reloader itself, not config.to_prepare: the :add_to_prepare_blocks
385
+ # finisher has already drained that list by now.
386
+ ActiveSupport::Reloader.to_prepare { GraphWeaver::Railtie.prepare_generated! }
387
+ end
388
+
389
+ def self.prepare_generated!
390
+ # The graph_weaver tasks write these files and need none of them loaded.
391
+ # Loading them would let a stale one block its own repair: a dropped
392
+ # extend_type leaves a dangling include, and generate depends on
393
+ # :environment, so boot failed before the task that would regenerate it.
394
+ return if GraphWeaver.skip_generated_load
395
+
396
+ # every graph is declared by now, and this is the last point before one of
397
+ # them loads
398
+ check_generated_ignored!
399
+
400
+ # Regenerate first, then load and here rather than in the watcher's own
401
+ # to_run, so an extend_type or register_enum the app registers in its own
402
+ # to_prepare is already in place. A run that regenerated has already
403
+ # reloaded what it wrote.
404
+ return if watcher&.execute_if_updated
405
+
406
+ # entries may be globs, so Dir[] rather than Dir.exist?
407
+ generated = GraphWeaver::Internal::Util.generated_dirs.any? do |dir|
408
+ Dir[GraphWeaver::Internal::Util.resolve(dir)].any?
409
+ end
410
+ return unless generated
411
+
412
+ # `require` no-ops on a file it has already read — which is what we want,
413
+ # except when the constant that file defined is gone. A graph's
414
+ # `namespace:` is normally a module Zeitwerk owns (app/graphql/accounts/
415
+ # implies Accounts), and unloading it on a dev reload takes the generated
416
+ # module nested inside it with it; require then restores nothing and every
417
+ # request 500s on "uninitialized constant Accounts::PersonQuery" until a
418
+ # .graphql edit happens to trigger the watcher. An un-namespaced module
419
+ # defines a top-level constant Zeitwerk never manages, so it survives.
420
+ if GraphWeaver.graphs.any?(&:namespace)
421
+ GraphWeaver.reload_generated!
422
+ else
423
+ GraphWeaver.load_generated!
369
424
  end
370
425
  end
371
426
  end
@@ -255,13 +255,12 @@ module GraphWeaver
255
255
  def self.unserve!(stub) = WebMock::StubRegistry.instance.request_stubs.delete(stub)
256
256
 
257
257
  # Every endpoint an example's modules can post to, each with the graph
258
- # whose resolvers belong behind it: the client each graph bakes into its
259
- # modules, or GraphWeaver.client for a graph baking none. One graph per
260
- # endpoint — an app whose graphs all bake clients needs no app default
261
- # at all.
258
+ # whose resolvers belong behind it: the client each graph names, or
259
+ # GraphWeaver.client for a graph naming none. One graph per endpoint —
260
+ # an app whose graphs all name clients needs no app default at all.
262
261
  def self.wire_targets
263
262
  targets = GraphWeaver.graphs.filter_map do |graph|
264
- client = baked_client(graph) || GraphWeaver.client
263
+ client = graph.client || GraphWeaver.client
265
264
  [endpoint!(client, graph), graph] if client
266
265
  end
267
266
  refuse_shared_endpoint!(targets)
@@ -283,24 +282,10 @@ module GraphWeaver
283
282
  raise GraphWeaver::Error, "#{TAG}: :wire serves one schema at each endpoint, and graphs " \
284
283
  "#{names} post to the same one (#{url}) — whichever were served there would answer the " \
285
284
  "others' queries, as fields its schema doesn't define. Give each graph a client of its " \
286
- "own (client: in the graph block), or tag the example #{TAG}: :in_process or " \
285
+ "own (`client` in the graph block), or tag the example #{TAG}: :in_process or " \
287
286
  "#{TAG}: :router, which run above the wire."
288
287
  end
289
288
 
290
- # The client a graph's generated modules call. `client:` holds a
291
- # constant or its name — codegen writes it into source — so a name is
292
- # resolved here the way the generated DEFAULT_CLIENT lambda resolves it.
293
- def self.baked_client(graph)
294
- named = graph.client
295
- return named unless named.is_a?(String)
296
-
297
- Object.const_get(named)
298
- rescue NameError
299
- raise GraphWeaver::Error, "#{TAG}: graph #{graph.name.inspect} bakes client: " \
300
- "#{named.inspect} into its modules and nothing defines that constant, so :wire can't " \
301
- "find the endpoint they post to."
302
- end
303
-
304
289
  # The endpoint a client posts to: a transport, a Retry around one, or a
305
290
  # Client that built one. `graph` says whose client it is, when it isn't
306
291
  # the app's own.
@@ -316,12 +301,16 @@ module GraphWeaver
316
301
  "#{TAG}: :in_process or #{TAG}: :router — they run above the wire."
317
302
  end
318
303
 
319
- # which client posts to nothing — the app's, or one graph's
304
+ # which client posts to nothing — the app's, or one graph's. A schema
305
+ # class in a client slot is named by ITS name: `client.class` is the
306
+ # word "Class", which names nothing anyone wrote.
320
307
  def self.whose_client(client, graph)
321
308
  return "GraphWeaver.client isn't set" unless client
322
- return "GraphWeaver.client is #{client.class}, which posts to none" unless graph&.name
323
309
 
324
- "graph #{graph.name.inspect} bakes client: #{client.class}, which posts to none"
310
+ named = client.is_a?(Module) ? client : client.class
311
+ return "GraphWeaver.client is #{named}, which posts to none" unless graph&.name
312
+
313
+ "graph #{graph.name.inspect} names client #{named}, which posts to none"
325
314
  end
326
315
 
327
316
  def self.webmock!
@@ -353,7 +342,7 @@ module GraphWeaver
353
342
  !WebMock::HttpLibAdapters::NetHttpAdapter::OriginalNetHTTP.equal?(Net::HTTP)
354
343
  end
355
344
 
356
- private_class_method :wire_targets, :refuse_shared_endpoint!, :baked_client, :whose_client,
345
+ private_class_method :wire_targets, :refuse_shared_endpoint!, :whose_client,
357
346
  :webmock!, :webmock_enabled?, :disclose!, :served, :unnamed_schemas, :loaded_schemas
358
347
 
359
348
  # Included into every example group, so graphql_context is there
@@ -273,9 +273,17 @@ namespace :graph_weaver do
273
273
  puts "#{name} #{Array(graph.queries).join(", ")} -> #{GraphWeaver::Internal::Util.relative(graph.output)}"
274
274
  puts " namespace: #{graph.namespace}" if graph.namespace
275
275
  # which server a graph's modules call — the one thing this task couldn't
276
- # say. A graph that bakes none falls back to GraphWeaver.client, which is
276
+ # say. A graph that names none falls back to GraphWeaver.client, which is
277
277
  # an app-wide setting and not this task's subject.
278
- puts " client: #{graph.client}" if graph.client
278
+ #
279
+ # Reported rather than raised: this is the task you run to find out why a
280
+ # graph is wrong, so a client whose constant is missing is the answer,
281
+ # not a reason to stop listing the others.
282
+ begin
283
+ puts " client: #{graph.client_url || graph.client}" if graph.client
284
+ rescue GraphWeaver::Error => e
285
+ puts " client: #{e.message}"
286
+ end
279
287
  # a registration is scoped to one graph, and nothing else says which
280
288
  GraphWeaver::Internal::Tasks.registrations(graph).each { |line| puts line }
281
289
  end
@@ -221,9 +221,10 @@ module GraphWeaver
221
221
  # warn line as it is constructed, and a predicate that raised to say
222
222
  # "no" put a refusal that never happened in the log of every :wire
223
223
  # example. The one that graph names, else config.router[:supergraph],
224
- # else the conventional dump when that's what it is. A client can't
225
- # supply one its schema is the API schema a router serves, with the
226
- # @join__* routing table stripped out.
224
+ # else the conventional dump when that's what it is, else the dump the
225
+ # app's own client was built from. A client's *schema* can't supply one
226
+ # — it is the API schema a router serves, with the @join__* routing
227
+ # table stripped out — but the file behind it carries the table.
227
228
  private def supergraph_for(graph)
228
229
  # named_schema?, so a graph that declared no schema of its own falls
229
230
  # through to config.router rather than past it to the conventional dump
@@ -232,7 +233,14 @@ module GraphWeaver
232
233
  return @router[:supergraph] if @router&.key?(:supergraph)
233
234
 
234
235
  path = GraphWeaver::SchemaLoader.locate_path
235
- path if path && GraphWeaver::Internal::Util.composed?(path)
236
+ return path if path && GraphWeaver::Internal::Util.composed?(path)
237
+
238
+ # Last, because codegen for the default graph reads the conventional
239
+ # dump, and the router must plan against what the modules were typed
240
+ # against.
241
+ client = GraphWeaver.client
242
+ source = client.schema_source if client.respond_to?(:schema_source)
243
+ source if source && GraphWeaver::Internal::Util.composed?(source)
236
244
  end
237
245
 
238
246
  # what to do about it, which differs by who asked: a graph in no
@@ -1,3 +1,3 @@
1
1
  module GraphWeaver
2
- VERSION = "0.7.1"
2
+ VERSION = "0.7.3"
3
3
  end