Skip to main content

zagens_runtime_api/openapi/
mod.rs

1//! OpenAPI 3.1 export for Zagens runtime HTTP API (D16 E1-c phase 1).
2
3mod paths;
4mod schemas;
5
6use schemars::Schema;
7use serde_json::{Map, Value, json};
8
9pub use paths::{build_paths, path_template_count};
10pub use schemas::{
11    ResumeSessionKernelReplay, ResumeSessionResponse, SessionDetailResponse, SessionsListResponse,
12    StartTurnResponse, StreamTurnRequest, ThreadSummary,
13};
14pub use schemas::{SCHEMA_EXPORTS, SchemaExportFn};
15
16fn rewrite_refs(value: &mut Value) {
17    match value {
18        Value::Object(map) => {
19            if let Some(Value::String(r)) = map.get("$ref")
20                && let Some(stripped) = r.strip_prefix("#/$defs/")
21            {
22                map.insert(
23                    "$ref".into(),
24                    Value::String(format!("#/components/schemas/{stripped}")),
25                );
26            }
27            for v in map.values_mut() {
28                rewrite_refs(v);
29            }
30        }
31        Value::Array(arr) => {
32            for v in arr {
33                rewrite_refs(v);
34            }
35        }
36        _ => {}
37    }
38}
39
40fn register_schema(components: &mut Map<String, Value>, name: &str, mut schema: Value) {
41    if let Some(defs) = schema.as_object_mut().and_then(|o| o.remove("$defs"))
42        && let Some(def_map) = defs.as_object()
43    {
44        for (def_name, def_schema) in def_map {
45            register_schema(components, def_name, def_schema.clone());
46        }
47    }
48    rewrite_refs(&mut schema);
49    components.insert(name.into(), schema);
50}
51
52fn register_exports(components: &mut Map<String, Value>, exports: &[(&str, SchemaExportFn)]) {
53    for (name, make_schema) in exports {
54        let schema: Schema = make_schema();
55        let value = serde_json::to_value(&schema).unwrap_or_else(|e| {
56            panic!("failed to serialize schema {name}: {e}");
57        });
58        register_schema(components, name, value);
59    }
60}
61
62/// Assemble the full OpenAPI document (JSON Schema 2020-12 components).
63pub fn build_openapi_value_with(extra_schemas: &[(&str, SchemaExportFn)]) -> Value {
64    let mut components_schemas = Map::new();
65    register_exports(&mut components_schemas, SCHEMA_EXPORTS);
66    register_exports(&mut components_schemas, extra_schemas);
67
68    json!({
69        "openapi": "3.1.0",
70        "info": {
71            "title": "Zagens Runtime HTTP API",
72            "version": "1.0.0",
73            "description": "Local sidecar (`zagens-runtime`) HTTP/SSE surface consumed by Zagens desktop web-ui. SSOT routes: `crates/runtime-api/src/openapi/paths.rs`."
74        },
75        "servers": [{ "url": "http://127.0.0.1:7878" }],
76        "components": {
77            "securitySchemes": {
78                "BearerAuth": {
79                    "type": "http",
80                    "scheme": "bearer",
81                    "description": "Runtime token from Zagens shell (`DEEPSEEK_RUNTIME_TOKEN` / Tauri proxy)."
82                }
83            },
84            "schemas": Value::Object(components_schemas)
85        },
86        "paths": Value::Object(build_paths())
87    })
88}
89
90/// Pretty-printed OpenAPI JSON for check-in and TS codegen.
91pub fn export_openapi_json_with(extra_schemas: &[(&str, SchemaExportFn)]) -> String {
92    serde_json::to_string_pretty(&build_openapi_value_with(extra_schemas)).expect("openapi json")
93}
94
95/// Assemble the full OpenAPI document (no sidecar-only schema extensions).
96pub fn build_openapi_value() -> Value {
97    build_openapi_value_with(&[])
98}
99
100/// Pretty-printed OpenAPI JSON for check-in and TS codegen.
101pub fn export_openapi_json() -> String {
102    export_openapi_json_with(&[])
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    /// `router.rs` registers 54 path templates (2026-05-26); update if routes change.
110    const EXPECTED_PATH_TEMPLATES: usize = 54;
111
112    #[test]
113    fn openapi_path_templates_match_router() {
114        assert_eq!(
115            path_template_count(),
116            EXPECTED_PATH_TEMPLATES,
117            "update EXPECTED_PATH_TEMPLATES or paths.rs when router.rs changes"
118        );
119    }
120
121    #[test]
122    fn openapi_exports_core_schemas() {
123        assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "ThreadRecord"));
124        assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "SessionMetadata"));
125    }
126
127    #[test]
128    fn openapi_exports_task_schemas() {
129        assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "TaskRecord"));
130        assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "TasksResponse"));
131    }
132
133    #[test]
134    fn openapi_components_resolve_session_list_ref() {
135        let doc = build_openapi_value_with(&[]);
136        let schemas = &doc["components"]["schemas"];
137        let list = &schemas["SessionsListResponse"];
138        let items_ref = &list["properties"]["sessions"]["items"]["$ref"];
139        assert_eq!(
140            items_ref.as_str(),
141            Some("#/components/schemas/SessionMetadata")
142        );
143        assert!(schemas.get("SessionMetadata").is_some());
144    }
145}