Skip to main content

featherbit/admin/
routes.rs

1//! Admin API endpoints for route CRUD. Mutations rewrite the in-memory
2//! gateway config and trigger validation + graph recompilation via
3//! `SharedState::reload`.
4
5use std::sync::Arc;
6
7use axum::extract::{Path, State};
8use axum::http::StatusCode;
9use axum::response::IntoResponse;
10use axum::routing::get;
11use axum::{Json, Router};
12
13use crate::config::RouteConfig;
14use crate::state::SharedState;
15
16/// Builds the router for the `/api/routes` endpoints.
17pub fn router() -> Router<Arc<SharedState>> {
18    Router::new()
19        .route("/api/routes", get(list_routes).post(create_route))
20        .route(
21            "/api/routes/{name}",
22            get(get_route).put(update_route).delete(delete_route),
23        )
24}
25
26/// `GET /api/routes` — returns all configured routes as a JSON array.
27async fn list_routes(State(state): State<Arc<SharedState>>) -> impl IntoResponse {
28    let gw = state.gateway.read().await;
29    Json(&gw.routes).into_response()
30}
31
32/// `GET /api/routes/{name}` — returns the named route as JSON.
33///
34/// Errors: `404 Not Found` if no route with that name exists.
35async fn get_route(
36    State(state): State<Arc<SharedState>>,
37    Path(name): Path<String>,
38) -> impl IntoResponse {
39    let gw = state.gateway.read().await;
40    match gw.routes.iter().find(|r| r.name == name) {
41        Some(route) => Json(route).into_response(),
42        None => (
43            StatusCode::NOT_FOUND,
44            Json(serde_json::json!({"error": "not_found"})),
45        )
46            .into_response(),
47    }
48}
49
50/// `POST /api/routes` — creates a new route from the JSON body, then
51/// revalidates and recompiles all route graphs. Returns `201 Created` with
52/// `{"status": "created"}` on success.
53///
54/// Errors: `409 Conflict` if a route with the same name already exists;
55/// `400 Bad Request` if the resulting configuration fails
56/// validation/recompilation (the previous compiled routes stay active).
57async fn create_route(
58    State(state): State<Arc<SharedState>>,
59    Json(route): Json<RouteConfig>,
60) -> impl IntoResponse {
61    let candidate = {
62        let gw = state.gateway.read().await;
63        if gw.routes.iter().any(|r| r.name == route.name) {
64            return (
65                StatusCode::CONFLICT,
66                Json(serde_json::json!({"error": "route already exists"})),
67            )
68                .into_response();
69        }
70        let mut candidate = gw.clone();
71        candidate.routes.push(route);
72        candidate
73    };
74
75    match state.config_store.clone().commit(&state, candidate).await {
76        Ok(_) => (
77            StatusCode::CREATED,
78            Json(serde_json::json!({"status": "created"})),
79        )
80            .into_response(),
81        Err(e) => (
82            StatusCode::BAD_REQUEST,
83            Json(serde_json::json!({"error": e})),
84        )
85            .into_response(),
86    }
87}
88
89/// `PUT /api/routes/{name}` — replaces the named route with the JSON body
90/// (the path name overrides any name in the body), then revalidates and
91/// recompiles. Returns `{"status": "updated"}` on success.
92///
93/// Errors: `404 Not Found` if the route does not exist (unlike policies,
94/// routes are not upserted); `400 Bad Request` if recompilation fails.
95async fn update_route(
96    State(state): State<Arc<SharedState>>,
97    Path(name): Path<String>,
98    Json(mut route): Json<RouteConfig>,
99) -> impl IntoResponse {
100    route.name = name.clone();
101    let candidate = {
102        let gw = state.gateway.read().await;
103        let mut candidate = gw.clone();
104        if let Some(existing) = candidate.routes.iter_mut().find(|r| r.name == name) {
105            *existing = route;
106        } else {
107            return (
108                StatusCode::NOT_FOUND,
109                Json(serde_json::json!({"error": "not_found"})),
110            )
111                .into_response();
112        }
113        candidate
114    };
115
116    match state.config_store.clone().commit(&state, candidate).await {
117        Ok(_) => Json(serde_json::json!({"status": "updated"})).into_response(),
118        Err(e) => (
119            StatusCode::BAD_REQUEST,
120            Json(serde_json::json!({"error": e})),
121        )
122            .into_response(),
123    }
124}
125
126/// `DELETE /api/routes/{name}` — removes the named route, then revalidates
127/// and recompiles. Returns `{"status": "deleted"}` on success.
128///
129/// Errors: `404 Not Found` if the route does not exist; `400 Bad Request`
130/// if recompilation fails.
131async fn delete_route(
132    State(state): State<Arc<SharedState>>,
133    Path(name): Path<String>,
134) -> impl IntoResponse {
135    let candidate = {
136        let gw = state.gateway.read().await;
137        let mut candidate = gw.clone();
138        let before = candidate.routes.len();
139        candidate.routes.retain(|r| r.name != name);
140        if candidate.routes.len() == before {
141            return (
142                StatusCode::NOT_FOUND,
143                Json(serde_json::json!({"error": "not_found"})),
144            )
145                .into_response();
146        }
147        candidate
148    };
149
150    match state.config_store.clone().commit(&state, candidate).await {
151        Ok(_) => Json(serde_json::json!({"status": "deleted"})).into_response(),
152        Err(e) => (
153            StatusCode::BAD_REQUEST,
154            Json(serde_json::json!({"error": e})),
155        )
156            .into_response(),
157    }
158}