1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct OpenApiSpec {
7 pub openapi: String,
8 pub info: Info,
9 pub paths: HashMap<String, PathItem>,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub components: Option<Components>,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub servers: Option<Vec<Server>>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Info {
18 pub title: String,
19 pub version: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub description: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Server {
26 pub url: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub description: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct PathItem {
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub get: Option<Operation>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub post: Option<Operation>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub put: Option<Operation>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub delete: Option<Operation>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44pub struct Operation {
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub summary: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub description: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub tags: Option<Vec<String>>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub parameters: Option<Vec<Parameter>>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub request_body: Option<RequestBody>,
55 pub responses: HashMap<String, Response>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Parameter {
60 pub name: String,
61 #[serde(rename = "in")]
62 pub location: String, #[serde(skip_serializing_if = "Option::is_none")]
64 pub description: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub required: Option<bool>,
67 pub schema: Schema,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct RequestBody {
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub description: Option<String>,
74 pub required: bool,
75 pub content: HashMap<String, MediaType>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Response {
80 pub description: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub content: Option<HashMap<String, MediaType>>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct MediaType {
87 pub schema: Schema,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(untagged)]
92pub enum Schema {
93 Simple {
94 #[serde(rename = "type")]
95 type_name: String,
96 },
97 Object {
98 #[serde(rename = "type")]
99 type_name: String,
100 properties: HashMap<String, Box<Schema>>,
101 },
102 Array {
103 #[serde(rename = "type")]
104 type_name: String,
105 items: Box<Schema>,
106 },
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110pub struct Components {
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub schemas: Option<HashMap<String, Schema>>,
113}
114
115pub struct OpenApiBuilder {
117 spec: OpenApiSpec,
118}
119
120impl OpenApiBuilder {
121 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
122 Self {
123 spec: OpenApiSpec {
124 openapi: "3.0.0".to_string(),
125 info: Info {
126 title: title.into(),
127 version: version.into(),
128 description: None,
129 },
130 paths: HashMap::new(),
131 components: None,
132 servers: None,
133 },
134 }
135 }
136
137 pub fn description(mut self, desc: impl Into<String>) -> Self {
138 self.spec.info.description = Some(desc.into());
139 self
140 }
141
142 pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
143 let servers = self.spec.servers.get_or_insert_with(Vec::new);
144 servers.push(Server {
145 url: url.into(),
146 description,
147 });
148 self
149 }
150
151 pub fn path(mut self, path: impl Into<String>, item: PathItem) -> Self {
152 self.spec.paths.insert(path.into(), item);
153 self
154 }
155
156 pub fn build(self) -> OpenApiSpec {
157 self.spec
158 }
159
160 pub fn to_json(&self) -> Result<String, serde_json::Error> {
161 serde_json::to_string_pretty(&self.spec)
162 }
163}
164
165pub fn get_operation(summary: impl Into<String>) -> Operation {
167 Operation {
168 summary: Some(summary.into()),
169 description: None,
170 tags: None,
171 parameters: None,
172 request_body: None,
173 responses: HashMap::new(),
174 }
175}
176
177pub fn post_operation(summary: impl Into<String>) -> Operation {
179 Operation {
180 summary: Some(summary.into()),
181 description: None,
182 tags: None,
183 parameters: None,
184 request_body: None,
185 responses: HashMap::new(),
186 }
187}
188
189pub trait AutoDocs {
191 fn with_auto_docs(self, spec: OpenApiSpec) -> Self;
193}
194
195pub fn generate_docs_html(spec: &OpenApiSpec) -> String {
197 let spec_json = serde_json::to_string_pretty(spec).unwrap_or_else(|_| "{}".to_string());
198
199 format!(r#"<!DOCTYPE html>
200<html lang="en">
201<head>
202 <meta charset="UTF-8">
203 <meta name="viewport" content="width=device-width, initial-scale=1.0">
204 <title>{} - API Documentation</title>
205 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
206</head>
207<body>
208 <div id="swagger-ui"></div>
209 <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
210 <script>
211 const spec = {};
212 SwaggerUIBundle({{
213 spec: spec,
214 dom_id: '#swagger-ui',
215 deepLinking: true,
216 presets: [
217 SwaggerUIBundle.presets.apis,
218 SwaggerUIBundle.SwaggerUIStandalonePreset
219 ],
220 }});
221 </script>
222</body>
223</html>"#, spec.info.title, spec_json)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_openapi_builder() {
232 let spec = OpenApiBuilder::new("Test API", "1.0.0")
233 .description("A test API")
234 .server("http://localhost:8080", Some("Local server".to_string()))
235 .build();
236
237 assert_eq!(spec.info.title, "Test API");
238 assert_eq!(spec.info.version, "1.0.0");
239 }
240}