xyz_rust/httpapi/
openapi.rs1use 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#[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 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 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 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 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 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}