Skip to content

Adopt graphql_client codegen for typed GraphQL queries - #196

Draft
leynos wants to merge 7 commits into
hyper-graphql-transportfrom
typed-graphql-queries
Draft

Adopt graphql_client codegen for typed GraphQL queries#196
leynos wants to merge 7 commits into
hyper-graphql-transportfrom
typed-graphql-queries

Conversation

@leynos

@leynos leynos commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

This branch delivers PR 3, the final phase of the GitHub API
modernisation governed by
ADR 001:
every GraphQL operation is now a named document under
graphql/,
validated against GitHub's vendored public schema at compile time by
graphql_client codegen. A malformed query fails the build rather than a
runtime request (demonstrated: a deliberate field typo yields
No field named titleTYPO on Issue from cargo check).

Execplan: docs/execplans/adopt-octocrab.md
— PR 3 of its three phases; stacked on #195 (hyper transport), which
stacks on #194 (octocrab REST).

The migration surfaced and fixes a latent production bug: the resolve
thread-lookup query selected PullRequest.reviewComments, a field that
does not exist in GitHub's published schema — vk resolve could never
locate a thread against the live API, and only fully mocked tests kept it
green. The lookup is redesigned onto reviewThreads, matching comments
by fullDatabaseId (the schema deprecates databaseId).

Review walkthrough

  • Start with
    graphql/
    — the vendored schema (72,911 lines, provenance and refresh procedure
    in its README) and one document per operation group.
  • Then
    src/api/client/mod.rs
    run_operation (typed execution returning generated
    ResponseData) and run_operation_as (schema-checked query,
    hand-written deserialization target). The _as escape hatch is the
    load-bearing design move: it preserves documented lenient behaviour
    the generated types cannot express (threads missing isOutdated are
    treated as current) and keeps serde_path_to_error paths
    byte-identical. The string-based surface (run_query, fetch_page,
    paginate_all, Query) is removed, with every characterization
    assertion ported to the shared run_payload core.
  • src/api/client/pagination.rs
    and the CursorVariables trait — typed cursor pagination, written
    test-first, preserving the 1,000-page cap and discard-on-error
    semantics.
  • src/resolve/graphql.rs
    — the redesigned thread lookup, its accepted limitation (a comment
    beyond the first 100 comments of one thread is not found; the same
    class of cap as the old flat query), and the typed
    resolveReviewThread mutation.
  • Wire submodules
    (src/review_threads/wire.rs,
    src/reviews/wire.rs)
    — pure refactor bringing every product source back under the
    400-line limit; public paths preserved via re-exports.

Validation

  • make check-fmt / make lint / make test: pass (215 lib tests plus all integration suites and 11 doctests, 0 failed)
  • cargo test --test e2e -- --ignored e2e_pr_42: pass (transcript replay unchanged)
  • Compile-fail demonstration: field typo in graphql/issue.graphql fails cargo check; reverted
  • Clean-build delta: 17 s post-codegen versus 46 s baseline (schema-parse cost immaterial)
  • make markdownlint / make nixie: pass
  • coderabbit review --agent: completed, zero findings (cumulative diff from main)

Notes

  • Public domain types (ReviewThread, ReviewComment,
    CommentConnection, PageInfo, PullRequestReview, Issue, User)
    are unchanged; generated types stay module-private.
  • Issue-not-found now surfaces as a semantic issue #N not found error
    rather than the previous accidental serde failure (no test pinned the
    old text; recorded in the ExecPlan decision log).
  • src/review_threads/tests.rs (655 lines, tests-only) already exceeded
    the 400-line limit before this branch and is left for a follow-up.

leynos added 7 commits July 9, 2026 11:17
Vendor GitHub's published public schema (72,911 lines, from
https://docs.github.com/public/fpt/schema.docs.graphql) at
`graphql/schema.docs.graphql` with a README recording provenance and
the refresh procedure. Add the `graphql_client` 0.16 dependency
(default features only; its optional reqwest transports stay off).

Clean-build baseline before any codegen derives: 46 s
(`cargo build --all-features` on this machine), recorded for the
ExecPlan's build-time tolerance.
Move the issue lookup, review-thread listing, per-thread comment
paging, reviews listing, PR-for-branch lookup, and the
`resolveReviewThread` mutation onto `graphql_client` 0.16 codegen.
Every operation now lives in a named `.graphql` document under
`graphql/`, validated against the vendored GitHub schema at compile
time — a malformed query fails the build rather than a runtime
request.

Typed execution surface on `GraphQLClient`:

- `run_operation` executes a codegen'd operation and returns its
  generated `ResponseData`;
- `run_operation_as` and `paginate_operation_as` build the
  schema-checked envelope from the generated operation but deserialize
  into hand-written domain structs, preserving lenient behaviour the
  generated types cannot express (notably the documented client-side
  default treating threads missing `isOutdated` as current) and
  keeping `serde_path_to_error` paths byte-identical;
- a `CursorVariables` trait plus `paginate_operation` drive typed
  cursor pagination with the same `MAX_PAGES` cap and discard-on-error
  semantics as `paginate_all`, covered by new red-green rstest units
  against scripted loopback stubs.

A shared `src/api/scalars.rs` maps GitHub's custom scalars
(`DateTime`, `URI`, `HTML`); the alias names must match the schema
scalars, so tightly-scoped `expect` attributes cover the
capitalized-acronym lint.

`src/graphql_queries.rs` is deleted. The string-based `run_query`
retains one production consumer: the resolve path's review-comments
paging query selects a `reviewComments` field that does not exist in
GitHub's published schema — a latent production bug exposed by the
codegen migration (only mocked tests kept it green). Its redesign onto
`reviewThreads` follows in a separate commit.

Issue-not-found now surfaces as a semantic `VkError::BadResponse`
("issue #N not found") rather than the previous accidental serde
failure; no test pinned the old text (recorded in the ExecPlan
decision log).
The resolve path's paging query selected
`PullRequest.reviewComments`, a field that does not exist in GitHub's
published GraphQL schema — `vk resolve` could never locate a thread
against the live API, and only fully mocked tests kept the path green.
Codegen validation exposed the bug the moment the operation moved to a
`.graphql` document.

Replace it with `ThreadForCommentQuery`, which pages
`repository.pullRequest.reviewThreads` (with each thread's first 100
comments) and scans for the requested comment id, returning the owning
thread's GraphQL id. Comments are matched by `fullDatabaseId` — the
schema deprecates `databaseId` in favour of the 64-bit-safe field — via
a new `BigInt` scalar alias carried as a decimal string. The
`resolveReviewThread` mutation also moves to a typed operation.

Behavioural guarantees are preserved: not-found mapping, the
missing-cursor and non-advancing-cursor aborts, and the mockall
fetcher seam with its sequence-based unit tests. Accepted limitation,
documented in the module: a comment beyond the first 100 comments of a
single thread is not found — the same class of cap as the old flat
query's page size.

`src/graphql_queries.rs` remnants are gone; every operation now lives
in a schema-validated document. The string-based query surface
(`run_query`, `fetch_page`, `paginate_all`) is retained deliberately as
a fully-tested raw escape hatch for the planned shared-crate
extraction (see the ExecPlan decision log).
Delete `run_query`, `fetch_page`, `paginate_all`, the `paginate` free
function, and the `Query` newtype now that every operation executes
through the typed codegen path. This corrects the previous commit's
note that the surface would be retained: the removal was completed
with every characterization assertion ported intact (recorded in the
ExecPlan decision log).

The retry, error-detail, and transcript characterization tests now
exercise the shared `run_payload` core directly with identical
assertions and scripted servers; the cursor-capture test moved to the
typed pagination path, asserting the paginator's cursor lands in
`variables.cursor` through the `CursorVariables` impl. The
non-object-variables rejection test is retired rather than ported:
typed `Variables` structs are objects by construction, so the guarded
failure mode cannot exist.

`operation_name` stays: the transport still derives request-context
strings from it.
Extract the codegen derives, envelope structs, `CursorVariables`
impls, and domain-type definitions from `src/review_threads.rs`
(505 lines) and `src/reviews.rs` (425 lines) into
`src/review_threads/wire.rs` and `src/reviews/wire.rs`, bringing every
product source back under the 400-line limit. Public paths are
preserved through `pub use wire::…` re-exports, so no consumer
changes.

Pure refactor: no behaviour or signature changes.
`src/review_threads/tests.rs` (655 lines, tests-only) already exceeded
the limit at HEAD and is left for a follow-up.
Rewrite the design document's networking section for the codegen era:
named operation documents under `graphql/` validated at compile time,
`run_operation` and the `_as` decoding escape hatches, `CursorVariables`
pagination with the 1000-page cap, the scalars module, and the wire
submodule pattern. Update the resolve-threads description to the
`reviewThreads`/`fullDatabaseId` lookup, noting the latent
`reviewComments` bug the migration exposed and the documented
first-100-comments-per-thread limitation. Add the `graphql/` directory
to the repository layout. Record PR 3 milestones, artifacts, and an
interim retrospective in the ExecPlan.
Record the zero-finding CodeRabbit review and the stacked draft PR;
set the plan status to COMPLETE pending review and merges.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f19823a7-382d-4663-8664-f77fae5432fa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch typed-graphql-queries

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant