mod paths;
mod schemas;
use schemars::Schema;
use serde_json::{Map, Value, json};
pub use paths::{build_paths, path_template_count};
pub use schemas::{
ResumeSessionKernelReplay, ResumeSessionResponse, SessionDetailResponse, SessionsListResponse,
StartTurnResponse, StreamTurnRequest, ThreadSummary,
};
pub use schemas::{SCHEMA_EXPORTS, SchemaExportFn};
fn rewrite_refs(value: &mut Value) {
match value {
Value::Object(map) => {
if let Some(Value::String(r)) = map.get("$ref")
&& let Some(stripped) = r.strip_prefix("#/$defs/")
{
map.insert(
"$ref".into(),
Value::String(format!("#/components/schemas/{stripped}")),
);
}
for v in map.values_mut() {
rewrite_refs(v);
}
}
Value::Array(arr) => {
for v in arr {
rewrite_refs(v);
}
}
_ => {}
}
}
fn register_schema(components: &mut Map<String, Value>, name: &str, mut schema: Value) {
if let Some(defs) = schema.as_object_mut().and_then(|o| o.remove("$defs"))
&& let Some(def_map) = defs.as_object()
{
for (def_name, def_schema) in def_map {
register_schema(components, def_name, def_schema.clone());
}
}
rewrite_refs(&mut schema);
components.insert(name.into(), schema);
}
fn register_exports(components: &mut Map<String, Value>, exports: &[(&str, SchemaExportFn)]) {
for (name, make_schema) in exports {
let schema: Schema = make_schema();
let value = serde_json::to_value(&schema).unwrap_or_else(|e| {
panic!("failed to serialize schema {name}: {e}");
});
register_schema(components, name, value);
}
}
pub fn build_openapi_value_with(extra_schemas: &[(&str, SchemaExportFn)]) -> Value {
let mut components_schemas = Map::new();
register_exports(&mut components_schemas, SCHEMA_EXPORTS);
register_exports(&mut components_schemas, extra_schemas);
json!({
"openapi": "3.1.0",
"info": {
"title": "Zagens Runtime HTTP API",
"version": "1.0.0",
"description": "Local sidecar (`zagens-runtime`) HTTP/SSE surface consumed by Zagens desktop web-ui. SSOT routes: `crates/runtime-api/src/openapi/paths.rs`."
},
"servers": [{ "url": "http://127.0.0.1:7878" }],
"components": {
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "Runtime token from Zagens shell (`DEEPSEEK_RUNTIME_TOKEN` / Tauri proxy)."
}
},
"schemas": Value::Object(components_schemas)
},
"paths": Value::Object(build_paths())
})
}
pub fn export_openapi_json_with(extra_schemas: &[(&str, SchemaExportFn)]) -> String {
serde_json::to_string_pretty(&build_openapi_value_with(extra_schemas)).expect("openapi json")
}
pub fn build_openapi_value() -> Value {
build_openapi_value_with(&[])
}
pub fn export_openapi_json() -> String {
export_openapi_json_with(&[])
}
#[cfg(test)]
mod tests {
use super::*;
const EXPECTED_PATH_TEMPLATES: usize = 54;
#[test]
fn openapi_path_templates_match_router() {
assert_eq!(
path_template_count(),
EXPECTED_PATH_TEMPLATES,
"update EXPECTED_PATH_TEMPLATES or paths.rs when router.rs changes"
);
}
#[test]
fn openapi_exports_core_schemas() {
assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "ThreadRecord"));
assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "SessionMetadata"));
}
#[test]
fn openapi_exports_task_schemas() {
assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "TaskRecord"));
assert!(SCHEMA_EXPORTS.iter().any(|(n, _)| *n == "TasksResponse"));
}
#[test]
fn openapi_components_resolve_session_list_ref() {
let doc = build_openapi_value_with(&[]);
let schemas = &doc["components"]["schemas"];
let list = &schemas["SessionsListResponse"];
let items_ref = &list["properties"]["sessions"]["items"]["$ref"];
assert_eq!(
items_ref.as_str(),
Some("#/components/schemas/SessionMetadata")
);
assert!(schemas.get("SessionMetadata").is_some());
}
}