1pub mod parameter_extraction;
7pub mod schema_conversion;
8pub mod spec_generation;
9
10use crate::SchemaRegistry;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use utoipa::openapi::security::SecurityScheme;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OpenApiConfig {
18 pub enabled: bool,
20
21 pub title: String,
23
24 pub version: String,
26
27 #[serde(default)]
29 pub description: Option<String>,
30
31 #[serde(default = "default_swagger_path")]
33 pub swagger_ui_path: String,
34
35 #[serde(default = "default_redoc_path")]
37 pub redoc_path: String,
38
39 #[serde(default = "default_openapi_json_path")]
41 pub openapi_json_path: String,
42
43 #[serde(default)]
45 pub contact: Option<ContactInfo>,
46
47 #[serde(default)]
49 pub license: Option<LicenseInfo>,
50
51 #[serde(default)]
53 pub servers: Vec<ServerInfo>,
54
55 #[serde(default)]
57 pub security_schemes: HashMap<String, SecuritySchemeInfo>,
58}
59
60impl Default for OpenApiConfig {
61 fn default() -> Self {
62 Self {
63 enabled: false,
64 title: "API".to_string(),
65 version: "1.0.0".to_string(),
66 description: None,
67 swagger_ui_path: default_swagger_path(),
68 redoc_path: default_redoc_path(),
69 openapi_json_path: default_openapi_json_path(),
70 contact: None,
71 license: None,
72 servers: Vec::new(),
73 security_schemes: HashMap::new(),
74 }
75 }
76}
77
78fn default_swagger_path() -> String {
79 "/docs".to_string()
80}
81
82fn default_redoc_path() -> String {
83 "/redoc".to_string()
84}
85
86fn default_openapi_json_path() -> String {
87 "/openapi.json".to_string()
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ContactInfo {
93 pub name: Option<String>,
95 pub email: Option<String>,
97 pub url: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct LicenseInfo {
104 pub name: String,
106 pub url: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ServerInfo {
113 pub url: String,
115 pub description: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(tag = "type", rename_all = "lowercase")]
122pub enum SecuritySchemeInfo {
123 #[serde(rename = "http")]
124 Http {
125 scheme: String,
126 #[serde(rename = "bearerFormat")]
127 bearer_format: Option<String>,
128 },
129 #[serde(rename = "apiKey")]
130 ApiKey {
131 #[serde(rename = "in")]
132 location: String,
133 name: String,
134 },
135}
136
137impl Default for SecuritySchemeInfo {
138 fn default() -> Self {
139 Self::Http {
140 scheme: "bearer".to_string(),
141 bearer_format: None,
142 }
143 }
144}
145
146pub fn security_scheme_info_to_openapi(info: &SecuritySchemeInfo) -> SecurityScheme {
148 match info {
149 SecuritySchemeInfo::Http { scheme, bearer_format } => {
150 let mut http_scheme = SecurityScheme::Http(utoipa::openapi::security::Http::new(
151 utoipa::openapi::security::HttpAuthScheme::Bearer,
152 ));
153 if let (SecurityScheme::Http(http), "bearer") = (&mut http_scheme, scheme.as_str()) {
154 http.scheme = utoipa::openapi::security::HttpAuthScheme::Bearer;
155 if let Some(format) = bearer_format {
156 http.bearer_format = Some(format.clone());
157 }
158 }
159 http_scheme
160 }
161 SecuritySchemeInfo::ApiKey { location, name } => {
162 use utoipa::openapi::security::ApiKey;
163
164 let api_key = match location.as_str() {
165 "header" => ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(name)),
166 "query" => ApiKey::Query(utoipa::openapi::security::ApiKeyValue::new(name)),
167 "cookie" => ApiKey::Cookie(utoipa::openapi::security::ApiKeyValue::new(name)),
168 _ => ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(name)),
169 };
170 SecurityScheme::ApiKey(api_key)
171 }
172 }
173}
174
175pub fn generate_openapi_spec(
177 routes: &[crate::RouteMetadata],
178 config: &OpenApiConfig,
179 _schema_registry: &SchemaRegistry,
180 server_config: Option<&crate::ServerConfig>,
181) -> Result<utoipa::openapi::OpenApi, String> {
182 spec_generation::assemble_openapi_spec(routes, config, server_config)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_openapi_config_default() {
191 let config = OpenApiConfig::default();
192 assert!(!config.enabled);
193 assert_eq!(config.title, "API");
194 assert_eq!(config.version, "1.0.0");
195 assert_eq!(config.swagger_ui_path, "/docs");
196 assert_eq!(config.redoc_path, "/redoc");
197 assert_eq!(config.openapi_json_path, "/openapi.json");
198 }
199
200 #[test]
201 fn test_generate_minimal_spec() {
202 let config = OpenApiConfig {
203 enabled: true,
204 title: "Test API".to_string(),
205 version: "1.0.0".to_string(),
206 ..Default::default()
207 };
208
209 let routes = vec![];
210 let registry = SchemaRegistry::new();
211
212 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
213 assert_eq!(spec.info.title, "Test API");
214 assert_eq!(spec.info.version, "1.0.0");
215 }
216
217 #[test]
218 fn test_generate_spec_with_contact() {
219 let config = OpenApiConfig {
220 enabled: true,
221 title: "Test API".to_string(),
222 version: "1.0.0".to_string(),
223 contact: Some(ContactInfo {
224 name: Some("API Team".to_string()),
225 email: Some("api@example.com".to_string()),
226 url: Some("https://example.com".to_string()),
227 }),
228 ..Default::default()
229 };
230
231 let routes = vec![];
232 let registry = SchemaRegistry::new();
233
234 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
235 assert!(spec.info.contact.is_some());
236 let contact = spec.info.contact.unwrap();
237 assert_eq!(contact.name, Some("API Team".to_string()));
238 assert_eq!(contact.email, Some("api@example.com".to_string()));
239 }
240
241 #[test]
242 fn test_generate_spec_with_license() {
243 let config = OpenApiConfig {
244 enabled: true,
245 title: "Test API".to_string(),
246 version: "1.0.0".to_string(),
247 license: Some(LicenseInfo {
248 name: "MIT".to_string(),
249 url: Some("https://opensource.org/licenses/MIT".to_string()),
250 }),
251 ..Default::default()
252 };
253
254 let routes = vec![];
255 let registry = SchemaRegistry::new();
256
257 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
258 assert!(spec.info.license.is_some());
259 let license = spec.info.license.unwrap();
260 assert_eq!(license.name, "MIT");
261 }
262
263 #[test]
264 fn test_generate_spec_with_servers() {
265 let config = OpenApiConfig {
266 enabled: true,
267 title: "Test API".to_string(),
268 version: "1.0.0".to_string(),
269 servers: vec![
270 ServerInfo {
271 url: "https://api.example.com".to_string(),
272 description: Some("Production".to_string()),
273 },
274 ServerInfo {
275 url: "http://localhost:8080".to_string(),
276 description: Some("Development".to_string()),
277 },
278 ],
279 ..Default::default()
280 };
281
282 let routes = vec![];
283 let registry = SchemaRegistry::new();
284
285 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
286 assert!(spec.servers.is_some());
287 let servers = spec.servers.unwrap();
288 assert_eq!(servers.len(), 2);
289 assert_eq!(servers[0].url, "https://api.example.com");
290 assert_eq!(servers[1].url, "http://localhost:8080");
291 }
292
293 #[test]
294 fn test_security_scheme_http_bearer() {
295 let scheme_info = SecuritySchemeInfo::Http {
296 scheme: "bearer".to_string(),
297 bearer_format: Some("JWT".to_string()),
298 };
299
300 let scheme = security_scheme_info_to_openapi(&scheme_info);
301 match scheme {
302 SecurityScheme::Http(http) => {
303 assert!(matches!(http.scheme, utoipa::openapi::security::HttpAuthScheme::Bearer));
304 assert_eq!(http.bearer_format, Some("JWT".to_string()));
305 }
306 _ => panic!("Expected Http security scheme"),
307 }
308 }
309
310 #[test]
311 fn test_security_scheme_api_key() {
312 let scheme_info = SecuritySchemeInfo::ApiKey {
313 location: "header".to_string(),
314 name: "X-API-Key".to_string(),
315 };
316
317 let scheme = security_scheme_info_to_openapi(&scheme_info);
318 match scheme {
319 SecurityScheme::ApiKey(api_key) => {
320 assert!(matches!(api_key, utoipa::openapi::security::ApiKey::Header(_)));
321 }
322 _ => panic!("Expected ApiKey security scheme"),
323 }
324 }
325}