Skip to main content

xyz_rust/httpapi/
openapi.rs

1// /openapi.json:OpenAPI 3 文档,与 MCP 前端同源(同一 InputSchema)。
2// 对齐 Go openapi.go:info 元数据、按 "路径 方法" 排序的 paths、path/query
3// 参数表、POST/PUT/PATCH 的 requestBody、响应表(200 带 outputSchema)。
4
5use axum::extract::Request;
6use axum::http::{StatusCode, header};
7use axum::response::{IntoResponse, Response};
8use serde_json::{Map, Value};
9
10use crate::errors;
11use crate::registry::Registry;
12use std::sync::Arc;
13
14/// 与 EntryHandler 同路线的具名 handler:构建期快照文档。
15#[derive(Clone)]
16pub struct OpenApiDoc(Arc<Value>);
17
18impl axum::handler::Handler<(), ()> for OpenApiDoc {
19    type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send>>;
20    fn call(self, _req: Request, _state: ()) -> Self::Future {
21        Box::pin(async move { respond_doc(&self.0) })
22    }
23}
24
25pub fn openapi_handler(reg: &Registry, app_name: &str, app_version: &str) -> OpenApiDoc {
26    OpenApiDoc(Arc::new(build_doc(reg, app_name, app_version)))
27}
28
29fn build_doc(reg: &Registry, app_name: &str, app_version: &str) -> Value {
30    let mut paths: Map<String, Value> = Map::new();
31    // 每注册方法一个 operation(spec §11.3):默认 GET+POST 的命令产出
32    // get 与 post 两条。
33    let mut order: Vec<(String, String, String)> = Vec::new();
34    for e in reg.all() {
35        if e.http.skip || e.cli.daemon || e.http.path.is_empty() {
36            continue;
37        }
38        for method in crate::httpapi::http_methods(&e) {
39            order.push((e.http.path.clone(), method, e.name.clone()));
40        }
41    }
42    order.sort();
43    for (path, method, name) in order {
44        let Some(e) = reg.all().into_iter().find(|e| e.name == name) else {
45            continue;
46        };
47        let op = build_operation(&e, &method);
48        let path_obj = paths
49            .entry(path)
50            .or_insert_with(|| Value::Object(Map::new()));
51        if let Some(obj) = path_obj.as_object_mut() {
52            obj.insert(method.to_lowercase(), op);
53        }
54    }
55    let mut doc = Map::new();
56    doc.insert("openapi".to_string(), Value::String("3.0.3".to_string()));
57    // info 身份 = 应用身份(§11.6 同值);解析不到时用参考回退。
58    let mut info = Map::new();
59    let title = if app_name.is_empty() {
60        "example service".to_string()
61    } else {
62        app_name.to_string()
63    };
64    let version = if app_version.is_empty() {
65        "1".to_string()
66    } else {
67        app_version.to_string()
68    };
69    info.insert("title".to_string(), Value::String(title));
70    info.insert("version".to_string(), Value::String(version));
71    doc.insert("info".to_string(), Value::Object(info));
72    doc.insert("paths".to_string(), Value::Object(paths));
73    Value::Object(doc)
74}
75
76fn build_operation(e: &crate::spec::Entry, method: &str) -> Value {
77    let mut op = Map::new();
78    if !e.summary.is_empty() {
79        op.insert("summary".to_string(), Value::String(e.summary.clone()));
80    }
81    if !e.description.is_empty() {
82        op.insert(
83            "description".to_string(),
84            Value::String(e.description.clone()),
85        );
86    }
87    // 参数表(spec §11.3):每个 path/query/header 字段一条,带线上名、
88    // 位置、required(path 参数恒 true)、描述与富 schema(type + enum/
89    // default/format——与 MCP inputSchema 同源的逐字段 schema)。
90    // 未标注 location 的字段运行期按 query 绑定(httpapi/mod.rs 的
91    // ""|"query" 臂),文档同口径收录,否则 openapi.json 漏掉全部默认
92    // 字段(框架使用者几乎都不逐字段标注)。
93    let mut params: Vec<Value> = Vec::new();
94    for f in &e.root.children {
95        if f.skip {
96            continue;
97        }
98        let location = if f.http.location.is_empty() {
99            "query"
100        } else {
101            f.http.location.as_str()
102        };
103        if location != "path" && location != "query" && location != "header" {
104            continue;
105        }
106        let mut p = Map::new();
107        p.insert(
108            "name".to_string(),
109            Value::String(crate::httpapi::http_name(f).to_string()),
110        );
111        p.insert("in".to_string(), Value::String(location.to_string()));
112        let required = if location == "path" { true } else { f.required };
113        p.insert("required".to_string(), Value::Bool(required));
114        if !f.description.is_empty() {
115            p.insert(
116                "description".to_string(),
117                Value::String(f.description.clone()),
118            );
119        }
120        p.insert(
121            "schema".to_string(),
122            crate::spec::schema::schema_to_value(&crate::spec::schema::field_schema(f)),
123        );
124        params.push(Value::Object(p));
125    }
126    if !params.is_empty() {
127        op.insert("parameters".to_string(), Value::Array(params));
128    }
129    // 请求体:POST/PUT/PATCH 以 inputSchema 为 schema。
130    if matches!(method, "POST" | "PUT" | "PATCH") {
131        let body = serde_json::json!({
132            "content": {
133                "application/json": {
134                    "schema": crate::spec::schema::schema_to_value(&e.input_schema),
135                }
136            }
137        });
138        op.insert("requestBody".to_string(), body);
139    }
140    // 响应表。
141    let mut ok_resp = Map::new();
142    ok_resp.insert("description".to_string(), Value::String("ok".to_string()));
143    if let Some(out) = &e.output_schema {
144        ok_resp.insert(
145            "content".to_string(),
146            serde_json::json!({
147                "application/json": {
148                    "schema": crate::spec::schema::schema_to_value(out),
149                }
150            }),
151        );
152    }
153    let mut responses = Map::new();
154    responses.insert("200".to_string(), Value::Object(ok_resp));
155    responses.insert(
156        "400".to_string(),
157        serde_json::json!({ "description": errors::Kind::InvalidInput.as_str() }),
158    );
159    responses.insert(
160        "404".to_string(),
161        serde_json::json!({ "description": errors::Kind::NotFound.as_str() }),
162    );
163    responses.insert(
164        "500".to_string(),
165        serde_json::json!({ "description": errors::Kind::Internal.as_str() }),
166    );
167    op.insert("responses".to_string(), Value::Object(responses));
168    Value::Object(op)
169}
170
171fn respond_doc(doc: &Arc<Value>) -> Response {
172    let s = serde_json::to_string_pretty(&**doc).unwrap_or_else(|_| "{}".to_string());
173    let mut b = s;
174    b.push('\n');
175    (
176        StatusCode::OK,
177        [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
178        b,
179    )
180        .into_response()
181}