Class: GraphWeaver::Codegen
- Inherits:
-
Object
- Object
- GraphWeaver::Codegen
- 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
-
#module_name ⇒ Object
readonly
Returns the value of attribute module_name.
Class Method Summary collapse
-
.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.
-
.clear_scalars! ⇒ Object
Empty the registry entirely, built-ins included.
-
.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).
- .enum_registry ⇒ Object
-
.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:.
-
.generate(schema:, query:, module_name: nil, client: nil, scalars: nil, enums: nil, types: nil) ⇒ Object
one-step shorthand.
-
.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.
-
.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. -
.helper_module(graphql_name, block) ⇒ Object
a block-built mixin needs a name generated files can reference: GraphWeaver::TypeHelpers::Pet (suffixed on re-registration).
-
.inline_fragments(query, shared) ⇒ Object
Append the shared fragments a query spreads (transitively) to its source, so the sent query is self-contained.
-
.load_fragments(paths) ⇒ Object
Parse every fragment file under
pathsinto one { name => FragmentDefinition } map — reusable fragments a query can spread. -
.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.
-
.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.
-
.register_builtin_scalars! ⇒ Object
Built-in scalars — pre-registered entries in the one registry.
-
.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.
-
.register_enums(mappings) ⇒ Object
Bulk, inference-only form: register_enums("Species" => PetKind, ...).
-
.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.
-
.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.
-
.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.
-
.scalar_field?(schema, coordinate) ⇒ Boolean
Whether
coordinate("Type.field") names an existing scalar field — the validation for a per-field register_scalar override. - .scalar_registry ⇒ Object
-
.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.
-
.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.
- .type_registry ⇒ Object
-
.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.
Instance Method Summary collapse
- #generate ⇒ Object
- #generate_inputs(input_types, enum_types) ⇒ Object
- #generate_unions(fragments, names) ⇒ Object
-
#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
constructor
A client is anything responding to
execute(query, variables:)whose resultto_hs into => ..., "errors" => ... — a GraphWeaver::Client, a transport, a schema class, a fake. -
#used_union_names ⇒ Object
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.
-
#variable_type_names ⇒ Object
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.
Methods included from Inflect
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_name ⇒ Object (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_registry ⇒ Object
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::
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>::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.
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_registry ⇒ Object
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.
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_registry ⇒ Object
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.
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
#generate ⇒ Object
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., 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_names ⇒ Object
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_names ⇒ Object
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 |