Skip to content

refactor(admin): merge resource JSON Schemas into served OpenAPI doc - #310

Merged
moonming merged 3 commits into
mainfrom
refactor/openapi-schema-ref
May 17, 2026
Merged

refactor(admin): merge resource JSON Schemas into served OpenAPI doc#310
moonming merged 3 commits into
mainfrom
refactor/openapi-schema-ref

Conversation

@moonming

Copy link
Copy Markdown
Collaborator

Stacked on #308 (which is stacked on #307). Base will switch to main once #308 merges.

Summary

Cuts the schema-duplication tail in crates/aisix-admin/src/openapi.rs. Resource shapes (Model, ApiKey, ProviderKey, Guardrail, CachePolicy, ObservabilityExporter, RateLimit, Routing) are no longer hand-written inside the OpenAPI document — they are pulled at compile time from the canonical files schemas/resources/*.schema.json (generated by dump-schema in #308, drift-guarded by CI in #309) and merged into the served spec at first request.

Before / After

Before After
Sources of truth for resource shape Rust struct + inline OpenAPI + AISIX-Cloud + dashboard form Rust struct only
Guardrail kind discrimination additionalProperties: true + comment oneOf with proper sub-schemas
Provider enum Hand-listed 6 variants Generated from Rust enum
Adapter enum (#302 Phase A) Not present Generated, kebab-case
Nested types (ParamConstraints, TelemetryTags, etc.) Missing Hoisted to components.schemas
Component schema count 17 33

Implementation sketch

const RESOURCE_SCHEMAS: &[(&str, &str)] = &[
    ("Model",       include_str!("../../../schemas/resources/model.schema.json")),
    ("ApiKey",      include_str!("../../../schemas/resources/api_key.schema.json")),
    // ... 8 total
];

fn merged_openapi() -> &'static str {
    static CELL: OnceLock<String> = OnceLock::new();
    CELL.get_or_init(|| {
        let mut doc: Value = serde_json::from_str(OPENAPI_JSON_BASE).unwrap();
        for (name, raw) in RESOURCE_SCHEMAS {
            let mut schema: Value = serde_json::from_str(raw).unwrap();
            // 1. hoist `definitions/*` into top-level `components.schemas/*`
            // 2. strip `$schema` / `title`
            // 3. place under `components.schemas.<name>`
            ...
        }
        // rewrite `$ref: #/definitions/X` → `$ref: #/components/schemas/X`
        rewrite_definitions_refs(&mut doc);
        serde_json::to_string(&doc).unwrap()
    })
}

Full diff: +148 / −141 across the one file.

Verification

  • cargo check -p aisix-admin clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all -- --check clean
  • cargo test -p aisix-admin --lib — all 7 openapi tests pass, including openapi_apikey_schema_excludes_max_budget_usd (regression test against managed-mode field leak)
  • External reference audit: parsed the merged doc, found 43 $ref references across 32 distinct targets, 0 unresolved. Every reference inside the served spec is locally resolvable — Scalar UI never needs to fetch anything external.

Behavior change

The served /admin/openapi.json body is materially different (more precise types, more nested definitions surfaced). Wire path / status codes / auth / error envelope are unchanged. Scalar UI keeps working at /admin/openapi-scalar unchanged.

What this does NOT do

  • Does not upgrade schemars 0.8 → 1.x. Output stays draft-07; OpenAPI 3.1 tolerates this for inline schemas, but a future upgrade is worth tracking (separate issue).
  • Does not touch the path definitions (/admin/v1/*). Those are still hand-maintained in OPENAPI_JSON_BASE (formerly OPENAPI_JSON).
  • Does not change the proxy /v1/chat/completions surface — out of scope per existing module-level comment ("operators refer to OpenAI's published spec").

Stack

Builds on:

Merge order: #307#308#309 → this PR.

Refs #304 (#1).

Copilot AI review requested due to automatic review settings May 17, 2026 01:28
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 1 second before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: acd781c4-964a-4f2c-ae42-7ea99c6003a7

📥 Commits

Reviewing files that changed from the base of the PR and between 36ed90b and 13bcf9c.

📒 Files selected for processing (3)
  • Dockerfile
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

moonming added a commit that referenced this pull request May 17, 2026
`merged_openapi` previously parsed and merged the embedded resource
schemas on the first `/admin/openapi.json` request. That delayed any
panic from a corrupt schema fragment until well after boot — a worse
ops failure mode than crashing immediately on startup, especially
since the panic is captured by axum's error handling and surfaces as
a 500 to whoever happens to hit Scalar first.

Move the init call up: `build_router` now calls `openapi::merged_openapi()`
once at construction time, before any request can land. The result is
cached in the same `OnceLock` so the handler still does a free lookup.

Visibility on `merged_openapi` flips from private to `pub(crate)` to
make the pre-warm callable from `lib.rs`; no other surface change.

Surfaced by independent audit of #310.

Refs #304 (#1).
@moonming
moonming changed the base branch from chore/dump-schema-binary to main May 17, 2026 01:49
@moonming moonming closed this May 17, 2026
@moonming moonming reopened this May 17, 2026
moonming added 2 commits May 17, 2026 09:55
The hand-written OpenAPI 3.1 document in `crates/aisix-admin/src/openapi.rs`
previously inlined its own copy of every resource schema (`Model`,
`ApiKey`, `ProviderKey`, `Guardrail`, `CachePolicy`,
`ObservabilityExporter`, `RateLimit`, `Routing`, plus the nested
`ModelCost` / `BackgroundModelCheck`). That left three places to keep
in sync whenever a resource field changed: the Rust struct, the
inline OpenAPI schema, and the cp-api / dashboard side.

This PR cuts the duplication. The Rust struct is now the single
source of truth; `dump-schema` (PR #308) writes canonical
draft-07 JSON Schemas into `schemas/resources/*.schema.json`; CI
(PR #309) enforces those files match the structs. This commit:

1. Removes the ten inlined resource schemas from `OPENAPI_JSON_BASE`
   (the const formerly named `OPENAPI_JSON`).
2. Embeds the eight canonical schema files at compile time via
   `include_str!` into a new `RESOURCE_SCHEMAS` const.
3. Adds `merged_openapi()` — runs once on first request, parses the
   base spec, parses each embedded schema, hoists `definitions/*`
   into top-level `components.schemas`, rewrites
   `$ref: #/definitions/X` to `$ref: #/components/schemas/X`
   (JSON Schema draft-07 → OpenAPI 3.1), and caches the result in
   an `OnceLock<String>`.
4. Changes `openapi_json()` to serve the merged doc instead of the
   raw `OPENAPI_JSON_BASE`.
5. Updates the three openapi unit tests to parse `merged_openapi()`.

## What this means for `/admin/openapi.json`

The served document keeps the same wrapper schemas (`ModelEntry`,
`ApiKeyEntry`, `ModelStatusView`, `ModelKind`, `RuntimeStatus`,
`SystemTime`, `AdminError`) and gains 16 new top-level component
schemas hoisted from the resource definitions (`Adapter`,
`BedrockConfig`, `CacheBackend`, `CooldownConfig`,
`GuardrailHookPoint`, `KeywordPattern`, `OnAllFilteredPolicy`,
`ParamConstraints`, `Provider`, `RequestOverrides`,
`ResponseOverrides`, `RoutingStrategy`, `RoutingTarget`,
`StreamDoneMarker`, `TelemetryTags`, etc.).

The resource schemas themselves are now precise reflections of the
Rust types — e.g. `Guardrail` uses a proper `oneOf` discriminator on
`kind` instead of the previous flat `additionalProperties: true`
hand-wave; `Provider` lists its 6 variants from the actual enum;
`Adapter` lists the 5 wire-shape kebab-case values from #302
Phase A.

## Verification

- `cargo check -p aisix-admin` clean
- `cargo clippy --workspace --all-targets -- -D warnings` clean
- `cargo fmt --all -- --check` clean
- `cargo test -p aisix-admin --lib` — all 7 openapi tests pass,
  including the regression test
  `openapi_apikey_schema_excludes_max_budget_usd`
- External validation: parsed the merged doc, collected 43 `$ref`
  references across 32 distinct targets, all resolve inside
  `#/components/schemas/*` (0 unresolved)

## Why nested `if let` instead of let-chains

Workspace is on `edition = "2021"`. The merge logic uses one level
of nesting in two spots; not pretty, but `edition = "2024"` is a
separate decision not in this PR's scope.

## Stack

Builds on:
- #307 (JsonSchema derives on resource structs)
- #308 (dump-schema binary + initial schema files)
- #309 (CI drift enforcement)

Merge order: 307 → 308 → 309 → this PR. Base will switch to `main`
once #308 merges.

Refs #304 (#1).
`merged_openapi` previously parsed and merged the embedded resource
schemas on the first `/admin/openapi.json` request. That delayed any
panic from a corrupt schema fragment until well after boot — a worse
ops failure mode than crashing immediately on startup, especially
since the panic is captured by axum's error handling and surfaces as
a 500 to whoever happens to hit Scalar first.

Move the init call up: `build_router` now calls `openapi::merged_openapi()`
once at construction time, before any request can land. The result is
cached in the same `OnceLock` so the handler still does a free lookup.

Visibility on `merged_openapi` flips from private to `pub(crate)` to
make the pre-warm callable from `lib.rs`; no other surface change.

Surfaced by independent audit of #310.

Refs #304 (#1).
Copilot AI review requested due to automatic review settings May 17, 2026 01:56
@moonming
moonming force-pushed the refactor/openapi-schema-ref branch from 54d0f47 to a6505f7 Compare May 17, 2026 01:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

`crates/aisix-admin/src/openapi.rs` uses `include_str!` to embed
every `schemas/resources/*.schema.json` at compile time. The Docker
release stage previously copied only `Cargo.{toml,lock}`,
`rust-toolchain.toml`, `rustfmt.toml`, and `crates/` — `cargo build`
inside the container therefore failed with eight
"couldn't read .../schemas/resources/*.schema.json" errors.

Adds a `COPY schemas ./schemas` line and an inline comment pinning
the dependency between `include_str!` and the docker context.

Surfaced by CI on PR #310 (build job).

Refs #304 (#1).
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.

2 participants