mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-15 01:55:13 +02:00
mr-668: OpenAPI multi-mode cluster filter (PR 4b/10)
PR 4b of the MR-668 multi-graph server work. In multi mode, the served
`/openapi.json` reports cluster routes (`/graphs/{graph_id}/...`) instead
of the legacy flat protected paths — matching what `build_app` actually
mounts (PR 4a's `Router::nest`). Single mode is unchanged.
Implementation:
- New `server_openapi` branch: when `state.mode()` is `Multi`, call
`nest_paths_under_cluster_prefix(&mut doc)` after `ApiDoc::openapi()`.
- The rewrite consumes `doc.paths.paths`, then for every path-item:
- If the path is in `ALWAYS_FLAT_PATHS` (`/healthz` for now), keep
it flat.
- Otherwise, prefix every operation_id with `cluster_` and reinsert
the item at `/graphs/{graph_id}<original_path>`.
- Single mode hits no extra work — the path map is untouched.
- The static `ApiDoc::openapi()` still emits the flat surface, so
in-process callers (the existing `openapi_json()` helper in tests)
see the unmodified spec.
Why cluster_ prefix on operation IDs: OpenAPI specs require unique
operation_ids across the document. With both flat (single-mode) and
cluster (multi-mode) surfaces ever co-existing in a generated SDK,
the prefix prevents collision. The current served doc only carries
one surface, so the prefix is forward-compat with potential future
dual-surface generation.
Tests: 6 new in `tests/openapi.rs`, all via the `/openapi.json` route
(not the static `ApiDoc::openapi()` helper):
- `multi_mode_openapi_lists_cluster_paths` — every protected path
appears as a cluster variant.
- `multi_mode_openapi_drops_flat_protected_paths` — flat protected
paths are absent.
- `multi_mode_openapi_keeps_healthz_flat` — `/healthz` survives.
- `multi_mode_openapi_prefixes_operation_ids_with_cluster` — every
cluster operation_id starts with `cluster_`.
- `multi_mode_operation_ids_are_unique` — no operation_id collisions.
- `single_mode_openapi_unchanged_by_cluster_filter` — single mode
still emits the legacy flat surface (regression).
New test helper `app_for_multi_mode(graph_ids)` exercises the new
`AppState::new_multi` constructor from PR 4a — first user of multi-mode
construction outside of unit tests.
Result: 66 openapi tests + 57 server integration tests + 74 lib tests
= 197 green. No regression in the existing OpenAPI drift check
(`openapi_spec_is_up_to_date` still validates the static flat surface
matches the committed openapi.json).
LOC: +67 in lib.rs (rewrite logic), +219 in tests/openapi.rs (test
suite + helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
57f5f191ab
commit
ecf01ef3fe
2 changed files with 286 additions and 0 deletions
|
|
@ -889,9 +889,76 @@ async fn server_openapi(State(state): State<AppState>) -> Json<utoipa::openapi::
|
|||
if !state.requires_bearer_auth() {
|
||||
strip_security(&mut doc);
|
||||
}
|
||||
// MR-668 PR 4b: in multi mode, the protected routes live under
|
||||
// `/graphs/{graph_id}/...`. Rewrite the doc so the spec matches
|
||||
// the routes the router actually serves. Public paths (`/healthz`)
|
||||
// stay flat in both modes.
|
||||
if matches!(state.mode(), ServerMode::Multi { .. }) {
|
||||
nest_paths_under_cluster_prefix(&mut doc);
|
||||
}
|
||||
Json(doc)
|
||||
}
|
||||
|
||||
/// Path prefix used to namespace per-graph routes in multi mode.
|
||||
/// Kept in sync with the `Router::nest(...)` invocation in `build_app`.
|
||||
const CLUSTER_PATH_PREFIX: &str = "/graphs/{graph_id}";
|
||||
|
||||
/// Operation-id prefix applied to every cloned cluster operation.
|
||||
/// Decision 7 in the implementation plan — keeps operation IDs unique
|
||||
/// across the spec when both flat and nested variants ever appear in
|
||||
/// the same generation pass.
|
||||
const CLUSTER_OPERATION_ID_PREFIX: &str = "cluster_";
|
||||
|
||||
/// Paths that stay flat in every server mode (public, no per-graph
|
||||
/// dependency). Update this list when adding new always-public endpoints.
|
||||
const ALWAYS_FLAT_PATHS: &[&str] = &["/healthz"];
|
||||
|
||||
/// In multi-mode `server_openapi`, every protected path-item is
|
||||
/// reattached under the cluster prefix. Operation IDs gain the
|
||||
/// `cluster_` prefix so SDK generators don't collide if/when both
|
||||
/// surfaces are merged. The `{graph_id}` URL placeholder is left
|
||||
/// implicit in the path; consuming clients see it as a standard
|
||||
/// OpenAPI path parameter.
|
||||
///
|
||||
/// Removing the flat protected paths matches the runtime router —
|
||||
/// in multi mode, requests to `/snapshot` etc. return 404, so the
|
||||
/// spec must agree.
|
||||
fn nest_paths_under_cluster_prefix(doc: &mut utoipa::openapi::OpenApi) {
|
||||
let original = std::mem::take(&mut doc.paths.paths);
|
||||
let mut rewritten = std::collections::BTreeMap::new();
|
||||
for (path, mut item) in original {
|
||||
if ALWAYS_FLAT_PATHS.contains(&path.as_str()) {
|
||||
rewritten.insert(path, item);
|
||||
continue;
|
||||
}
|
||||
rename_operation_ids(&mut item, CLUSTER_OPERATION_ID_PREFIX);
|
||||
let new_path = format!("{CLUSTER_PATH_PREFIX}{path}");
|
||||
rewritten.insert(new_path, item);
|
||||
}
|
||||
doc.paths.paths = rewritten;
|
||||
}
|
||||
|
||||
/// Prefix every operation_id in this PathItem with `prefix`.
|
||||
fn rename_operation_ids(item: &mut utoipa::openapi::PathItem, prefix: &str) {
|
||||
for op in [
|
||||
item.get.as_mut(),
|
||||
item.post.as_mut(),
|
||||
item.put.as_mut(),
|
||||
item.delete.as_mut(),
|
||||
item.options.as_mut(),
|
||||
item.head.as_mut(),
|
||||
item.patch.as_mut(),
|
||||
item.trace.as_mut(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if let Some(id) = op.operation_id.as_deref() {
|
||||
op.operation_id = Some(format!("{prefix}{id}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_security(doc: &mut utoipa::openapi::OpenApi) {
|
||||
if let Some(components) = doc.components.as_mut() {
|
||||
components.security_schemes.clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue