v0.2.1 (2026-07-13)
- Conventional paths are appendable lists: queries_paths /
generated_paths (singular accessors read the first entry, so existing
config keeps working); load_generated!, Client#load_queries!, and the
Railtie walk every entry — append spec/support/graphql/* from a spec
helper to load test-only queries. Entries may be globs, and the
generated default includes app/graphql/*/generated so per-schema
layouts auto-load
- inputs_module derives from the output path: multi-schema layouts name
each schema's module after its directory
(app/graphql/github/generated -> GithubInputs), the conventional
layout keeps GraphQLInputs; GraphWeaver.inputs_module= and
generate!(inputs_module:) still override
- Shared types split one-file-per-type: generated/inputs/ holds each
input struct/enum in its own small file (PokeAPI: 573 files, median
24 lines vs one 11.5k-line blob) with inputs.rb as the manifest
(forward declarations make load order irrelevant); regeneration
prunes files for types the schema dropped, verify flags strays;
generate!/verify take inputs_module: per invocation (multi-schema
apps generate into different modules)
- Shared input types: generate! emits every variable type (input
structs + their enums + mapped-enum tables) ONCE per schema into
generated/inputs.rb (module GraphQLInputs; GraphWeaver.inputs_module=
renames, shared_inputs: false opts out), with query modules aliasing
only what their own surface references — AdoptQuery::AdoptionInput
keeps working and shared types gain one identity across modules.
Three filtered Hasura queries: 34,684 lines inline -> 11,754 shared
(~90 lines per query module)
- BREAKING (vs 0.2.0): auto_fake is opt-in again — require
"graph_weaver/rspec" no longer swaps every example onto a fake;
set config.auto_fake = true explicitly (the schema still auto-locates
once you do). Less magic, no unexpected behavior
- Generated input structs are table-driven: typed consts + a per-field
FIELDS table (conversions as lambdas) interpreted by the
GraphWeaver::InputStruct runtime, replacing unrolled
serialize/coerce/value_at per struct — a bool_exp-heavy PokeAPI module
shrinks 29k -> 11.5k lines (-60%) with identical behavior (nil
omission, wire-value enums, nested/recursive coercion, spellchecked
unknown keys all covered by the existing suite)
- Internal: Node base class for the codegen IR protocol; module
assembly moved from Codegen#generate into Emit#emit_module
(byte-identical output)
v0.2.0 (2026-07-12)
- Cleanup pass (staff-engineer review): scalar registrations get the
same typo validation as enums/types; cassette replay stops recomputing
its key per entry; dependency-order DFS uses hash bookkeeping (big
bool_exp graphs); require/vocabulary residue swept; the vestigial
graph_weaver/testing/rspec shim removed
- BREAKING: "client" replaces "executor" across the whole surface.
Generated modules: the per-call override is an optional POSITIONAL
first argument — PersonQuery.execute(github, id: "1") — so variables
own the entire kwarg namespace and NOTHING is reserved (a $client or
$executor variable is fine; only Ruby keywords refuse); per-module is
MyQuery.client=, the baked param is client:. GraphWeaver.executor= is
gone — GraphWeaver.client= is the one ambient slot (auto_fake swaps
it per example; explicit clients are self-contained and never see it).
Client#executor is now Client#transport (transport: to bring your
own); SchemaLoader.introspect/stale? speak transport. Renames:
FakeExecutor => Testing::FakeClient, SequenceExecutor =>
Testing::Sequence, RetryExecutor => GraphWeaver::Retry,
Recording/ReplayExecutor => Recorder/Replayer, Cassette.use(client:)
- Live federation integration: two Ruby subgraphs (apollo-federation
gem) composed and routed by a real Apollo gateway (node harness under
spec/support/federation), with GraphWeaver introspecting through the
router and executing a query stitched across BOTH subgraphs — part of
make integration. Complements the existing supergraph-SDL codegen spec
- graphql-over-http: a non-2xx response carrying a GraphQL errors body
(Apollo Server/Router send request errors as 4xx JSON) flows into the
Response envelope so QueryError sees the structured errors; only
non-GraphQL bodies (proxy pages) raise ServerError
- Fix: input-struct serialize used bare locals (result/value) that a
same-named prop silently shadowed — a field named "result" dropped
its value onto the wrong target; generated locals now wear the
reserved __gw prefix (GraphQL reserves __-names, so no collision is
possible)
- Input fields and variables whose Ruby name would be a keyword
(nil/def/end/...), a generated method (serialize/to_h), or the
reserved executor kwarg now refuse at generation with a pointed
error instead of emitting broken code
- Non-JSON 200 bodies (proxy error pages) classify as ServerError, and
unserializable variables (NaN/Infinity) raise GraphWeaver::Error —
raw JSON::* errors no longer escape the umbrella
- Transports redact on inspect/to_s (class + url only) — Authorization
headers can't leak through logs or exception dumps
- Narrowed
... on X selections require at least one unconditional
field: with every field behind @skip/@include, a matching response is
{} — byte-identical to a non-match — so generation refuses rather
than silently dropping real matches to nil
- Integration spec against Hasura's PokeAPI: snake_case codegen,
recursive bool_exp variable filtering, untyped jsonb pass-through
(make integration)
- BREAKING: ValidationError now descends from GraphWeaver::Error (was
ArgumentError) — one
rescue GraphWeaver::Error catches everything
- Input-struct .coerce raises on unknown hash keys with a spellchecked
hint — a typo'd filter key no longer silently drops off the wire
- Client registrations (register_type/register_enum) validate at the
call site when the schema is already loaded; lazy clients still
validate at generation
- Unregistered custom scalars emit bare T.untyped (not
T.nilable(T.untyped), an srb tc error under typed: strict)
- Wire log lines carry [req N OperationName] tags; long queries
(introspection) truncate at debug
- Logging: GraphWeaver.logger (any stdlib-compatible Logger; Rails.logger
auto-wired by the railtie) — wire traffic + timings at debug,
introspection/cache/codegen at info, every raised error at warn
- Recursive input types generate — self- and mutually-referential inputs
(Hasura's bool_exp filter surface) emit dependency-ordered structs with
runtime forward declarations for cycles, so variable-driven Hasura
filtering works; previously raised "recursive input type"
- Fix: snake_case GraphQL type names (Hasura, PostGraphile) camelize
into valid Ruby constants — pokemon_v2_pokemon => PokemonV2Pokemon
(previously generated a SyntaxError); wire names (__typename dispatch,
registry keys) are untouched
- Everything raised is rescuable: unparseable queries wrap as
ValidationError (GraphQL::ParseError no longer leaks), and internal
NotImplementedError raises (recursive inputs, unsupported kinds,
subscriptions) became GraphWeaver::Error
- Transport::HTTP takes open_timeout:/read_timeout: (defaults 10s/30s);
timeouts surface as retriable TransportError
- Transport::HTTP reuses its connection (keep-alive, mutex-serialized,
keep_alive_timeout: for the idle window); any failure drops the socket
so the next call starts fresh
- GraphQLError#code also reads a top-level "type" (GitHub's dialect:
NOT_FOUND, FORBIDDEN) when extensions.code is absent
- Typo'd client-scoped registrations raise at generation with a
spellchecked hint (register_type("Pett") => "did you mean 'Pet'?")
instead of silently no-oping
- Abstract selections narrow: __typename is only required when the
selection varies by concrete type. Interface-level-fields-only
selections generate one shared struct (no dispatch); a single
... on X condition generates X's struct, always nilable — a
non-matching runtime type casts to nil, so narrowing doubles as
filtering
- Zero-config rspec: require "graph_weaver/rspec" now defaults
auto_fake on and auto-locates the schema from the committed dump
(config.schema= / config.auto_fake = false to override) — one line is
the whole test setup in a conventional app
- examples/: runnable demos, all directly executable — countries.rb
(public API, no auth, all dynamic), rick_and_morty.rb (filtered
search, pagination, a block-built type helper), and github/ (auth,
checked-in generated modules; stars the repo ⭐ then tours the
stargazers, their top repos, and what else they've starred); excluded
from the gem package
- Fix: requires: now load before codec probing, so inference sees
methods the required file provides — register_scalar("DateTime", Time,
requires: "time") correctly infers Time.parse in a fresh process
(previously the cast was silently skipped unless "time" was already
loaded)
- docs/quickstart.md renamed to docs/getting_started.md
- Rails Railtie: the graph_weaver:* rake tasks self-register (no
Rakefile edit) and depend on :environment, and generated modules load
at boot (after initializers) when generated_path exists; outside
Rails, require "graph_weaver/tasks" and call load_generated! as before
- BREAKING (vs 0.1.0): reset_scalars! lost its coerce: flavor —
GraphWeaver.auto_coerce = true is the one way to default-coerce
(broader: convertible built-ins AND full cast/serialize scalars,
resolved lazily, per-registration coerce: still wins)
- GraphWeaver.client= — the blessed global wiring: assign the app's
default client and generated modules resolve through it (per call ->
per module -> baked -> executor= -> client). executor= stays as the
low-level override, so test fakes still win
- Enum mappings: register_enum("Species", PetKind) (+ bulk
register_enums, client-scoped variants) — generated code speaks YOUR
T::Enum, with the wire mapping inferred by name, pinned via map:,
exhaustiveness-checked at generation (fails naming gaps), and
fallback: to absorb unknown wire values on cast (inputs stay strict);
translation tables emitted into the source (X_FROM_WIRE / X_TO_WIRE)
- Type helpers: register_type("Pet", PetHelpers) (global or
client-scoped, additive) — app-owned modules included into every
struct generated from that GraphQL type, so derived values live as
methods beside the honest wire data and srb tc checks them against
each query's selection. Or build the mixin inline with a block
(module_eval'd into an auto-named GraphWeaver::TypeHelpers constant —
quick decoration, invisible to srb tc)
- BREAKING (vs 0.1.0): register_scalar takes the type positionally —
register_scalar("Money", Money, requires: ...) — matching the new
registrars: the GraphQL name + your Ruby type up front, options as
kwargs
- GraphWeaver::Client — transport, schema, and scalars for one server in
one object: GraphWeaver.new(url_or_schema) takes a url (transport
built, schema introspected lazily per cache:/ttl:) or a schema source
(live class — also the in-process executor — or a path/SDL/dump);
#parse and #execute/#execute! bind the implicit schema + transport;
#register_scalar scopes scalar mappings to the client (overlaying the
global registry), so two servers can disagree about a scalar type
- BREAKING: GraphWeaver.connect removed — GraphWeaver.new(url) replaces
it (wire generated modules with GraphWeaver.executor = client.executor)
- BREAKING: the one-shots are now GraphWeaver.execute(url_or_schema,
query, **variables) / execute! — Client#execute on a throwaway client;
variables are plain kwargs, as on a generated module
- Client#load_queries! — parse every query file into modules named like
generation would name them (reloadable; namespace: to scope): the
no-build-step analog of generate! + load_generated!
- Introspected schema dumps record provenance (source url + timestamp):
a parsable SDL header comment, a "graph_weaver" sibling key in JSON —
read it back with SchemaLoader.provenance(path), check drift with
SchemaLoader.stale?(path) or rake graph_weaver:schema:verify, rewrite
with rake graph_weaver:schema:refresh (GRAPHWEAVER_AUTH for tokens)
- generate!/verify_generated!/rake auto-locate the schema dump at
schema_path in any supported format; SchemaLoader.locate is public
- Calling a result field by its camelCase wire name raises a pointed
NoMethodError naming the snake_case prop that does exist
(result.addPet => "use 'add_pet'"), and near-miss typos in either
casing get a spellchecked suggestion (result.addPt => "did you mean
'add_pet'?") — the runtime companion to srb tc's static flag
- BREAKING: an operation whose only variable is a required input object
(the Relay convention) now flattens the input's fields into execute's
kwargs — AdoptQuery.execute!(name:, species:) instead of
execute!(input: ...); multi-variable / nullable-input operations
keep the input: kwarg (struct or hash)
- Enum kwargs accept the T::Enum or its wire value (T.any(Enum, String))
everywhere — variables now match input-hash fields
- BREAKING: HttpExecutor / FaradayExecutor are now Transport::HTTP /
Transport::Faraday, subclasses of the new abstract GraphWeaver::Transport
base, which owns the shared flow (encode, TransportError reclassify,
non-2xx ServerError, parse) — a custom transport just implements
post(body) => [status, body]. Opt-in require moved:
"graph_weaver/faraday_executor" -> "graph_weaver/transport/faraday"
- SchemaLoader.introspect cache: reuses a fresh dump in ANY supported
format before re-introspecting (an existing schema.graphql wins over
writing schema.json), and accepts :json / :graphql / :gql to pick the
format at GraphWeaver.schema_path's location
- rubydoc.info rendering: ship .yardopts (markdown markup, docs/ guides
as extra files) and re-indent docstring examples so code blocks and
backticks render; make docs previews locally
- GraphWeaver.connect(url, auth:, headers:, retries:): one-shot setup —
best transport (Faraday when the app loads it; detection is defined?,
never a require), bearer/verbatim auth, opt-in RetryExecutor wrapping
(true / options Hash; off by default), wired in as the global executor
- Generation workflow: GraphWeaver.generate! (queries dir -> generated
dir), verify_generated! (the freshness guard — raises naming stale
files), load_generated! (factory_bot-style explicit loading), rake
tasks (require "graph_weaver/tasks": graph_weaver:generate / :verify),
all defaulting to configurable conventional paths (queries_path /
generated_path / schema_path)
- GraphWeaver.auto_coerce = true: default input coercion for scalars
without an explicit coerce:, resolved lazily at generation time (no
reset_scalars! ordering dance) — convertible built-ins take their
conversion, cast/serialize pairs take parse-style coercion
- SchemaLoader.introspect cache: true — caches at GraphWeaver.schema_path,
in the format the extension picks: .json (verbatim wire artifact) or
.graphql/.gql (SDL — human-readable, PR-reviewable diffs);
the same dump rake graph_weaver:generate reads
- docs/transports.md: connect, the executor contract, Faraday, retries
- Cassette workflow: GRAPHWEAVER_RECORD=1 / config.record force
re-recording; config.anonymize scrubs responses as they are recorded
(caller sees the anonymized data too, so assertions hold on replay);
rake graph_weaver:cassettes:anonymize; docs/cassettes.md guide
- auto_coerce reaches input-object fields: raw scalar values inside
input hashes coerce via the registry, mutations included
- RetryExecutor: composable retries over any transport — tries:,
exponential/linear/custom backoff with jitter and max clamp,
retry-by-error-class (5xx yes, 4xx no by default; retry_if: override)
and retry-by-GraphQL-code (retry_codes: ["THROTTLED"])
v0.1.0 (2026-07-11)
- Structured errors: execute returns a typed Response envelope (#data/#data!,
#errors, #errors?, #extensions) instead of raising on GraphQL errors, so
partial data and top-level extensions (cost/throttle) survive. Error classes
under GraphWeaver::Error — TransportError (network), ServerError (non-2xx
HTTP, #status/#body), QueryError (#errors/#data/#extensions/#codes),
ValidationError (build-time) — plus a GraphQLError value object with #code.
Transport-error classification is an extensible Set (GraphWeaver.transport_errors
/ register_transport_error): each transport seeds its own network exceptions
and apps can add more (e.g. a connection-pool timeout).
The envelope is a single generic GraphWeaver::Response (no per-query
wrapper class). execute! is the shortcut for execute(...).data! — the typed
result or a raised QueryError — on both generated modules and the one-shot
GraphWeaver.execute!/execute.
BREAKING: module #execute returns Response; use #execute! (or #data!) for
the old raise-or-result behavior. GraphWeaver.execute now returns the
envelope too; GraphWeaver.execute! returns the result.
- GraphWeaver.register_scalar: custom scalar deserialization into rich Ruby
objects. cast/serialize inferred from a class type via paired codecs
(.parse/#to_s or .load/.dump), or given as a Symbol/Proc (:itself opts out);
requires: emits (validated, and require-checked when type: is a class)
requires into generated source — the built-in Date scalar carries
require "date" so Date-using queries are self-contained; coerce: true lets a
variable accept the value or its raw input (coerce: :to_f for a built-in
conversion), casting/converting the latter — reset_scalars!(coerce: true)
reloads the built-ins coercible; built-in scalars pre-registered in one
overridable registry (reset_scalars!/clear_scalars!)
- FaradayExecutor: url, Faraday connection, or middleware block
- GraphWeaver.executor default transport; per-module executor= override
- GraphWeaver.parse and GraphWeaver.execute (dynamic queries)
- Codegen.generate shorthand; executor: takes a constant; module_name
derived from operation or file name
- Error ergonomics: schema_stale? (validation-shaped rejections hint at
regeneration), errors_at(path) + each_error/errors_by_field filtering,
#report (field-keyed rollup with entity ids resolved from partial
data), #to_h across the hierarchy (JSON-ready machine output), and
GraphWeaver::TypeError wrapping cast failures with the failing struct
- SchemaLoader: introspect(executor, cache:, ttl:) fetches schemas from
live endpoints with file caching; load accepts introspection JSON /
SDL content / Hashes as well as paths (cache round-trips)
- GraphWeaver::Testing (require "graph_weaver/testing", or
"graph_weaver/rspec" for the rspec integration): FakeExecutor
fabricates schema-correct castable responses (mode: :faker semantic
values / :literal; overrides by GraphQL name; seeded; list_size /
null_chance), failure simulation (Failure.transport/server/graphql/
throttled/stale_schema, SequenceExecutor for retries, fail_at: with
spec-correct null propagation, corrupt: for derived type mismatches),
cassette record/replay above the transport, and Cassette#anonymize!
(shape-preserving, consistent id mapping). rspec: seed follows
--seed; auto_fake installs a fake executor per example
- one-off integration specs against live GitHub + Countries APIs
(make integration)
- Input objects: INPUT_OBJECT variables generate module-level T::Structs
with serialize (aliased to_h) producing the wire hash; execute kwargs
also accept plain hashes, normalized + type-checked via the generated
.coerce (underscored Symbol/String keys, enums as instances or wire
values, nested inputs as hashes)
- fields under @skip/@include generate nilable regardless of schema
nullability; FakeExecutor honors first/last/limit when sizing lists
- eval hardening for parse: module names must be constant names, and
QUERY heredocs can't be terminated early by block strings
- GraphWeaver::Selection: one shared query-walk (codegen, FakeExecutor,
anonymizer); codegen split into scalar_type / nodes / emit
- docs/: generated_modules, real_world, scalars, errors, testing;
README slimmed to pitch + quickstart
v0.0.1 (2026-07-07)
- voila: typed codegen (T::Structs, T::Enums, typed variable kwargs)
- queries + mutations; fragments, unions, interfaces, enums, custom scalars
- schema sources: live class, introspection JSON, SDL (incl. supergraph)
- pluggable executor: in-process schema or HTTP
- dynamic (no-build) mode for development