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, VarDef
Constant Summary collapse
- 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
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) ⇒ Object
shared with Client#register_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
-
.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.
-
.helper_module(graphql_name, block) ⇒ Object
a block-built mixin needs a name generated files can reference: GraphWeaver::TypeHelpers::Pet (suffixed on re-registration).
-
.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.
-
.register_type(graphql_name, *mixins, requires: nil, &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:.
-
.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_registry ⇒ Object
- .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
-
#initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil, scalars: nil, enums: nil, types: nil, inputs_namespace: 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. -
#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, #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) ⇒ 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.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/graph_weaver/codegen.rb', line 52 def initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil, scalars: nil, enums: nil, types: nil, inputs_namespace: 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 @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) ⇒ Object
shared with Client#register_type: build/validate the mixins and append them to a registry entry
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/graph_weaver/codegen/enum_type.rb', line 116 def add_type_helpers(entry, graphql_name, mixins, requires, block) mixins = mixins.dup mixins << helper_module(graphql_name, block) if block raise ArgumentError, "pass one or more helper modules, or a block" if mixins.empty? 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 end |
.clear_scalars! ⇒ Object
Empty the registry entirely, built-ins included. Mostly useful for tests; see reset_scalars! to restore the built-in defaults.
221 222 223 224 |
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 221 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).
73 74 75 76 77 78 |
# File 'lib/graph_weaver/codegen.rb', line 73 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 |
.generate(schema:, query:, module_name: nil, client: nil, scalars: nil, enums: nil, types: nil) ⇒ Object
one-step shorthand
81 82 83 |
# File 'lib/graph_weaver/codegen.rb', line 81 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 }.
119 120 121 122 123 |
# File 'lib/graph_weaver/codegen.rb', line 119 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 |
.helper_module(graphql_name, block) ⇒ Object
a block-built mixin needs a name generated files can reference: GraphWeaver::TypeHelpers::Pet (suffixed on re-registration)
134 135 136 137 138 139 140 |
# File 'lib/graph_weaver/codegen/enum_type.rb', line 134 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 |
.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=).
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/graph_weaver/codegen.rb', line 90 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).
245 246 247 248 249 250 251 252 |
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 245 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).
201 202 203 204 |
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 201 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 |
.register_type(graphql_name, *mixins, requires: nil, &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.register_type("Pet", PetHelpers)
Or build the mixin inline — the block is module_eval'd into a fresh
module auto-named GraphWeaver::TypeHelpers::
GraphWeaver.register_type("Pet") do
def display_name = "#{name} the pet"
end
Additive: repeated registrations (and client-scoped ones) stack.
105 106 107 108 |
# File 'lib/graph_weaver/codegen/enum_type.rb', line 105 def register_type(graphql_name, *mixins, requires: nil, &block) entry = type_registry[graphql_name.to_s] ||= { mixins: [], requires: [] } add_type_helpers(entry, graphql_name, mixins, requires, block) 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.)
230 231 232 233 234 |
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 230 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.
209 210 211 212 213 |
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 209 def scalar(graphql_name) scalar_registry.fetch(graphql_name.to_s) do ScalarType.new(graphql_name, "T.untyped") end end |
.scalar_registry ⇒ Object
215 216 217 |
# File 'lib/graph_weaver/codegen/scalar_type.rb', line 215 def scalar_registry @scalar_registry ||= {} end |
.type_registry ⇒ Object
110 111 112 |
# File 'lib/graph_weaver/codegen/enum_type.rb', line 110 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.
216 217 218 219 220 221 222 223 |
# File 'lib/graph_weaver/codegen.rb', line 216 def self.validate_registration!(schema, kind, name) return if schema.get_type(name) suggestion = defined?(DidYouMean::SpellChecker) && DidYouMean::SpellChecker.new(dictionary: schema.types.keys).correct(name).first hint = suggestion ? " — did you mean '#{suggestion}'?" : "" raise GraphWeaver::Error, "register_#{kind}(#{name.inspect}) matches no type in this schema#{hint}" end |
Instance Method Details
#generate ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 165 166 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 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/graph_weaver/codegen.rb', line 154 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 = {} # 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 root = object_node(root_type, operation.selections, "Result") emit_module(root, variables) end |
#generate_inputs(input_types, enum_types) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/graph_weaver/codegen.rb', line 125 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 |
#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.
110 111 112 |
# File 'lib/graph_weaver/codegen.rb', line 110 def variable_type_names { inputs: @variable_inputs.keys, enums: @variable_enums.keys, mapped: @mapped_enums.keys } end |