Skip to main content

featherbit/admin/
vars.rs

1//! Admin API endpoint serving the context-variable catalog
2//! (src/vars/catalog.rs) — consumed by the UI's autocomplete and var legend.
3
4use std::sync::Arc;
5
6use axum::response::IntoResponse;
7use axum::routing::get;
8use axum::{Json, Router};
9
10use crate::state::SharedState;
11
12/// Builds the router for `/api/vars`.
13pub fn router() -> Router<Arc<SharedState>> {
14    Router::new().route("/api/vars", get(list_vars))
15}
16
17/// `GET /api/vars` — the static catalog of `$var` names plugins can
18/// interpolate, with kinds, family sources, and descriptions. Always `200 OK`.
19async fn list_vars() -> impl IntoResponse {
20    Json(serde_json::json!({ "vars": crate::vars::catalog::var_catalog() }))
21}
22
23#[cfg(test)]
24mod tests {
25    use super::*;
26    use axum::body::Body;
27    use axum::http::{Request, StatusCode};
28    use tower::ServiceExt;
29
30    #[tokio::test]
31    async fn test_list_vars_shape() {
32        // Stateless handler; a state-free router instance suffices.
33        let app: Router = Router::new().route("/api/vars", get(list_vars));
34        let resp = app
35            .oneshot(Request::get("/api/vars").body(Body::empty()).unwrap())
36            .await
37            .unwrap();
38        assert_eq!(resp.status(), StatusCode::OK);
39        let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
40            .await
41            .unwrap();
42        let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
43        let vars = v["vars"].as_array().unwrap();
44        assert!(vars
45            .iter()
46            .any(|e| e["name"] == "uri" && e["kind"] == "static"));
47        assert!(vars.iter().any(|e| e["name"] == "http_*"
48            && e["kind"] == "family"
49            && e["family_source"] == "request_headers"));
50        assert!(vars.iter().any(|e| e["name"] == "sent_http_*"));
51        assert!(vars.iter().any(|e| e["name"] == "request_body"));
52    }
53}