1use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct OpenAPISpec {
16 pub paths: HashMap<String, serde_json::Value>,
17 pub info: serde_json::Value,
18}
19
20impl OpenAPISpec {
21 pub fn to_json_string(&self) -> String {
22 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PathInfo {
29 pub method: String,
30 pub summary: String,
31 pub parameters: Vec<serde_json::Value>,
32 pub responses: HashMap<String, serde_json::Value>,
33}
34
35impl PathInfo {
36 pub fn new(method: &str, summary: &str) -> Self {
37 Self {
38 method: method.to_string(),
39 summary: summary.to_string(),
40 parameters: vec![],
41 responses: HashMap::new(),
42 }
43 }
44
45 pub fn with_response(mut self, code: &str, desc: &str) -> Self {
46 self.responses
47 .insert(code.to_string(), serde_json::json!({ "description": desc }));
48 self
49 }
50
51 pub fn with_parameter(mut self, param: serde_json::Value) -> Self {
52 self.parameters.push(param);
53 self
54 }
55}
56
57pub struct OpenAPIGenerator {
58 paths: Vec<(String, PathInfo)>,
59 info: serde_json::Value,
60}
61
62impl OpenAPIGenerator {
63 pub fn new() -> Self {
64 Self {
65 paths: vec![],
66 info: serde_json::json!({
67 "title": "API",
68 "version": "1.0.0",
69 "description": "Generated by sz-orm-swagger"
70 }),
71 }
72 }
73
74 pub fn with_info(mut self, info: serde_json::Value) -> Self {
75 self.info = info;
76 self
77 }
78
79 pub fn register_path(&mut self, path: &str, info: PathInfo) -> &mut Self {
82 self.paths.push((path.to_string(), info));
83 self
84 }
85
86 pub fn generate(&self) -> OpenAPISpec {
88 let mut paths: HashMap<String, serde_json::Value> = HashMap::new();
89 for (path, info) in &self.paths {
90 let method = info.method.to_lowercase();
91 let entry = paths
92 .entry(path.clone())
93 .or_insert_with(|| serde_json::json!({}));
94 entry[method] = serde_json::json!({
95 "summary": info.summary,
96 "parameters": info.parameters,
97 "responses": info.responses
98 });
99 }
100 OpenAPISpec {
101 paths,
102 info: self.info.clone(),
103 }
104 }
105}
106
107impl Default for OpenAPIGenerator {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113pub struct SwaggerUi {
114 mount_path: String,
115 spec: Option<OpenAPISpec>,
116}
117
118impl SwaggerUi {
119 pub fn new(path: &str) -> Self {
120 Self {
121 mount_path: path.to_string(),
122 spec: None,
123 }
124 }
125
126 pub fn with_spec(mut self, spec: OpenAPISpec) -> Self {
127 self.spec = Some(spec);
128 self
129 }
130
131 pub fn mount(&self) -> String {
132 format!("{}docs", self.mount_path)
133 }
134
135 pub fn render_html(&self) -> String {
138 let spec_json = match &self.spec {
139 Some(s) => s.to_json_string(),
140 None => serde_json::json!({
141 "openapi": "3.0.0",
142 "info": { "title": "API", "version": "1.0.0" },
143 "paths": {}
144 })
145 .to_string(),
146 };
147 let mount = self.mount();
148 format!(
149 r#"<!DOCTYPE html>
150<html lang="en">
151<head>
152 <meta charset="UTF-8">
153 <title>Swagger UI</title>
154 <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui.css">
155</head>
156<body>
157 <div id="swagger-ui"></div>
158 <script src="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui-bundle.js"></script>
159 <script src="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui-standalone-preset.js"></script>
160 <script>
161 const spec = {spec};
162 window.onload = () => {{
163 SwaggerUIBundle({{
164 spec: spec,
165 dom_id: '#swagger-ui',
166 url: '{mount}/openapi.json',
167 presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
168 layout: 'StandaloneLayout'
169 }});
170 }};
171 </script>
172</body>
173</html>"#,
174 spec = spec_json,
175 mount = mount
176 )
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_gen_empty_has_no_paths() {
186 let s = OpenAPIGenerator::new().generate();
187 assert!(s.paths.is_empty());
188 assert_eq!(s.info["title"], "API");
190 }
191
192 #[test]
193 fn test_register_and_generate_single_path() {
194 let mut g = OpenAPIGenerator::new();
195 g.register_path(
196 "/users",
197 PathInfo::new("GET", "List users").with_response("200", "OK"),
198 );
199 let spec = g.generate();
200 let users = spec.paths.get("/users").expect("/users should exist");
201 let get = users.get("get").expect("GET method should exist");
202 assert_eq!(get["summary"], "List users");
203 assert!(get["responses"]["200"].is_object());
204 }
205
206 #[test]
207 fn test_register_multiple_methods_same_path() {
208 let mut g = OpenAPIGenerator::new();
209 g.register_path(
210 "/users",
211 PathInfo::new("GET", "List users").with_response("200", "OK"),
212 );
213 g.register_path(
214 "/users",
215 PathInfo::new("POST", "Create user").with_response("201", "Created"),
216 );
217 let spec = g.generate();
218 assert_eq!(spec.paths.len(), 1, "only one /users path key");
219 let users = spec.paths.get("/users").unwrap();
220 assert!(users.get("get").is_some());
221 assert!(users.get("post").is_some());
222 assert_eq!(users["get"]["summary"], "List users");
223 assert_eq!(users["post"]["summary"], "Create user");
224 assert_eq!(users["post"]["responses"]["201"]["description"], "Created");
225 }
226
227 #[test]
228 fn test_register_multiple_paths() {
229 let mut g = OpenAPIGenerator::new();
230 g.register_path("/users", PathInfo::new("GET", "List users"));
231 g.register_path("/orders", PathInfo::new("GET", "List orders"));
232 g.register_path(
233 "/items/{id}",
234 PathInfo::new("GET", "Get item").with_response("404", "Not found"),
235 );
236 let spec = g.generate();
237 assert_eq!(spec.paths.len(), 3);
238 assert!(spec.paths.contains_key("/users"));
239 assert!(spec.paths.contains_key("/orders"));
240 assert!(spec.paths.contains_key("/items/{id}"));
241 }
242
243 #[test]
244 fn test_ui_mount() {
245 let ui = SwaggerUi::new("/api");
246 assert_eq!(ui.mount(), "/apidocs");
247 }
248
249 #[test]
250 fn test_ui_html_contains_cdn_and_bundle() {
251 let ui = SwaggerUi::new("/api").with_spec(OpenAPIGenerator::new().generate());
252 let html = ui.render_html();
253 assert!(html.contains("swagger-ui-dist"));
254 assert!(html.contains("swagger-ui.css"));
255 assert!(html.contains("swagger-ui-bundle.js"));
256 assert!(html.contains("SwaggerUIBundle"));
257 assert!(html.contains("id=\"swagger-ui\""));
258 assert!(html.contains("<!DOCTYPE html>"));
259 }
260
261 #[test]
262 fn test_ui_html_embeds_spec_content() {
263 let mut g = OpenAPIGenerator::new();
264 g.register_path(
265 "/items",
266 PathInfo::new("GET", "List items").with_response("200", "OK"),
267 );
268 let ui = SwaggerUi::new("/api").with_spec(g.generate());
269 let html = ui.render_html();
270 assert!(html.contains("/items"));
272 assert!(html.contains("List items"));
273 assert!(html.contains("\"get\""));
274 }
275
276 #[test]
277 fn test_ui_html_without_spec_uses_default_spec() {
278 let ui = SwaggerUi::new("/api");
279 let html = ui.render_html();
280 assert!(html.contains("swagger-ui"));
282 assert!(html.contains("\"openapi\""));
283 }
284
285 #[test]
286 fn test_spec_to_json_string_is_valid_json() {
287 let mut g = OpenAPIGenerator::new();
288 g.register_path("/users", PathInfo::new("GET", "List users"));
289 let spec = g.generate();
290 let json = spec.to_json_string();
291 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
292 assert!(parsed["paths"]["/users"]["get"].is_object());
293 }
294
295 #[test]
296 fn test_path_info_builder() {
297 let p = PathInfo::new("PUT", "Update user")
298 .with_response("200", "OK")
299 .with_response("404", "Not found")
300 .with_parameter(serde_json::json!({"name": "id", "in": "path"}));
301 assert_eq!(p.method, "PUT");
302 assert_eq!(p.responses.len(), 2);
303 assert_eq!(p.parameters.len(), 1);
304 }
305}