GraphWeaver

Gem codecov

A typed GraphQL client for Ruby, built for federation, extensibility, Sorbet, and testing.

GraphWeaver generates # typed: strict Ruby from your queries: nested T::Structs, casting code, and a typed execute — so srb tc sees the exact shape of every query result, and a typo'd field is a static error, not a runtime surprise.

# queries/person.graphql
query($id: ID!) {
  person(id: $id) {
    name
    birthday
    pets { name }
  }
}
result = PersonQuery.execute!(id: "1")   # typed result, or raises on errors (execute returns an envelope)

result.person&.name       # => "Daniel" (typed String)
result.person&.birthday   # => Date (custom scalars deserialize)
result.person&.nmae       # => srb tc: Method `nmae` does not exist

New here? The getting started guide walks the production setup end to end — initializer, codegen, fakes, CI. Or run the examples, smallest first: examples/countries.rb (public API, no auth, all dynamic), examples/rick_and_morty.rb (filtering, pagination, a block-built type helper), and examples/github/run.rb (auth + checked-in generated modules; it stars this repo ⭐ and introduces you to your fellow stargazers).

Features

  • Queries and mutations with typed variable kwargs — enums as T::Enums, input objects as T::Structs, required vs optional falling out of nullability and defaults
  • Fragments (inline, named, type conditions), unions and interfaces (member structs, __typename dispatch), custom scalars (pluggable registry), @skip/@include nullability
  • Any schema source: live schema class, introspection JSON, or SDL — including Apollo Federation supergraph SDL; introspect live endpoints with caching
  • Any transport: in-process schema execution, the zero-dependency HTTP executor, or Faraday with your own middleware — plus a composable Retry (exponential/linear/custom backoff, jitter, retry-by-error-class or GraphQL code) — swap per call with executor:
  • Structured errors: a typed response envelope (partial data + extensions survive), an error hierarchy split by failure site, field-level reports with entity ids, and schema_stale? detection — every error dual-surfaced as a human message plus JSON-ready #to_h
  • Testing built in: schema-correct fakes, failure simulation, record/replay cassettes with anonymization, rspec integration
  • Dynamic mode for development: GraphWeaver.parse(...) generates and evals on the fly, no build step

Usage

Three ways to run a query — pick by context:

Context Use
Production checked-in codegen (rake graph_weaver:generate) — reviewed, srb tc-checked
Development, consoles client.parse / client.load_queries! — no build step
Scripts, one-offs client.execute! — no module at all

The production path assembled is the getting started; the pieces:

require "graph_weaver"

# a client for one server: transport (Faraday when loaded), auth, and a
# lazily introspected schema. The first argument is a url or any schema
# source — a live schema class, or a .json/.graphql dump
api = GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"], cache: true)

# make it the app default — generated modules execute through it
GraphWeaver.client = api

# generate checked-in typed modules (rake graph_weaver:generate, or directly)
source = GraphWeaver::Codegen.generate(
  schema: api.schema,
  query: File.read("queries/person.graphql"),
  module_name: "PersonQuery",
)
File.write("app/queries/person_query.rb", source)

# at runtime
PersonQuery.execute(id: "1")                        # via GraphWeaver.client
PersonQuery.execute(other_client, id: "1")          # or per call

Module names derive from the operation name (query GetPersonGetPerson) or, for parse on a .graphql file, from the file name; pass module_name:/name: to override. Pass client: (a constant) to bake a default client into the generated module. Prefer Faraday? It's opt-in (gem "faraday"), and the client picks it up when loaded — middleware blocks and ready connections in transports.

In development, skip the build step entirely — modules from client.parse carry the client's transport, no global wiring needed:

# parse a query into a typed module on the fly — a .graphql path or a raw string
PersonQuery = api.parse("queries/person.graphql")
PersonQuery.execute(id: "1")

# or every query file at once (queries_path convention), named like generation would
api.load_queries!

# or one-shot, no module at all — variables are plain kwargs
api.execute!("query($id: ID!) { person(id: $id) { name } }", id: "1")

Dig deeper

  • Getting started — the production path in Rails, step by step: initializer, rake tasks, fakes, CI, Sorbet or not
  • Generated modules — module anatomy, typed variables (enums, input objects), fragments/unions/interfaces, @skip/@include, naming, clients, dynamic mode
  • Against a real API — the exploratory tour: introspect a live endpoint (GitHub end to end), dynamic mode, schema caching
  • Transports — clients, the execute contract, Faraday, retries and backoff
  • Custom scalars — the registry: codec inference, requires, input coercion
  • Errors — the Response envelope, the error hierarchy, field-level reports with entity ids, stale-schema detection
  • Logging — point GraphWeaver.logger at any Logger: wire traffic at debug, introspection/cache/codegen at info, errors at warn
  • Testing — schema-correct fakes, failure simulation, rspec integration
  • Cassettes — capture and replay real API responses; anonymized recording (GRAPHWEAVER_RECORD=1, rake tasks)

Installation

# Gemfile
gem "graph_weaver"

or

gem install graph_weaver

Development

  • make check — regenerate spec fixtures, run specs, typecheck
  • make integration — one-off checks against the live GitHub and Countries APIs