Skip to main content

oxidite_openapi/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// OpenAPI 3.0 Specification
5#[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, // "query", "path", "header"
63    #[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
115/// OpenAPI Documentation Builder
116pub 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
165/// Helper to create a GET operation
166pub 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
177/// Helper to create a POST operation
178pub 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
189/// Auto-documentation trait for routers
190pub trait AutoDocs {
191    /// Register the /api/docs endpoint with OpenAPI documentation
192    fn with_auto_docs(self, spec: OpenApiSpec) -> Self;
193}
194
195/// Generate HTML documentation page
196pub 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}