Class: GraphWeaver::Codegen

Inherits:
Object
  • Object
show all
Includes:
Emit, Inflect, Selection
Defined in:
lib/graph_weaver/codegen/emit.rb,
lib/graph_weaver/codegen.rb,
lib/graph_weaver/codegen/nodes.rb,
lib/graph_weaver/codegen/enum_type.rb,
lib/graph_weaver/codegen/scalar_type.rb

Overview

typed: true frozen_string_literal: true

Defined Under Namespace

Modules: Emit Classes: EnumNode, EnumType, InputNode, List, MappedEnum, NarrowedNode, Node, NonNull, ObjectNode, Scalar, ScalarType, UnionNode, UnionRefNode, VarDef

Constant Summary collapse

HOISTED_UNION_RESERVED =

module-level constants every generated query module defines — a hoisted union aliased to one of these would clash at load

%w[Result QUERY].to_set.freeze
RUBY_KEYWORDS =

Names that cannot appear bare in generated Ruby: keywords aren't valid identifiers, and the struct's own generated methods would be silently replaced by a same-named prop reader.

%w[
  alias and begin break case class def defined? do else elsif end
  ensure false for if in module next nil not or redo rescue retry
  return self super then true undef unless until when while yield
  BEGIN END __FILE__ __LINE__ __ENCODING__
].to_set.freeze
GENERATED_METHODS =
%w[serialize to_h].to_set.freeze
ALIAS_NAME =

accessor names and path segments are interpolated verbatim into generated source, so — like module_name — they must be plain identifiers, never arbitrary text that could inject code

/\A[a-zA-Z_]\w*[?!]?\z/
ALIAS_SEGMENT =
/\A[a-zA-Z_]\w*\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Inflect

camelize, underscore

Methods included from Selection

#applies?, #each_field, #gather, #load_operation, #operation_root_type

Constructor Details

#initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil, scalars: nil, enums: nil, types: nil, inputs_namespace: nil, unions_namespace: nil, hoistable_unions: nil) ⇒ Codegen

A client is anything responding to execute(query, variables:) whose result to_hs into => ..., "errors" => ... — a GraphWeaver::Client, a transport, a schema class, a fake.

client: (a constant, or its name as a string) becomes the generated module's baked default; when omitted, generated code falls back to the app default (GraphWeaver.client=). module_name: defaults to the operation's name; default_module_name: is parse's container-scoped fallback (file generation stays strict — a checked-in file deserves a deliberate name). scalars:/enums:/types: are client-scoped overlays consulted before the global registries (ScalarType, EnumType, and arrays of mixin modules, each keyed by GraphQL name). inputs_namespace: is the shared-inputs workflow (see GraphWeaver.generate!): variable types live once in that module and the query module aliases what it uses. unions_namespace:/hoistable_unions: are the parallel shared-unions workflow — a whole-union field spread as a named shared fragment resolves to one canonical type in that module (see used_union_names).



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/graph_weaver/codegen.rb', line 55

def initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil,
  scalars: nil, enums: nil, types: nil, inputs_namespace: nil, unions_namespace: nil,
  hoistable_unions: nil)
  @schema = schema
  @query = query.strip
  @module_name = module_name
  @default_module_name = default_module_name
  @scalars = scalars || {}
  @enums = enums || {}
  @types = types || {}
  @inputs_namespace = inputs_namespace
  # the shared-unions workflow: unions_namespace names the module hoisted
  # unions live in; hoistable_unions is the set of shared fragment names this
  # query may hoist (spreads it inlined, minus any it shadows locally)
  @unions_namespace = unions_namespace
  @hoistable_unions = hoistable_unions || []
  @used_unions = []
  @client_const = self.class.client_const(client)

  if client && @client_const.nil?
    # a live object can't be spelled in generated source — parse can
    # set one via the module's writer, but file generation cannot
    raise ArgumentError, "client: must be a named constant or String (got #{client.inspect}); pass live objects to parse"
  end
end

Instance Attribute Details

#module_nameObject (readonly)

Returns the value of attribute module_name.



35
36
37
# File 'lib/graph_weaver/codegen.rb', line 35

def module_name
  @module_name
end

Class Method Details

.add_type_helpers(entry, graphql_name, mixins, requires, block, aliases = {}) ⇒ Object

shared with Client#extend_type: build/validate the mixins and append them to a registry entry



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/graph_weaver/codegen/enum_type.rb', line 171

def add_type_helpers(entry, graphql_name, mixins, requires, block, aliases = {})
  mixins = mixins.dup
  mixins << helper_module(graphql_name, block) if block

  if mixins.empty? && aliases.empty?
    raise ArgumentError, "pass one or more helper modules, a block, or alias:"
  end
  mixins.each do |mixin|
    unless mixin.is_a?(Module) && mixin.name
      raise ArgumentError, "type helpers must be named modules, got #{mixin.inspect}"
    end
  end

  entry[:mixins].concat(mixins)
  entry[:requires].concat(Array(requires))
  (entry[:aliases] ||= {}).merge!(aliases)
  entry
end

.clear_scalars!Object

Empty the registry entirely, built-ins included. Mostly useful for tests; see reset_scalars! to restore the built-in defaults.



224
225
226
227
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 224

def clear_scalars!
  scalar_registry.clear
  self
end

.client_const(client) ⇒ Object

The constant name a client can be referenced by in generated source — nil when it can't be (live objects, anonymous modules).



83
84
85
86
87
88
# File 'lib/graph_weaver/codegen.rb', line 83

def self.client_const(client)
  case client
  when String then client
  when Module then client.name
  end
end

.enum_registryObject



86
87
88
# File 'lib/graph_weaver/codegen/enum_type.rb', line 86

def enum_registry
  @enum_registry ||= {}
end

.extend_type(graphql_name, *mixins, requires: nil, **kw, &block) ⇒ Object

Attach app-owned helper modules to every struct generated from a GraphQL type — the logic stays in your code, generation wires it in:

 GraphWeaver.extend_type("Pet", PetHelpers)

Or build the mixin inline — the block is module_eval'd into a fresh module auto-named GraphWeaver::TypeHelpers::. Handy for quick decoration; srb tc can't see into block-defined methods, so prefer a named module where static checking matters:

 GraphWeaver.extend_type("Pet") do
   def display_name = "#{name} the pet"
 end

Additive: repeated registrations (and client-scoped ones) stack.

alias: projects a (possibly nested) selected field onto a flat, typed accessor on the struct — the one derivation codegen can type itself, so it's emitted into the struct body where the field is in scope:

 GraphWeaver.extend_type("Widget", alias: { tag: "meta.tag" })
 GraphWeaver.extend_type("Widget", alias: "meta.tag")        # accessor named `tag`
 GraphWeaver.extend_type("Widget", alias: ["meta.tag", "meta.color"])

A path segment is a field, or first/last to pick one element out of a list hop (always nilable): alias: { entity: "_entities.first" }.

optional: true makes the aliases lenient — a query whose selection doesn't fit the path just omits the accessor instead of failing generation. Use it for a root-type accessor (a Query alias every query would otherwise have to satisfy) or one that only fits some selections.



121
122
123
124
125
# File 'lib/graph_weaver/codegen/enum_type.rb', line 121

def extend_type(graphql_name, *mixins, requires: nil, **kw, &block)
  aliases = take_aliases(kw)
  entry = type_registry[graphql_name.to_s] ||= { mixins: [], requires: [], aliases: {} }
  add_type_helpers(entry, graphql_name, mixins, requires, block, aliases)
end

.generate(schema:, query:, module_name: nil, client: nil, scalars: nil, enums: nil, types: nil) ⇒ Object

one-step shorthand



91
92
93
# File 'lib/graph_weaver/codegen.rb', line 91

def self.generate(schema:, query:, module_name: nil, client: nil, scalars: nil, enums: nil, types: nil)
  new(schema:, query:, module_name:, client:, scalars:, enums:, types:).generate
end

.generate_inputs(schema:, module_name:, input_types: [], enum_types: [], scalars: nil, enums: nil, types: nil) ⇒ Object

The shared inputs artifact: the named input/enum types — plus everything they transitively reference — emitted once per schema as a manifest (inputs.rb) plus one file per type under inputs/, so a schema migration diffs only the types it touched. Returns { relative_filename => source }.



134
135
136
137
138
# File 'lib/graph_weaver/codegen.rb', line 134

def self.generate_inputs(schema:, module_name:, input_types: [], enum_types: [],
  scalars: nil, enums: nil, types: nil)
  codegen = new(schema:, query: "", module_name:, scalars:, enums:, types:)
  codegen.generate_inputs(input_types, enum_types)
end

.generate_unions(schema:, module_name:, fragments:, names:, scalars: nil, enums: nil, types: nil) ⇒ Object

The shared unions artifact: each named shared fragment a query hoisted, built once against the schema as <module_name>::, so the same union across queries resolves to one Ruby type family. fragments is the loaded shared-fragment table (nested spreads resolve through it); names the fragments to build. Returns { "unions.rb" => source }.



161
162
163
164
165
# File 'lib/graph_weaver/codegen.rb', line 161

def self.generate_unions(schema:, module_name:, fragments:, names:,
  scalars: nil, enums: nil, types: nil)
  codegen = new(schema:, query: "", module_name:, scalars:, enums:, types:)
  codegen.generate_unions(fragments, names)
end

.helper_module(graphql_name, block) ⇒ Object

a block-built mixin needs a name generated files can reference: GraphWeaver::TypeHelpers::Pet (suffixed on re-registration)



192
193
194
195
196
197
198
# File 'lib/graph_weaver/codegen/enum_type.rb', line 192

def helper_module(graphql_name, block)
  base = GraphWeaver::Inflect.camelize(graphql_name.to_s)
  name = base
  count = 1
  name = "#{base}V#{count += 1}" while GraphWeaver::TypeHelpers.const_defined?(name, false)
  GraphWeaver::TypeHelpers.const_set(name, Module.new(&block))
end

.inline_fragments(query, shared) ⇒ Object

Append the shared fragments a query spreads (transitively) to its source, so the sent query is self-contained. Unused shared fragments are left out.



343
344
345
346
347
348
# File 'lib/graph_weaver/codegen.rb', line 343

def self.inline_fragments(query, shared)
  used = shared_fragment_spreads(query, shared)
  return query if used.empty?

  "#{query.rstrip}\n\n#{used.sort.map { |name| shared.fetch(name).to_query_string }.join("\n\n")}\n"
end

.load_fragments(paths) ⇒ Object

Parse every fragment file under paths into one { name => FragmentDefinition } map — reusable fragments a query can spread. Fragment files hold only fragments (no operations); names are unique across them.



316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/graph_weaver/codegen.rb', line 316

def self.load_fragments(paths)
  Array(paths).flat_map { |dir| Dir[File.join(dir, "*.graphql")].sort }.each_with_object({}) do |file, out|
    doc = GraphQL.parse(File.read(file))
    if doc.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).any?
      raise GraphWeaver::Error, "#{file}: fragment files define only fragments, no operations"
    end
    doc.definitions.grep(GraphQL::Language::Nodes::FragmentDefinition).each do |frag|
      raise GraphWeaver::Error, "duplicate shared fragment '#{frag.name}' (#{file})" if out.key?(frag.name)
      out[frag.name] = frag
    end
  end
end

.normalize_aliases(input, optional:) ⇒ Object

{ accessor => { segments:, optional: } } from a path string (accessor named after the last segment), an array of such, or an { accessor => path } hash. optional: marks every alias in this registration as lenient.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/graph_weaver/codegen/enum_type.rb', line 144

def normalize_aliases(input, optional:)
  pairs = case input
  when nil then []
  when String then [[input.split(".").last, input.split(".")]]
  when Array then input.map { |path| [path.split(".").last, path.split(".")] }
  when Hash then input.map { |name, path| [name.to_s, path.to_s.split(".")] }
  else raise ArgumentError, "alias: expects a String, Array, or Hash, got #{input.class}"
  end
  pairs.to_h do |name, segments|
    unless name.to_s.match?(ALIAS_NAME)
      raise ArgumentError, "alias name #{name.inspect} is not a valid method name"
    end
    raise ArgumentError, "alias #{name.inspect} has an empty path" if segments.empty?

    bad = segments.reject { |seg| seg.match?(ALIAS_SEGMENT) }
    raise ArgumentError, "alias #{name.inspect} has an invalid path segment: #{bad.first.inspect}" if bad.any?

    [name, { segments:, optional: }]
  end
end

.parse(schema:, query:, module_name: nil, client: nil, scalars: nil, enums: nil, types: nil) ⇒ Object

Development convenience: generate + eval in one step, no build artifact or checked-in file. Same runtime semantics as the generated file, but invisible to srb tc — use the build step for static typing. Evaluates into an anonymous container, so no global constants leak; client: additionally accepts a live object (set via .client=).



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/graph_weaver/codegen.rb', line 100

def self.parse(schema:, query:, module_name: nil, client: nil, scalars: nil, enums: nil, types: nil)
  client_const = client_const(client)

  codegen = new(schema:, query:, module_name:, client: client_const, default_module_name: "Query",
    scalars:, enums:, types:)
  source = codegen.generate

  container = Module.new
  container.module_eval(source, "(graph_weaver)", 1)
  mod = container.const_get(codegen.module_name)
  GraphWeaver.log(:debug) { "parsed #{codegen.module_name} (dynamic module, #{source.bytesize} bytes)" }
  # live objects (or anonymous modules) can't be referenced from
  # generated source — set them via the module's writer instead
  mod.client = client if client && client_const.nil?
  mod
end

.register_builtin_scalars!Object

Built-in scalars — pre-registered entries in the one registry. The standard scalars stay pass-through: their Ruby classes (String, Integer, Float) define neither .parse nor .load, so codec inference matches nothing and leaves them identity — which is exactly why we can name them with the real class constants. Date deserializes via ISO-8601 (it does define .parse, but we want iso8601 specifically, so it's explicit). Input coercion is a generation-time concern: GraphWeaver.auto_coerce gives the convertible built-ins their conversion (see ScalarType::AUTO_CONVERSIONS).



248
249
250
251
252
253
254
255
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 248

def register_builtin_scalars!
  register_scalar "ID", String
  register_scalar "String", String
  register_scalar "Int", Integer
  register_scalar "Float", Float
  register_scalar "Boolean", "T::Boolean"
  register_scalar "Date", Date, cast: :iso8601, serialize: :iso8601, requires: "date"
end

.register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil) ⇒ Object

Map a GraphQL enum onto an app-owned T::Enum (see EnumType); the global default — client.register_enum scopes to one client.



77
78
79
# File 'lib/graph_weaver/codegen/enum_type.rb', line 77

def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
  enum_registry[graphql_name.to_s] = EnumType.new(graphql_name, type, map:, fallback:, requires:)
end

.register_enums(mappings) ⇒ Object

Bulk, inference-only form: register_enums("Species" => PetKind, ...)



82
83
84
# File 'lib/graph_weaver/codegen/enum_type.rb', line 82

def register_enums(mappings)
  mappings.each { |graphql_name, type| register_enum(graphql_name, type) }
end

.register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil) ⇒ Object

Register (or override) how a GraphQL custom scalar deserializes into a Ruby object and serializes back onto the wire. See ScalarType for the accepted cast:/serialize:/requires: forms. Later registrations win, so an app can override a built-in (e.g. map Date onto its own type).



204
205
206
207
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 204

def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
  scalar_registry[graphql_name.to_s] =
    ScalarType.new(graphql_name, type, cast:, serialize:, requires:, coerce:)
end

.reset_scalars!Object

Drop every custom registration and restore the built-in scalars — the clean slate to reach for between tests, or to undo overrides. (Want the built-ins to coerce loose input? That's GraphWeaver.auto_coerce, resolved at generation time — no re-registering.)



233
234
235
236
237
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 233

def reset_scalars!
  clear_scalars!
  register_builtin_scalars!
  self
end

.scalar(graphql_name) ⇒ Object

The ScalarType for a scalar name; unknown scalars fall back to an untyped pass-through (T.untyped, no cast) — the prior behavior for scalars outside the table.



212
213
214
215
216
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 212

def scalar(graphql_name)
  scalar_registry.fetch(graphql_name.to_s) do
    ScalarType.new(graphql_name, "T.untyped")
  end
end

.scalar_field?(schema, coordinate) ⇒ Boolean

Whether coordinate ("Type.field") names an existing scalar field — the validation for a per-field register_scalar override.

Returns:

  • (Boolean)


303
304
305
306
307
308
309
310
311
# File 'lib/graph_weaver/codegen.rb', line 303

def self.scalar_field?(schema, coordinate)
  type_name, field_name = coordinate.split(".", 2)
  return false unless field_name

  field = schema.get_field(type_name, field_name)
  !!field && field.type.unwrap.kind.name == "SCALAR"
rescue StandardError
  false
end

.scalar_registryObject



218
219
220
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 218

def scalar_registry
  @scalar_registry ||= {}
end

.shared_fragment_spreads(query, shared) ⇒ Object

The shared fragments a query spreads (transitively), excluding any it shadows with a local definition of the same name — the names inline_fragments appends, and the set the generate! workflow may hoist when they sit on a whole-union field.



333
334
335
336
337
338
339
# File 'lib/graph_weaver/codegen.rb', line 333

def self.shared_fragment_spreads(query, shared)
  return [] if shared.empty?

  doc = GraphQL.parse(query)
  local = doc.definitions.grep(GraphQL::Language::Nodes::FragmentDefinition).map(&:name)
  reachable_fragments(fragment_spreads(doc.definitions), shared, local)
end

.take_aliases(kw) ⇒ Object

Pull alias:/optional: out of the keyword rest and normalize; any other keyword is a typo worth flagging rather than silently dropping.

Raises:

  • (ArgumentError)


129
130
131
132
133
# File 'lib/graph_weaver/codegen/enum_type.rb', line 129

def take_aliases(kw)
  aliases = normalize_aliases(kw.delete(:alias), optional: !!kw.delete(:optional))
  raise ArgumentError, "unknown keyword: #{kw.keys.first}" unless kw.empty?
  aliases
end

.type_registryObject



165
166
167
# File 'lib/graph_weaver/codegen/enum_type.rb', line 165

def type_registry
  @type_registry ||= {}
end

.validate_registration!(schema, kind, name) ⇒ Object

A client-scoped registration names a type in a specific schema — a typo'd name would otherwise be a silent no-op, the most confusing failure mode available. Called eagerly by Client#register_* when the schema is already loaded, and again at generation (covers clients whose schema introspects lazily). Global registrations skip this: they may target a different client's server.

Raises:



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/graph_weaver/codegen.rb', line 283

def self.validate_registration!(schema, kind, name)
  # register_scalar("Type.field", ...) overrides one field's scalar — validate
  # the field exists and is a scalar, not that a type named "Type.field" exists.
  if kind == "scalar" && name.include?(".")
    return if scalar_field?(schema, name)

    raise GraphWeaver::Error, "register_scalar(#{name.inspect}) matches no scalar field in this schema"
  end

  return if schema.get_type(name)

  suggestion = GraphWeaver.did_you_mean(schema.types.keys, name)
  hint = suggestion ? " — did you mean '#{suggestion}'?" : ""
  # the type registry is reached via extend_type; scalars/enums via register_*
  method = kind == "type" ? "extend_type" : "register_#{kind}"
  raise GraphWeaver::Error, "#{method}(#{name.inspect}) matches no type in this schema#{hint}"
end

Instance Method Details

#generateObject



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/graph_weaver/codegen.rb', line 211

def generate
  begin
    errors = @schema.validate(@query)
  rescue GraphQL::ParseError => e
    # unparseable queries wrap like invalid ones — everything raised
    # here descends from GraphWeaver::Error
    raise GraphWeaver::ValidationError.new([{ message: e.message, line: nil, column: nil }])
  end
  if errors.any?
    raise GraphWeaver::ValidationError.new(errors.map { |e| validation_detail(e) })
  end

  validate_registrations!

  @variable_enums = {}
  @variable_inputs = {}
  @mapped_enums = {}
  @used_unions = []
  # requires the generated file needs (custom scalars, enum mappings,
  # type helpers all contribute)
  @requires = []

  operation = load_operation(@query)
  root_type = operation_root_type(operation)

  @module_name ||= operation.name || @default_module_name
  unless @module_name
    raise ArgumentError, "module_name: required for anonymous operations"
  end

  # generated source is eval'd by parse — never let a name inject code
  unless @module_name.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
    raise ArgumentError, "module_name: must be a constant name, got #{@module_name.inspect}"
  end

  variables = operation.variables.map do |var|
    node = ast_type_ref(var.type)
    # a variable is optional when nullable or defaulted; optional kwargs
    # default to nil and are omitted from the wire
    required = node.non_null? && var.default_value.nil?
    kwarg = underscore(var.name)
    # kwargs are declared and forwarded bare in generated source
    if RUBY_KEYWORDS.include?(kwarg)
      raise GraphWeaver::Error,
        "variable $#{var.name} would become the kwarg '#{kwarg}:', which generated code can't declare " \
        "(a Ruby keyword) — rename the variable"
    end
    VarDef.new(kwarg, var.name, node, required)
  end

  # two variables that underscore to the same kwarg ($userId + $user_id) would
  # silently drop one on the wire — flag it like a prop collision
  collision = variables.group_by(&:kwarg).find { |_, vars| vars.size > 1 }
  if collision
    wire = collision.last.map { |var| "$#{var.wire}" }.join(", ")
    raise GraphWeaver::Error,
      "variables #{wire} both map to the kwarg '#{collision.first}:' — rename one"
  end

  root = object_node(root_type, operation.selections, "Result")

  emit_module(root, variables)
end

#generate_inputs(input_types, enum_types) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/graph_weaver/codegen.rb', line 140

def generate_inputs(input_types, enum_types)
  unless @module_name&.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
    raise ArgumentError, "inputs module name must be a constant name, got #{@module_name.inspect}"
  end

  @variable_enums = {}
  @variable_inputs = {}
  @mapped_enums = {}
  @requires = []

  enum_types.sort.each { |name| variable_core(@schema.get_type(name)) }
  input_types.sort.each { |name| input_node(@schema.get_type(name)) }

  emit_inputs_files
end

#generate_unions(fragments, names) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/graph_weaver/codegen.rb', line 167

def generate_unions(fragments, names)
  unless @module_name&.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
    raise ArgumentError, "unions module name must be a constant name, got #{@module_name.inspect}"
  end

  @requires = []
  @mapped_enums = {}
  # nested spreads inside a shared fragment resolve through the whole table
  @fragments = fragments

  unions = names.uniq.sort.map do |name|
    class_name = camelize(name)
    # the query module aliases <class_name> = <unions module>::<class_name>;
    # a name that camelizes to a generated module-level constant (the Result
    # struct, the QUERY heredoc) would collide with that alias at load
    if HOISTED_UNION_RESERVED.include?(class_name)
      raise GraphWeaver::Error,
        "shared fragment #{name.inspect} hoists to #{class_name}, which collides with a generated constant — rename the fragment"
    end
    fragment = fragments.fetch(name)
    type = @schema.get_type(fragment.type.name)
    UnionNode.new(class_name, union_members(type, fragment.selections))
  end

  emit_unions_file(unions)
end

#used_union_namesObject

The shared union fragments this query hoisted, by name — the generate! workflow unions these across queries to decide what the shared unions module must contain.



127
# File 'lib/graph_weaver/codegen.rb', line 127

def used_union_names = @used_unions.dup

#variable_type_namesObject

The schema-level variable types this query touched, by GraphQL name — the generate! workflow unions these across queries to decide what the shared inputs module must contain.



120
121
122
# File 'lib/graph_weaver/codegen.rb', line 120

def variable_type_names
  { inputs: @variable_inputs.keys, enums: @variable_enums.keys, mapped: @mapped_enums.keys }
end