relayer/docs/openapi-architecture.md
How the relayer's OpenAPI spec is generated, what design decisions shape it, and how to maintain it.
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
#[derive(ToSchema)] across 8 files produce JSON Schema components#[schema(...)] attrs used:
example — field-level inline examplesvalue_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 namesskip_serializing_if = "Option::is_none" → nullable, not requireduntagged → oneOf without discriminator///) become description fields in the spec#[utoipa::path] annotations across 5 handler filesresponses(...) with status codes + body types, tagExampleInjector from ERROR_MATRIX (no hand-maintained examples(...) blocks)impl handler blocks:#[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
}
ApiDoc in src/http/middleware/openapi.rs:
#[derive(OpenApi)] macro wires everything togetherpaths(...) — 12 handler functions (9 v2 + 3 health)components(schemas(...)) — all request/response/error typestags(...) — 6 tags (5 endpoint groups + Health)modifiers(&TagGroupModifier, &ErrorLabelEnricher, &ExampleInjector) — three Modify trait impls (see §1.4)build_openapi_doc() → returns utoipa::openapi::OpenApiopenapi_middleware() → serves Redoc UI at /docsModify trait)All three modifiers operate on the in-memory OpenApi struct before serialization — no YAML string manipulation.
| Modifier | Purpose | Source |
|---|---|---|
TagGroupModifier | Injects 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 table | src/http/middleware/openapi.rs |
| (2) Appends "Possible error labels: …" to each V2 error response description | src/http/middleware/openapi.rs | |
ExampleInjector | Auto-generates named examples for every V2 error response from ERROR_MATRIX | src/http/middleware/openapi.rs |
Two-phase modify():
ERROR_LABEL_DEFS, builds a markdown table with columns Label | HTTP | SDK Action, replaces the <!-- ERROR_LABEL_TABLE --> placeholder in info.descriptionopenapi.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 descriptionWalks openapi.paths.paths, filters to /v2/ endpoints, and for each error status code:
labels_for_status(path, status, is_post) to get the labelsstatus, error.label, error.messagerequestId for all errors except POST 429 rate_limiteddetails[] for validation_failed and missing_fields labelscontent.examples as BTreeMap<PascalCaseLabel, Example>Data sources:
| Function | Location | Returns |
|---|---|---|
labels_for_status(path, …) | src/http/openapi/expected_labels.rs | Vec<(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.rs | Derived from ERROR_LABEL_DEFS — 16 labels |
src/bin/openapi-export.rs applies one transform after serde_yaml::to_string():
| Transform | Purpose | Implementation |
|---|---|---|
ensure_hex_strings_quoted() | Prevent YAML parsers from interpreting 0x… values as hex integers | Line-by-line string matching on example:, - , and key: patterns |
| Command | Purpose |
|---|---|
make openapi-generate | Regenerate openapi.yml from code |
make openapi-check | CI drift guard — regenerate + diff |
make openapi-lint | Redocly lint (npx @redocly/cli lint) |
openapi_middleware() | Serves Redoc UI at /docs (runtime) |
| Layer | Types | Purpose |
|---|---|---|
| Runtime | *StatusResponseJson (optional fields) | Handler response construction |
| OpenAPI-only | V2StatusQueued, V2StatusFailed, *SucceededStatusResponse | Per-status-code schema accuracy |
OpenAPI-only types are documentation artifacts — they don't need Deserialize, constructors, or methods.
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 specall_error_labels() function — derived from ERROR_LABEL_DEFS, no manual maintenance neededThree endpoint groups repeat identical shapes:
| Pattern | Instances |
|---|---|
{jobId: String} | InputProofQueuedResult, PublicDecryptQueuedResult, UserDecryptQueuedResult |
{status, requestId, result: …} | InputProofPostResponseJson, PublicDecryptPostResponseJson, UserDecryptPostResponseJson |
Five tests across two modules form a closed verification loop:
| Test | Module | Guarantees |
|---|---|---|
catalog_labels_are_in_all_error_labels | expected_labels.rs | Every label returned by labels_for_status() exists in all_error_labels() |
all_error_labels_appear_in_catalog | expected_labels.rs | Every all_error_labels() entry is produced by at least one catalog path (exempts gateway_not_reachable — defined but not yet wired) |
all_labels_have_metadata | expected_labels.rs | Every all_error_labels() entry has explicit label_metadata() (no default fallthrough) |
spec_examples_match_catalog | expected_labels.rs | Generated OpenAPI spec named examples match labels_for_status() exactly |
all_constructor_labels_are_in_canonical_list | types/error.rs | Every 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.
serde_json::ValueModify trait modifiers — operate on typed OpenApi struct, not fragile YAML stringsx-tagGroups — organizes Redoc sidebar into Endpoints vs Operationslabels_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.| Limitation | Workaround used |
|---|---|
| No generic type resolution | Manual #[schema(value_type = …)] overrides |
No ToSchema on type aliases | Newtype wrappers or inline references |
| Enum field examples need pinning | #[schema(value_type = String, example = "…")] |
#[derive(ToSchema)]*SucceededStatusResponse (OpenAPI-only) and *StatusResponseJson (runtime)#[utoipa::path] annotation (error examples are auto-generated)ApiDoc (openapi.rs)labels_for_status() in expected_labels.rsmake openapi-generate + commit openapi.ymlERROR_LABEL_DEFS in expected_labels.rs (all_error_labels() is derived automatically)V2ErrorLabel enum in error.rsV2ErrorResponseBody (enforced by all_constructor_labels_are_in_canonical_list test)ERROR_MATRIX in expected_labels.rs (examples are auto-generated by ExampleInjector)make openapi-generate*StatusResponseJson) and OpenAPI-only type (*SucceededStatusResponse)make openapi-generate (error examples are auto-regenerated)| Resource | Path |
|---|---|
| Generated spec | openapi.yml |
| Assembly + modifiers | src/http/middleware/openapi.rs |
| Error label catalog + metadata | src/http/openapi/expected_labels.rs |
| Generation binary | src/bin/openapi-export.rs |
| Error hierarchy + labels | src/http/endpoints/v2/types/error.rs |
| Representative handler pattern | src/http/endpoints/v2/handlers/input_proof.rs |
| Representative schema types | src/http/endpoints/v2/types/input_proof.rs |