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>,
94 pub email: Option<String>,
95 pub url: Option<String>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct LicenseInfo {
101 pub name: String,
102 pub url: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ServerInfo {
108 pub url: String,
109 pub description: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "type", rename_all = "lowercase")]
115pub enum SecuritySchemeInfo {
116 #[serde(rename = "http")]
117 Http {
118 scheme: String,
119 #[serde(rename = "bearerFormat")]
120 bearer_format: Option<String>,
121 },
122 #[serde(rename = "apiKey")]
123 ApiKey {
124 #[serde(rename = "in")]
125 location: String,
126 name: String,
127 },
128}
129
130pub fn security_scheme_info_to_openapi(info: &SecuritySchemeInfo) -> SecurityScheme {
132 match info {
133 SecuritySchemeInfo::Http { scheme, bearer_format } => {
134 let mut http_scheme = SecurityScheme::Http(utoipa::openapi::security::Http::new(
135 utoipa::openapi::security::HttpAuthScheme::Bearer,
136 ));
137 if let (SecurityScheme::Http(http), "bearer") = (&mut http_scheme, scheme.as_str()) {
138 http.scheme = utoipa::openapi::security::HttpAuthScheme::Bearer;
139 if let Some(format) = bearer_format {
140 http.bearer_format = Some(format.clone());
141 }
142 }
143 http_scheme
144 }
145 SecuritySchemeInfo::ApiKey { location, name } => {
146 use utoipa::openapi::security::ApiKey;
147
148 let api_key = match location.as_str() {
149 "header" => ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(name)),
150 "query" => ApiKey::Query(utoipa::openapi::security::ApiKeyValue::new(name)),
151 "cookie" => ApiKey::Cookie(utoipa::openapi::security::ApiKeyValue::new(name)),
152 _ => ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(name)),
153 };
154 SecurityScheme::ApiKey(api_key)
155 }
156 }
157}
158
159pub fn generate_openapi_spec(
161 routes: &[crate::RouteMetadata],
162 config: &OpenApiConfig,
163 _schema_registry: &SchemaRegistry,
164 server_config: Option<&crate::ServerConfig>,
165) -> Result<utoipa::openapi::OpenApi, String> {
166 spec_generation::assemble_openapi_spec(routes, config, server_config)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_openapi_config_default() {
175 let config = OpenApiConfig::default();
176 assert!(!config.enabled);
177 assert_eq!(config.title, "API");
178 assert_eq!(config.version, "1.0.0");
179 assert_eq!(config.swagger_ui_path, "/docs");
180 assert_eq!(config.redoc_path, "/redoc");
181 assert_eq!(config.openapi_json_path, "/openapi.json");
182 }
183
184 #[test]
185 fn test_generate_minimal_spec() {
186 let config = OpenApiConfig {
187 enabled: true,
188 title: "Test API".to_string(),
189 version: "1.0.0".to_string(),
190 ..Default::default()
191 };
192
193 let routes = vec![];
194 let registry = SchemaRegistry::new();
195
196 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
197 assert_eq!(spec.info.title, "Test API");
198 assert_eq!(spec.info.version, "1.0.0");
199 }
200
201 #[test]
202 fn test_generate_spec_with_contact() {
203 let config = OpenApiConfig {
204 enabled: true,
205 title: "Test API".to_string(),
206 version: "1.0.0".to_string(),
207 contact: Some(ContactInfo {
208 name: Some("API Team".to_string()),
209 email: Some("api@example.com".to_string()),
210 url: Some("https://example.com".to_string()),
211 }),
212 ..Default::default()
213 };
214
215 let routes = vec![];
216 let registry = SchemaRegistry::new();
217
218 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
219 assert!(spec.info.contact.is_some());
220 let contact = spec.info.contact.unwrap();
221 assert_eq!(contact.name, Some("API Team".to_string()));
222 assert_eq!(contact.email, Some("api@example.com".to_string()));
223 }
224
225 #[test]
226 fn test_generate_spec_with_license() {
227 let config = OpenApiConfig {
228 enabled: true,
229 title: "Test API".to_string(),
230 version: "1.0.0".to_string(),
231 license: Some(LicenseInfo {
232 name: "MIT".to_string(),
233 url: Some("https://opensource.org/licenses/MIT".to_string()),
234 }),
235 ..Default::default()
236 };
237
238 let routes = vec![];
239 let registry = SchemaRegistry::new();
240
241 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
242 assert!(spec.info.license.is_some());
243 let license = spec.info.license.unwrap();
244 assert_eq!(license.name, "MIT");
245 }
246
247 #[test]
248 fn test_generate_spec_with_servers() {
249 let config = OpenApiConfig {
250 enabled: true,
251 title: "Test API".to_string(),
252 version: "1.0.0".to_string(),
253 servers: vec![
254 ServerInfo {
255 url: "https://api.example.com".to_string(),
256 description: Some("Production".to_string()),
257 },
258 ServerInfo {
259 url: "http://localhost:8080".to_string(),
260 description: Some("Development".to_string()),
261 },
262 ],
263 ..Default::default()
264 };
265
266 let routes = vec![];
267 let registry = SchemaRegistry::new();
268
269 let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
270 assert!(spec.servers.is_some());
271 let servers = spec.servers.unwrap();
272 assert_eq!(servers.len(), 2);
273 assert_eq!(servers[0].url, "https://api.example.com");
274 assert_eq!(servers[1].url, "http://localhost:8080");
275 }
276
277 #[test]
278 fn test_security_scheme_http_bearer() {
279 let scheme_info = SecuritySchemeInfo::Http {
280 scheme: "bearer".to_string(),
281 bearer_format: Some("JWT".to_string()),
282 };
283
284 let scheme = security_scheme_info_to_openapi(&scheme_info);
285 match scheme {
286 SecurityScheme::Http(http) => {
287 assert!(matches!(http.scheme, utoipa::openapi::security::HttpAuthScheme::Bearer));
288 assert_eq!(http.bearer_format, Some("JWT".to_string()));
289 }
290 _ => panic!("Expected Http security scheme"),
291 }
292 }
293
294 #[test]
295 fn test_security_scheme_api_key() {
296 let scheme_info = SecuritySchemeInfo::ApiKey {
297 location: "header".to_string(),
298 name: "X-API-Key".to_string(),
299 };
300
301 let scheme = security_scheme_info_to_openapi(&scheme_info);
302 match scheme {
303 SecurityScheme::ApiKey(api_key) => {
304 assert!(matches!(api_key, utoipa::openapi::security::ApiKey::Header(_)));
305 }
306 _ => panic!("Expected ApiKey security scheme"),
307 }
308 }
309}