feat(api): topology endpoints (catalog / classify / build)
Stateless, auth-gated routes backing the topology builder UI:
- GET /api/topologies — catalog of the 12 kinds + descriptions + role mix
- POST /api/topologies/classify — infer a kind + metrics from a posted graph
- POST /api/topologies/build — build a canonical graph from {kind, roles}
Add Serialize to cm-topology Classification/GraphMetrics; add ApiError::BadRequest
(400) for invalid build input; add cm-topology dep. 3 integration tests
(catalog/build/auth) green against Postgres; offline build + clippy clean.
No DB or provider yet — running/comparing topologies is a later provider-backed
endpoint. Server redeploy will batch with the ReactFlow UI that consumes these.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b0c122b88d
commit
fa7e5dec5f
@@ -0,0 +1,72 @@
|
||||
//! Topology endpoints: the catalog, structural classification, and building a
|
||||
//! canonical graph from a kind + roles. Stateless (no DB, no provider) — these
|
||||
//! back the topology builder UI. Running/comparing topologies is a later,
|
||||
//! provider-backed endpoint.
|
||||
|
||||
use axum::Json;
|
||||
use cm_topology::{build, classify, heuristics, Classification, TopologyGraph, TopologyKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{ApiError, Authed};
|
||||
|
||||
/// A role and its suggested share of the team.
|
||||
#[derive(Serialize)]
|
||||
pub struct RoleWeight {
|
||||
pub role: String,
|
||||
pub weight: f32,
|
||||
}
|
||||
|
||||
/// One supported topology kind, with its description and default role mix.
|
||||
#[derive(Serialize)]
|
||||
pub struct CatalogEntry {
|
||||
pub kind: TopologyKind,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub role_distribution: Vec<RoleWeight>,
|
||||
}
|
||||
|
||||
/// `GET /api/topologies` — the catalog of supported topology kinds.
|
||||
pub async fn catalog(_auth: Authed) -> Json<Vec<CatalogEntry>> {
|
||||
let entries = TopologyKind::ALL
|
||||
.iter()
|
||||
.map(|&kind| {
|
||||
let h = heuristics(kind);
|
||||
CatalogEntry {
|
||||
kind,
|
||||
name: kind.as_str().to_string(),
|
||||
description: kind.description().to_string(),
|
||||
role_distribution: h
|
||||
.role_distribution
|
||||
.iter()
|
||||
.map(|(role, weight)| RoleWeight {
|
||||
role: (*role).to_string(),
|
||||
weight: *weight,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Json(entries)
|
||||
}
|
||||
|
||||
/// `POST /api/topologies/classify` — infer a topology kind from a graph.
|
||||
pub async fn classify_graph(_auth: Authed, Json(graph): Json<TopologyGraph>) -> Json<Classification> {
|
||||
Json(classify(&graph))
|
||||
}
|
||||
|
||||
/// Request body for building a canonical topology.
|
||||
#[derive(Deserialize)]
|
||||
pub struct BuildRequest {
|
||||
pub kind: TopologyKind,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
/// `POST /api/topologies/build` — build a canonical graph from a kind + roles.
|
||||
pub async fn build_graph(
|
||||
_auth: Authed,
|
||||
Json(req): Json<BuildRequest>,
|
||||
) -> Result<Json<TopologyGraph>, ApiError> {
|
||||
let roles: Vec<&str> = req.roles.iter().map(String::as_str).collect();
|
||||
let graph = build(req.kind, &roles).map_err(|_| ApiError::BadRequest)?;
|
||||
Ok(Json(graph))
|
||||
}
|
||||
Reference in New Issue
Block a user