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.
@@ -59,15 +59,10 @@ class GraphWeaver::Codegen
59
59
 
60
60
  attr_reader :name
61
61
 
62
- # A client is anything responding to `execute(query, variables:)`
63
- # whose result `to_h`s into {"data" => ..., "errors" => ...} a
64
- # GraphWeaver::Client, a transport, a schema class, a fake.
65
- #
66
- # client: (a constant, or its name as a string) becomes the generated
67
- # module's baked default; when omitted, generated code falls back to
68
- # the app default (GraphWeaver.client=). graph_name: is the graph the
69
- # module belongs to, baked in so a test mode can build its stand-in from
70
- # the right schema. name: is the module the file
62
+ # graph_name: is the graph the module belongs to, baked in because it is
63
+ # the one thing a module can't be told at call time: it decides which
64
+ # client the module runs against (GraphWeaver::QueryModule) and which
65
+ # schema a test mode fabricates from. name: is the module the file
71
66
  # defines, defaulting to the operation's own name; default_name: is
72
67
  # parse's container-scoped fallback (file generation stays strict — a
73
68
  # checked-in file deserves a deliberate name). types_namespace: is the shared-types workflow (see
@@ -78,7 +73,7 @@ class GraphWeaver::Codegen
78
73
  # whole-union field spread as one of them resolves to a canonical type in the
79
74
  # shared module (see used_union_names). path: is the file the query was read
80
75
  # from, named alongside line and column in validation errors.
81
- def initialize(schema:, query:, name: nil, client: nil, default_name: nil,
76
+ def initialize(schema:, query:, name: nil, default_name: nil,
82
77
  types_namespace: nil, hoistable_unions: nil, path: nil, module_name: nil,
83
78
  graph_name: nil, registry: GraphWeaver::Codegen.registry)
84
79
  renamed!(module_name)
@@ -96,28 +91,10 @@ class GraphWeaver::Codegen
96
91
  @used_unions = []
97
92
  # scalars this generation had no registration for (see report_untyped_scalars)
98
93
  @untyped_scalars = []
99
- @client_const = CLIENT_CONST.call(client)
100
- # the graph this module belongs to, baked in beside the client: a test
101
- # mode builds its stand-in from the module's own schema, and only the
102
- # module can say whose that is (GraphWeaver::Internal::TestClients).
103
- # A Symbol, as GraphWeaver.graph makes it — the name is the identity.
94
+ # the graph this module belongs to: its client and, under a test mode,
95
+ # its stand-in are both read off it, and only the module can say whose
96
+ # it is. A Symbol, as GraphWeaver.graph makes it the name is the identity.
104
97
  @graph_name = graph_name&.to_sym
105
-
106
- if client && @client_const.nil?
107
- # a live object can't be spelled in generated source — parse can
108
- # set one via the module's writer, but file generation cannot
109
- raise ArgumentError, "client: must be a named constant or String (got #{client.inspect}) — " \
110
- "put the object in a constant and name it, client: \"MyApi::CLIENT\"; pass live objects to parse"
111
- end
112
- # The String is written into the module verbatim, so anything that isn't a
113
- # constant path emits source that doesn't parse. A url is the way to get
114
- # here — it is where the endpoint is spelled everywhere else — so the fix
115
- # names the value that was passed.
116
- if @client_const && !@client_const.match?(CONSTANT_NAME)
117
- raise ArgumentError, "client: #{@client_const.inspect} isn't a constant — generated source " \
118
- "spells this name, so it has to be one: CLIENT = GraphWeaver.new(#{@client_const.inspect}), " \
119
- "then client \"CLIENT\""
120
- end
121
98
  end
122
99
 
123
100
  # 0.5 spelled it module_name:, in two of the three doors. One knob, one
@@ -129,33 +106,19 @@ class GraphWeaver::Codegen
129
106
  end
130
107
  private :renamed!
131
108
 
132
- # The constant name a client can be referenced by in generated
133
- # source — nil when it can't be (live objects, anonymous modules).
134
- # A lambda rather than a method: both `parse` and `initialize` need it,
135
- # from the class and from an instance.
136
- CLIENT_CONST = lambda do |client|
137
- case client
138
- when String then client
139
- when Module then client.name
140
- end
141
- end
142
- private_constant :CLIENT_CONST
143
-
144
109
  # one-step shorthand
145
- def self.generate(schema:, query:, name: nil, client: nil, path: nil, module_name: nil)
146
- new(schema:, query:, name:, client:, path:, module_name:).generate
110
+ def self.generate(schema:, query:, name: nil, path: nil, module_name: nil)
111
+ new(schema:, query:, name:, path:, module_name:).generate
147
112
  end
148
113
 
149
114
  # Development convenience: generate + eval in one step, no build
150
115
  # artifact or checked-in file. Same runtime semantics as the generated
151
116
  # file, but invisible to srb tc — use the build step for static typing.
152
- # Evaluates into an anonymous container, so no global constants leak;
153
- # client: additionally accepts a live object (set via .client=).
117
+ # Evaluates into an anonymous container, so no global constants leak.
118
+ # client: is the client the parsed module runs against — it has no graph to
119
+ # read one off — and a per-call `client:` still wins over it.
154
120
  def self.parse(schema:, query:, name: nil, client: nil, path: nil, module_name: nil, graph_name: nil)
155
- client_const = CLIENT_CONST.call(client)
156
-
157
- codegen = new(schema:, query:, name:, client: client_const, path:, module_name:, graph_name:,
158
- default_name: "Query")
121
+ codegen = new(schema:, query:, name:, path:, module_name:, graph_name:, default_name: "Query")
159
122
  source = codegen.generate
160
123
 
161
124
  container = Module.new
@@ -167,9 +130,10 @@ class GraphWeaver::Codegen
167
130
  container.module_eval(source, "(graph_weaver)", 1)
168
131
  mod = container.const_get(codegen.name)
169
132
  GraphWeaver::Internal::Log.log(:debug) { "parsed #{codegen.name} (dynamic module, #{source.bytesize} bytes)" }
170
- # live objects (or anonymous modules) can't be referenced from
171
- # generated source set them via the module's writer instead
172
- mod.client = client if client && client_const.nil?
133
+ # a parsed module generates no file, so it has no graph to read a client
134
+ # offclient: binds one, whatever kind of object it is. The writer is
135
+ # private: parsing is the only thing that may bind one.
136
+ mod.send(:client=, client) if client
173
137
  mod
174
138
  end
175
139
 
@@ -65,7 +65,24 @@ module GraphWeaver
65
65
 
66
66
  def queries = @queries || GraphWeaver.queries_paths
67
67
  def output = @output || GraphWeaver.generated_paths.first
68
- def client = @client
68
+
69
+ # The client this graph's modules call: the object `client` named, or the
70
+ # constant its name spells. nil when the graph names none — its modules
71
+ # go to GraphWeaver.client, like every other module.
72
+ #
73
+ # Resolved here, at call time, rather than spelled into generated source:
74
+ # renaming the constant is then an initializer edit and not a regeneration
75
+ # of every module, and a graph can name a live object.
76
+ def client
77
+ return @client unless @client.is_a?(String)
78
+
79
+ Object.const_get(@client)
80
+ rescue NameError
81
+ raise GraphWeaver::Error, "the client#{described} names #{@client.inspect} and nothing " \
82
+ "defines that constant, so its modules have no server to reach. Define it where the graph " \
83
+ "block can see it (config/initializers), or name the object itself: client " \
84
+ "GraphWeaver.new(\"https://api.example.com/graphql\")"
85
+ end
69
86
 
70
87
  # Every constant this graph generates lives under `namespace:` — the query
71
88
  # modules and the shared types module alike. Two schemas that each have a
@@ -95,7 +112,7 @@ module GraphWeaver
95
112
 
96
113
  # The dump this graph's schema was named by, when it was named by a file —
97
114
  # what a validation error's subgraph branding is read off. nil for a live
98
- # class, a Client, or inline SDL.
115
+ # class, inline SDL, or a Client built from any of those.
99
116
  def dump_path
100
117
  path = named_dump_path
101
118
  path if path && File.exist?(path)
@@ -109,6 +126,9 @@ module GraphWeaver
109
126
  return GraphWeaver::SchemaLoader.locate_path unless @schema
110
127
 
111
128
  source = named_source
129
+ # a client built from a dump names that dump: the file is where a
130
+ # supergraph's routing table is, and the loaded schema is not
131
+ source = source.schema_source if source.respond_to?(:schema_source) && source.schema_source
112
132
  path = source.respond_to?(:to_path) ? source.to_path : source
113
133
  path if path.is_a?(String) && GraphWeaver::SchemaLoader.dump_path?(path)
114
134
  end
@@ -133,26 +153,16 @@ module GraphWeaver
133
153
  url || live_schema
134
154
  end
135
155
 
136
- # The url this graph's modules post to, or nil. `client:` holds a constant
137
- # or its name codegen spells it into source so a name is resolved here
138
- # the way the generated DEFAULT_CLIENT lambda resolves it; a graph baking
139
- # none posts to the app default, which is where its modules go too.
156
+ # The url this graph's modules post to, or nil the graph's own client,
157
+ # else the app default, which is where its modules go too. What
158
+ # `schema:refresh` bootstraps a missing dump from, and what `rake
159
+ # graph_weaver:graphs` reports; a client with no url (a schema class
160
+ # running in-process) has none to report.
140
161
  def client_url
141
- client = @client.is_a?(String) ? resolve_client! : @client
142
- client ||= GraphWeaver.client
143
- target = (client.transport if client.respond_to?(:transport)) || client
162
+ target = client || GraphWeaver.client
163
+ target = (target.transport if target.respond_to?(:transport)) || target
144
164
  target.url if target.respond_to?(:url)
145
165
  end
146
- private :client_url
147
-
148
- def resolve_client!
149
- Object.const_get(@client)
150
- rescue NameError
151
- raise GraphWeaver::Error, "graph #{name.inspect} bakes client #{@client.inspect} into its " \
152
- "modules and nothing defines that constant, so there is no endpoint to introspect " \
153
- "#{named_dump_path} from"
154
- end
155
- private :resolve_client!
156
166
 
157
167
  # The composed supergraph this graph plans against, or nil — the dump it
158
168
  # names (for the default graph, the conventional one) when that dump
@@ -222,13 +232,12 @@ module GraphWeaver
222
232
  # The same three calls an app already writes at the top level, scoped here
223
233
  # to this graph alone.
224
234
  REGISTRATIONS = %i[register_scalar register_enum extend_type].freeze
225
- # These three end up spelled in generated source, so each takes the
226
- # constant or its name and stores the name.
227
- CONSTANT_SETTINGS = %i[client namespace types_module].freeze
228
- # …and these two are spelled as a `module` DEFINITION rather than a
229
- # reference, which is why a root anchor is refused on them below.
235
+ # These two are spelled in generated source, as a `module` DEFINITION,
236
+ # so each takes the constant or its name and stores the name — and a
237
+ # root anchor is refused on them below. `client` isn't spelled anywhere:
238
+ # the graph resolves it at call time, so it takes the object.
230
239
  MODULE_SETTINGS = %i[namespace types_module].freeze
231
- private_constant :CONSTANT_SETTINGS, :MODULE_SETTINGS
240
+ private_constant :MODULE_SETTINGS
232
241
 
233
242
  attr_reader :settings, :registrations
234
243
 
@@ -283,17 +292,18 @@ module GraphWeaver
283
292
  def respond_to_missing?(name, _private = false) = false
284
293
 
285
294
  # A Module where a constant's name goes says the same thing, and is what
286
- # `client Billing::CLIENT` reads like. Anything else passes through:
287
- # a schema is a path, SDL, a class, a Client, or a callable.
295
+ # `namespace Billing` reads like. Anything else passes through: a schema
296
+ # is a path, SDL, a class, a Client, or a callable, and a client is
297
+ # whatever object answers #execute.
288
298
  # On the singleton so the define_method setters above can reach it — srb
289
299
  # reads a define_method block's self as the class.
290
300
  def self.constant_name(setting, value)
291
- return value unless CONSTANT_SETTINGS.include?(setting)
301
+ return value unless MODULE_SETTINGS.include?(setting)
292
302
 
293
303
  # Generated modules are defined at the top level, where a root anchor
294
304
  # says nothing — and `module ::A::B` is not a name const_get can spell,
295
305
  # so it used to surface as a verdict on the .graphql file's name.
296
- if MODULE_SETTINGS.include?(setting) && value.is_a?(String) && value.start_with?("::")
306
+ if value.is_a?(String) && value.start_with?("::")
297
307
  raise ArgumentError, "#{setting} #{value.inspect}: drop the leading `::` — #{setting} " \
298
308
  "names a module generated source defines, and it defines it at the top level either way"
299
309
  end
@@ -8,13 +8,12 @@ module GraphWeaver
8
8
  # a `graphql_*` helper writes to.
9
9
  #
10
10
  # A tag used to work by swapping GraphWeaver.client, which is the LAST
11
- # place a module looks: one generated with `client:` reads its baked
12
- # DEFAULT_CLIENT first and never got there, so the tag quietly didn't
13
- # apply. The mode installs itself here instead, and QueryModule asks
14
- # before it reads that constant — so a tag reaches every module the
15
- # example runs, bound or not.
11
+ # place a module looks: one whose graph names a client of its own reads
12
+ # that first and never got there, so the tag quietly didn't apply. The
13
+ # mode installs itself here instead, and QueryModule asks before it reads
14
+ # the graph — so a tag reaches every module the example runs.
16
15
  #
17
- # Keyed by the graph a module was generated from (its baked GRAPH), since
16
+ # Keyed by the graph a module was generated from (its GRAPH), since
18
17
  # the honest answer varies: :fake for a billing module has to fabricate
19
18
  # billing's shapes, not the other schema's. A helper names its graphs the
20
19
  # same way and lands in the same table, so what an example says applies to
@@ -254,11 +253,8 @@ module GraphWeaver
254
253
  # generated before its graph was declared, or by an older release —
255
254
  # and guessing would fake one schema's shapes at another's module.
256
255
  def graph_for!(mod)
257
- graphs = GraphWeaver.graphs
258
- return graphs.first if graphs.one?
259
-
260
256
  name = mod.const_defined?(:GRAPH, false) ? mod.const_get(:GRAPH) : nil
261
- found = graphs.find { |graph| graph.name == name }
257
+ found = Util.graph_named(name)
262
258
  return found if found
263
259
 
264
260
  # Two doors produce a module, so the fix has two spellings: a file
@@ -268,7 +264,7 @@ module GraphWeaver
268
264
  raise GraphWeaver::Error, "#{mod} doesn't say which of this app's graphs " \
269
265
  "(#{declared_names}) it was generated from, so #{@mode.inspect} has nothing to run " \
270
266
  "it against — regenerate it (rake graph_weaver:generate), or, if it came from " \
271
- "GraphWeaver.parse, say which there (graph: #{graphs.first.name.inspect})."
267
+ "GraphWeaver.parse, say which there (graph: #{GraphWeaver.graphs.first.name.inspect})."
272
268
  end
273
269
  end
274
270
  end
@@ -141,6 +141,21 @@ module GraphWeaver
141
141
  schema && GraphWeaver.graphs.find { |candidate| candidate.live_schema.equal?(schema) }
142
142
  end
143
143
 
144
+ # The declared graph `name` names, or nil — how a generated module
145
+ # finds the graph whose client it runs against, and whose schema a
146
+ # test mode fabricates from.
147
+ #
148
+ # One graph in an app is the answer whatever a module calls it: a
149
+ # module generated before its graph was named, or by an older release,
150
+ # still belongs to the only graph there is. With several, guessing
151
+ # would send one schema's query to another's endpoint.
152
+ def graph_named(name)
153
+ graphs = GraphWeaver.graphs
154
+ return graphs.first if graphs.one?
155
+
156
+ graphs.find { |graph| graph.name == name }
157
+ end
158
+
144
159
  # Where generated modules are READ from: the configured patterns, plus
145
160
  # any graph writing somewhere they don't already cover. generated_paths'
146
161
  # default glob (app/graphql/*/generated) covers the conventional layout,
@@ -12,21 +12,21 @@ module GraphWeaver
12
12
  # Runtime for generated query modules: the client plumbing, which is the
13
13
  # one part of a generated module that carries no per-query type
14
14
  # information — every module's copy was identical. `extend
15
- # GraphWeaver::QueryModule` supplies `client`/`client=`; execute and
16
- # from_response stay generated, since their sigs are the query's types and
17
- # those are the point.
15
+ # GraphWeaver::QueryModule` supplies `client`; execute and from_response
16
+ # stay generated, since their sigs are the query's types and those are the
17
+ # point.
18
18
  #
19
- # Resolution order, per the docs: per call → per module (`MyQuery.client =`)
20
- # → a test mode's stand-in (Internal::TestClients) → the module's baked
21
- # DEFAULT_CLIENT → `GraphWeaver.client`.
19
+ # Resolution order, per the docs: per call → a test mode's stand-in
20
+ # (Internal::TestClients) → the client the module's graph names →
21
+ # `GraphWeaver.client`. A module has no fifth slot you can set: a parsed
22
+ # module runs against whatever parsed it (GraphWeaver.parse(client:)),
23
+ # which is a property of parsing rather than a per-module override.
22
24
  module QueryModule
23
25
  extend T::Sig
24
26
 
25
- sig { params(client: T.untyped).void }
26
- attr_writer :client
27
-
28
- # the default client (a GraphWeaver::Client or any transport) for
29
- # execute: per-module override, else the baked default, else the app one
27
+ # What this module would execute through, right now — the client a parse
28
+ # bound it to, else the order above. A diagnostic, and what `execute`
29
+ # reads when the call names none.
30
30
  sig { returns(T.untyped) }
31
31
  def client
32
32
  @client || default_client
@@ -34,6 +34,13 @@ module GraphWeaver
34
34
 
35
35
  private
36
36
 
37
+ # Bound by GraphWeaver.parse, which is the only caller: a parsed module
38
+ # generates no file, so it has no graph to read a client off. Private
39
+ # because a generated module's client comes from its graph — one way to
40
+ # say a thing.
41
+ sig { params(client: T.untyped).void }
42
+ attr_writer :client
43
+
37
44
  # The one call a generated `execute` makes: resolve the client, run this
38
45
  # module's own operation, hand the raw response back for from_response to
39
46
  # wrap. Here rather than emitted, so what has to BRACKET a request — the
@@ -55,9 +62,7 @@ module GraphWeaver
55
62
  mod = T.unsafe(self)
56
63
  # the graph codegen baked in, never one inferred from the client — a
57
64
  # wrong label on a request is worse than no label
58
- graph = mod.const_defined?(:GRAPH, false) ? mod.const_get(:GRAPH) : nil
59
-
60
- GraphWeaver::Internal::Log.with_graph(graph) do
65
+ GraphWeaver::Internal::Log.with_graph(graph_name) do
61
66
  client_for(client).execute(mod.const_get(:QUERY), variables:,
62
67
  operation_name: mod.const_get(:OPERATION_NAME))
63
68
  end
@@ -79,20 +84,28 @@ module GraphWeaver
79
84
  "#{self}: client must respond to #execute(query, variables:), got #{target.class}"
80
85
  end
81
86
 
82
- # Codegen's `client:` constant, emitted as a DEFAULT_CLIENT lambda so the
83
- # constant it names is resolved on first use rather than at load — a
84
- # generated file may load before the initializer that builds the client.
87
+ # A module knows which graph it belongs to, and the graph knows how to
88
+ # reach it: the client that graph names, else the app default. Read at
89
+ # call time, so renaming the constant a graph names is an initializer
90
+ # edit rather than a regeneration of every module.
85
91
  #
86
- # A test mode stands in for it: what codegen baked in is exactly what a
87
- # `graphql:` tag means to replace, so a bound module is covered by the
88
- # tag like every other one.
92
+ # A test mode stands in ahead of it: the graph's client is exactly what a
93
+ # `graphql:` tag means to replace, so a tagged example reaches a module
94
+ # whose graph names a client like every other one.
89
95
  sig { returns(T.untyped) }
90
96
  def default_client
91
- mod = T.unsafe(self)
92
- stand_in = GraphWeaver::Internal::TestClients.for(mod)
97
+ stand_in = GraphWeaver::Internal::TestClients.for(T.unsafe(self))
93
98
  return stand_in if stand_in
94
99
 
95
- mod.const_defined?(:DEFAULT_CLIENT, false) ? mod.const_get(:DEFAULT_CLIENT).call : GraphWeaver.client!
100
+ GraphWeaver::Internal::Util.graph_named(graph_name)&.client || GraphWeaver.client!
101
+ end
102
+
103
+ # The graph codegen baked in, by name — nil for a module generated before
104
+ # graphs existed, or by a GraphWeaver.parse that named none.
105
+ sig { returns(T.untyped) }
106
+ def graph_name
107
+ mod = T.unsafe(self)
108
+ mod.const_defined?(:GRAPH, false) ? mod.const_get(:GRAPH) : nil
96
109
  end
97
110
  end
98
111
  end