Class: GraphWeaver::Codegen

Inherits:
Object
  • Object
show all
Defined in:
lib/graph_weaver/codegen.rb

Overview

Generates plain, statically-typecheckable Ruby from a GraphQL query + schema: nested T::Structs, from_h casting code, and a sig'd execute method. Unlike StructTypes (which builds classes at parse time, visible only at runtime), the output is source on disk — srb tc sees the exact result type of each query.

Supports queries and mutations; plain fields, inline fragments, named fragment spreads (including interface type conditions), union- and interface-typed fields (dispatch on __typename), enums (generated T::Enum), and typed variables (kwargs on execute). Input objects and subscriptions are still open.

Defined Under Namespace

Classes: EnumNode, List, NonNull, ObjectNode, Scalar, UnionNode, VarDef

Constant Summary collapse

SCALARS =

sorbet types for scalars

{
  "ID" => "String",
  "String" => "String",
  "Int" => "Integer",
  "Float" => "Float",
  "Boolean" => "T::Boolean",
  "Date" => "Date",
}.freeze
SCALAR_CASTS =

deserialization for scalars whose wire format differs from their Ruby type; anything absent passes through untouched

{
  "Date" => ->(expr) { "Date.iso8601(#{expr})" },
}.freeze
SCALAR_SERIALIZERS =

the inverse, for serializing variables onto the wire

{
  "Date" => ->(expr) { "#{expr}.iso8601" },
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema:, executor_const:, query:, module_name:) ⇒ Codegen

executor_const names anything responding to execute(query, variables:) whose result to_hs into => ..., "errors" => ... — a Schema class for in-process execution, or an HttpExecutor for a remote endpoint. The generated execute also accepts an executor: override at runtime.



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

def initialize(schema:, executor_const:, query:, module_name:)
  @schema = schema
  @executor_const = executor_const
  @query = query.strip
  @module_name = module_name
end

Class Method Details

.load(schema:, executor_const:, query:, module_name:) ⇒ 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.



222
223
224
225
226
# File 'lib/graph_weaver/codegen.rb', line 222

def self.load(schema:, executor_const:, query:, module_name:)
  source = new(schema:, executor_const:, query:, module_name:).generate
  Object.class_eval(source, "(struct_codegen)", 1)
  Object.const_get(module_name)
end

Instance Method Details

#generateObject



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
274
275
276
277
278
279
280
281
282
# File 'lib/graph_weaver/codegen.rb', line 230

def generate
  errors = @schema.validate(@query)
  if errors.any?
    raise ArgumentError, "invalid query: #{errors.map(&:message).join("; ")}"
  end

  doc = GraphQL.parse(@query)
  @fragments = doc.definitions
    .grep(GraphQL::Language::Nodes::FragmentDefinition)
    .to_h { |fragment| [fragment.name, fragment] }
  @variable_enums = {}

  operation = doc.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).first
  root_type = case operation&.operation_type
  when "query", nil then @schema.query
  when "mutation" then @schema.mutation
  else raise NotImplementedError, "unsupported operation: #{operation.operation_type}"
  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?
    VarDef.new(underscore(var.name), var.name, node, required)
  end

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

  out = []
  out << "# typed: strict"
  out << "# frozen_string_literal: true"
  out << ""
  out << "# Generated by GraphWeaver — do not edit."
  out << ""
  out << "module #{@module_name}"
  out << "  extend T::Sig"
  out << ""
  out << "  QUERY = T.let(<<~'GRAPHQL', String)"
  @query.each_line { |line| out << "    #{line}".rstrip }
  out << "  GRAPHQL"
  out << ""
  @variable_enums.each_value do |enum|
    emit_enum(enum, out, 1)
    out << ""
  end
  emit_nested(root, out, 1)
  out << ""
  emit_execute(out, variables)
  out << "end"

  out.join("\n") + "\n"
end