use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAPISpec {
pub paths: HashMap<String, serde_json::Value>,
pub info: serde_json::Value,
}
impl OpenAPISpec {
pub fn to_json_string(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathInfo {
pub method: String,
pub summary: String,
pub parameters: Vec<serde_json::Value>,
pub responses: HashMap<String, serde_json::Value>,
}
impl PathInfo {
pub fn new(method: &str, summary: &str) -> Self {
Self {
method: method.to_string(),
summary: summary.to_string(),
parameters: vec![],
responses: HashMap::new(),
}
}
pub fn with_response(mut self, code: &str, desc: &str) -> Self {
self.responses
.insert(code.to_string(), serde_json::json!({ "description": desc }));
self
}
pub fn with_parameter(mut self, param: serde_json::Value) -> Self {
self.parameters.push(param);
self
}
}
pub struct OpenAPIGenerator {
paths: Vec<(String, PathInfo)>,
info: serde_json::Value,
}
impl OpenAPIGenerator {
pub fn new() -> Self {
Self {
paths: vec![],
info: serde_json::json!({
"title": "API",
"version": "1.0.0",
"description": "Generated by sz-orm-swagger"
}),
}
}
pub fn with_info(mut self, info: serde_json::Value) -> Self {
self.info = info;
self
}
pub fn register_path(&mut self, path: &str, info: PathInfo) -> &mut Self {
self.paths.push((path.to_string(), info));
self
}
pub fn generate(&self) -> OpenAPISpec {
let mut paths: HashMap<String, serde_json::Value> = HashMap::new();
for (path, info) in &self.paths {
let method = info.method.to_lowercase();
let entry = paths
.entry(path.clone())
.or_insert_with(|| serde_json::json!({}));
entry[method] = serde_json::json!({
"summary": info.summary,
"parameters": info.parameters,
"responses": info.responses
});
}
OpenAPISpec {
paths,
info: self.info.clone(),
}
}
}
impl Default for OpenAPIGenerator {
fn default() -> Self {
Self::new()
}
}
pub struct SwaggerUi {
mount_path: String,
spec: Option<OpenAPISpec>,
}
impl SwaggerUi {
pub fn new(path: &str) -> Self {
Self {
mount_path: path.to_string(),
spec: None,
}
}
pub fn with_spec(mut self, spec: OpenAPISpec) -> Self {
self.spec = Some(spec);
self
}
pub fn mount(&self) -> String {
format!("{}docs", self.mount_path)
}
pub fn render_html(&self) -> String {
let spec_json = match &self.spec {
Some(s) => s.to_json_string(),
None => serde_json::json!({
"openapi": "3.0.0",
"info": { "title": "API", "version": "1.0.0" },
"paths": {}
})
.to_string(),
};
let mount = self.mount();
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui-standalone-preset.js"></script>
<script>
const spec = {spec};
window.onload = () => {{
SwaggerUIBundle({{
spec: spec,
dom_id: '#swagger-ui',
url: '{mount}/openapi.json',
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
layout: 'StandaloneLayout'
}});
}};
</script>
</body>
</html>"#,
spec = spec_json,
mount = mount
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gen_empty_has_no_paths() {
let s = OpenAPIGenerator::new().generate();
assert!(s.paths.is_empty());
assert_eq!(s.info["title"], "API");
}
#[test]
fn test_register_and_generate_single_path() {
let mut g = OpenAPIGenerator::new();
g.register_path(
"/users",
PathInfo::new("GET", "List users").with_response("200", "OK"),
);
let spec = g.generate();
let users = spec.paths.get("/users").expect("/users should exist");
let get = users.get("get").expect("GET method should exist");
assert_eq!(get["summary"], "List users");
assert!(get["responses"]["200"].is_object());
}
#[test]
fn test_register_multiple_methods_same_path() {
let mut g = OpenAPIGenerator::new();
g.register_path(
"/users",
PathInfo::new("GET", "List users").with_response("200", "OK"),
);
g.register_path(
"/users",
PathInfo::new("POST", "Create user").with_response("201", "Created"),
);
let spec = g.generate();
assert_eq!(spec.paths.len(), 1, "only one /users path key");
let users = spec.paths.get("/users").unwrap();
assert!(users.get("get").is_some());
assert!(users.get("post").is_some());
assert_eq!(users["get"]["summary"], "List users");
assert_eq!(users["post"]["summary"], "Create user");
assert_eq!(users["post"]["responses"]["201"]["description"], "Created");
}
#[test]
fn test_register_multiple_paths() {
let mut g = OpenAPIGenerator::new();
g.register_path("/users", PathInfo::new("GET", "List users"));
g.register_path("/orders", PathInfo::new("GET", "List orders"));
g.register_path(
"/items/{id}",
PathInfo::new("GET", "Get item").with_response("404", "Not found"),
);
let spec = g.generate();
assert_eq!(spec.paths.len(), 3);
assert!(spec.paths.contains_key("/users"));
assert!(spec.paths.contains_key("/orders"));
assert!(spec.paths.contains_key("/items/{id}"));
}
#[test]
fn test_ui_mount() {
let ui = SwaggerUi::new("/api");
assert_eq!(ui.mount(), "/apidocs");
}
#[test]
fn test_ui_html_contains_cdn_and_bundle() {
let ui = SwaggerUi::new("/api").with_spec(OpenAPIGenerator::new().generate());
let html = ui.render_html();
assert!(html.contains("swagger-ui-dist"));
assert!(html.contains("swagger-ui.css"));
assert!(html.contains("swagger-ui-bundle.js"));
assert!(html.contains("SwaggerUIBundle"));
assert!(html.contains("id=\"swagger-ui\""));
assert!(html.contains("<!DOCTYPE html>"));
}
#[test]
fn test_ui_html_embeds_spec_content() {
let mut g = OpenAPIGenerator::new();
g.register_path(
"/items",
PathInfo::new("GET", "List items").with_response("200", "OK"),
);
let ui = SwaggerUi::new("/api").with_spec(g.generate());
let html = ui.render_html();
assert!(html.contains("/items"));
assert!(html.contains("List items"));
assert!(html.contains("\"get\""));
}
#[test]
fn test_ui_html_without_spec_uses_default_spec() {
let ui = SwaggerUi::new("/api");
let html = ui.render_html();
assert!(html.contains("swagger-ui"));
assert!(html.contains("\"openapi\""));
}
#[test]
fn test_spec_to_json_string_is_valid_json() {
let mut g = OpenAPIGenerator::new();
g.register_path("/users", PathInfo::new("GET", "List users"));
let spec = g.generate();
let json = spec.to_json_string();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
assert!(parsed["paths"]["/users"]["get"].is_object());
}
#[test]
fn test_path_info_builder() {
let p = PathInfo::new("PUT", "Update user")
.with_response("200", "OK")
.with_response("404", "Not found")
.with_parameter(serde_json::json!({"name": "id", "in": "path"}));
assert_eq!(p.method, "PUT");
assert_eq!(p.responses.len(), 2);
assert_eq!(p.parameters.len(), 1);
}
}