Back to Fhevm

OpenAPI Architecture

relayer/docs/openapi-architecture.md

0.13.0-013.3 KB
Original Source

OpenAPI Architecture

How the relayer's OpenAPI spec is generated, what design decisions shape it, and how to maintain it.

1. Generation pipeline

Rust types (#[derive(ToSchema)])  ──┐
                                    ├──► ApiDoc (#[derive(OpenApi)])
Handler annotations (#[utoipa::path]) ┘            │
                                                   ▼
                                       build_openapi_doc()
                                          │
                           ┌──────────────┼──────────────┐
                           ▼              ▼              ▼
                   TagGroupModifier  ErrorLabelEnricher  ExampleInjector
                           │              │              │
                           ▼              ▼              ▼
                       utoipa::openapi::OpenApi (in-memory)
                                          │
                                          ▼
                               serde_yaml::to_string()
                                          │
                                          ▼
                   Post-processing (hex quoting only — no YAML AST manipulation)
                                          │
                                          ▼
                                  openapi.yml

1.1 Schema generation

  • 43 #[derive(ToSchema)] across 8 files produce JSON Schema components
  • Key #[schema(...)] attrs used:
    • example — field-level inline examples
    • value_type — override inferred type (e.g. pin V2ErrorLabel on a String field)
    • min_items — array constraints (e.g. ciphertextHandles)
  • serde attrs propagate automatically:
    • rename_all = "camelCase" → property names
    • skip_serializing_if = "Option::is_none" → nullable, not required
    • untaggedoneOf without discriminator
  • Doc comments (///) become description fields in the spec

1.2 Path/operation generation

  • 12 #[utoipa::path] annotations across 5 handler files
  • Each annotation specifies: HTTP method, path, request body, responses(...) with status codes + body types, tag
  • Error examples are auto-generated by ExampleInjector from ERROR_MATRIX (no hand-maintained examples(...) blocks)
  • Thin wrapper functions exist solely for utoipa — real logic lives on impl handler blocks:
rust
#[utoipa::path(post, path = "/v2/input-proof", ...)]
pub async fn input_proof_post_v2<D>(handler: Arc<InputProofHandler<D>>, req: Request<...>) -> impl IntoResponse {
    handler.input_proof_post_v2(req).await
}

1.3 Assembly

ApiDoc in src/http/middleware/openapi.rs:

  • #[derive(OpenApi)] macro wires everything together
  • paths(...) — 12 handler functions (9 v2 + 3 health)
  • components(schemas(...)) — all request/response/error types
  • tags(...) — 6 tags (5 endpoint groups + Health)
  • modifiers(&TagGroupModifier, &ErrorLabelEnricher, &ExampleInjector) — three Modify trait impls (see §1.4)
  • build_openapi_doc() → returns utoipa::openapi::OpenApi
  • openapi_middleware() → serves Redoc UI at /docs

1.4 Modifiers (utoipa Modify trait)

All three modifiers operate on the in-memory OpenApi struct before serialization — no YAML string manipulation.

ModifierPurposeSource
TagGroupModifierInjects x-tagGroups extension for Redoc sidebar (Endpoints vs Operations)src/http/middleware/openapi.rs
ErrorLabelEnricher(1) Replaces <!-- ERROR_LABEL_TABLE --> placeholder with auto-generated error catalog tablesrc/http/middleware/openapi.rs
(2) Appends "Possible error labels: …" to each V2 error response descriptionsrc/http/middleware/openapi.rs
ExampleInjectorAuto-generates named examples for every V2 error response from ERROR_MATRIXsrc/http/middleware/openapi.rs

ErrorLabelEnricher detail

Two-phase modify():

  1. Error reference table — iterates ERROR_LABEL_DEFS, builds a markdown table with columns Label | HTTP | SDK Action, replaces the <!-- ERROR_LABEL_TABLE --> placeholder in info.description
  2. Per-response label summaries — walks openapi.paths.paths, filters to /v2/ endpoints, calls labels_for_status(path, status, is_post) for each error status code, appends a backtick-formatted label list to the response description

ExampleInjector detail

Walks openapi.paths.paths, filters to /v2/ endpoints, and for each error status code:

  • Calls labels_for_status(path, status, is_post) to get the labels
  • Builds a JSON envelope per label with status, error.label, error.message
  • Adds requestId for all errors except POST 429 rate_limited
  • Adds details[] for validation_failed and missing_fields labels
  • Inserts into content.examples as BTreeMap<PascalCaseLabel, Example>

Data sources:

FunctionLocationReturns
labels_for_status(path, …)src/http/openapi/expected_labels.rsVec<(label, example_message, dropdown_summary)> per endpoint + status
label_metadata(label)src/http/openapi/expected_labels.rs(http_status, retryable, sdk_action) per label
all_error_labels()src/http/endpoints/v2/types/error.rsDerived from ERROR_LABEL_DEFS — 16 labels

1.5 Post-processing

src/bin/openapi-export.rs applies one transform after serde_yaml::to_string():

TransformPurposeImplementation
ensure_hex_strings_quoted()Prevent YAML parsers from interpreting 0x… values as hex integersLine-by-line string matching on example:, - , and key: patterns

1.6 CI and runtime

CommandPurpose
make openapi-generateRegenerate openapi.yml from code
make openapi-checkCI drift guard — regenerate + diff
make openapi-lintRedocly lint (npx @redocly/cli lint)
openapi_middleware()Serves Redoc UI at /docs (runtime)

2. Type architecture

Two-layer type system

LayerTypesPurpose
Runtime*StatusResponseJson (optional fields)Handler response construction
OpenAPI-onlyV2StatusQueued, V2StatusFailed, *SucceededStatusResponsePer-status-code schema accuracy

OpenAPI-only types are documentation artifacts — they don't need Deserialize, constructors, or methods.

Error hierarchy

V2ApiError {label, message}
V2ApiErrorWithDetails {label, message, details[]}
        │
        ▼
V2ErrorResponseBody (untagged oneOf)
        │
        ├──► RelayerV2ResponseFailed  (POST 4xx/5xx wrapper)
        └──► V2StatusFailed           (GET 4xx/5xx wrapper)
  • V2ErrorLabel enum — 16 variants, rendered as enum in spec
  • all_error_labels() function — derived from ERROR_LABEL_DEFS, no manual maintenance needed
  • Drift tests (see §3) ensure labels, metadata, and spec examples stay in sync

Known duplication

Three endpoint groups repeat identical shapes:

PatternInstances
{jobId: String}InputProofQueuedResult, PublicDecryptQueuedResult, UserDecryptQueuedResult
{status, requestId, result: …}InputProofPostResponseJson, PublicDecryptPostResponseJson, UserDecryptPostResponseJson

3. Drift detection

Five tests across two modules form a closed verification loop:

TestModuleGuarantees
catalog_labels_are_in_all_error_labelsexpected_labels.rsEvery label returned by labels_for_status() exists in all_error_labels()
all_error_labels_appear_in_catalogexpected_labels.rsEvery all_error_labels() entry is produced by at least one catalog path (exempts gateway_not_reachable — defined but not yet wired)
all_labels_have_metadataexpected_labels.rsEvery all_error_labels() entry has explicit label_metadata() (no default fallthrough)
spec_examples_match_catalogexpected_labels.rsGenerated OpenAPI spec named examples match labels_for_status() exactly
all_constructor_labels_are_in_canonical_listtypes/error.rsEvery constructor on V2ErrorResponseBody uses a label in all_error_labels()

Together these ensure: add a label to ERROR_LABEL_DEFS → must add catalog entry → examples auto-generated. Missing any step fails CI.


4. Pros and cons

What works well

  • Single source of truth — spec derived from code, CI prevents drift
  • Per-status-code schemas — consumers know exact shape per HTTP status
  • Typed error bodies with label enum — machine-readable, no opaque serde_json::Value
  • Closed drift-test loop — catches label, metadata, and example drift at build time
  • Modify trait modifiers — operate on typed OpenApi struct, not fragile YAML strings
  • x-tagGroups — organizes Redoc sidebar into Endpoints vs Operations

What is fragile

  • labels_for_status() is a manual mapping that must stay in sync with handler code. Drift tests check labels exist, but not that the mapping matches actual handler error paths.
  • Two type layers means field changes touch both runtime and OpenAPI-only types.

utoipa limitations

LimitationWorkaround used
No generic type resolutionManual #[schema(value_type = …)] overrides
No ToSchema on type aliasesNewtype wrappers or inline references
Enum field examples need pinning#[schema(value_type = String, example = "…")]

5. Maintenance playbook

Adding a new V2 endpoint

  1. Create request/response types with #[derive(ToSchema)]
  2. Create *SucceededStatusResponse (OpenAPI-only) and *StatusResponseJson (runtime)
  3. Add thin wrapper function with #[utoipa::path] annotation (error examples are auto-generated)
  4. Register path + schemas in ApiDoc (openapi.rs)
  5. Add endpoint to labels_for_status() in expected_labels.rs
  6. make openapi-generate + commit openapi.yml

Adding a new error label

  1. Add entry to ERROR_LABEL_DEFS in expected_labels.rs (all_error_labels() is derived automatically)
  2. Add variant to V2ErrorLabel enum in error.rs
  3. Add constructor method on V2ErrorResponseBody (enforced by all_constructor_labels_are_in_canonical_list test)
  4. Add to ERROR_MATRIX in expected_labels.rs (examples are auto-generated by ExampleInjector)
  5. make openapi-generate

Modifying response fields

  1. Update both the runtime type (*StatusResponseJson) and OpenAPI-only type (*SucceededStatusResponse)
  2. make openapi-generate (error examples are auto-regenerated)

6. Cross-references

ResourcePath
Generated specopenapi.yml
Assembly + modifierssrc/http/middleware/openapi.rs
Error label catalog + metadatasrc/http/openapi/expected_labels.rs
Generation binarysrc/bin/openapi-export.rs
Error hierarchy + labelssrc/http/endpoints/v2/types/error.rs
Representative handler patternsrc/http/endpoints/v2/handlers/input_proof.rs
Representative schema typessrc/http/endpoints/v2/types/input_proof.rs