zagens_runtime_api/openapi/
mod.rs1mod 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 ResumeSessionResponse, SessionDetailResponse, SessionsListResponse, StartTurnResponse,
12 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
62pub 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
90pub 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
95pub fn build_openapi_value() -> Value {
97 build_openapi_value_with(&[])
98}
99
100pub fn export_openapi_json() -> String {
102 export_openapi_json_with(&[])
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 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}