pub mod parameter_extraction;
pub mod schema_conversion;
pub mod spec_generation;
use crate::SchemaRegistry;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use utoipa::openapi::security::SecurityScheme;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenApiConfig {
pub enabled: bool,
pub title: String,
pub version: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_swagger_path")]
pub swagger_ui_path: String,
#[serde(default = "default_redoc_path")]
pub redoc_path: String,
#[serde(default = "default_openapi_json_path")]
pub openapi_json_path: String,
#[serde(default)]
pub contact: Option<ContactInfo>,
#[serde(default)]
pub license: Option<LicenseInfo>,
#[serde(default)]
pub servers: Vec<ServerInfo>,
#[serde(default)]
pub security_schemes: HashMap<String, SecuritySchemeInfo>,
}
impl Default for OpenApiConfig {
fn default() -> Self {
Self {
enabled: false,
title: "API".to_string(),
version: "1.0.0".to_string(),
description: None,
swagger_ui_path: default_swagger_path(),
redoc_path: default_redoc_path(),
openapi_json_path: default_openapi_json_path(),
contact: None,
license: None,
servers: Vec::new(),
security_schemes: HashMap::new(),
}
}
}
fn default_swagger_path() -> String {
"/docs".to_string()
}
fn default_redoc_path() -> String {
"/redoc".to_string()
}
fn default_openapi_json_path() -> String {
"/openapi.json".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactInfo {
pub name: Option<String>,
pub email: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseInfo {
pub name: String,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub url: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SecuritySchemeInfo {
#[serde(rename = "http")]
Http {
scheme: String,
#[serde(rename = "bearerFormat")]
bearer_format: Option<String>,
},
#[serde(rename = "apiKey")]
ApiKey {
#[serde(rename = "in")]
location: String,
name: String,
},
}
pub fn security_scheme_info_to_openapi(info: &SecuritySchemeInfo) -> SecurityScheme {
match info {
SecuritySchemeInfo::Http { scheme, bearer_format } => {
let mut http_scheme = SecurityScheme::Http(utoipa::openapi::security::Http::new(
utoipa::openapi::security::HttpAuthScheme::Bearer,
));
if let (SecurityScheme::Http(http), "bearer") = (&mut http_scheme, scheme.as_str()) {
http.scheme = utoipa::openapi::security::HttpAuthScheme::Bearer;
if let Some(format) = bearer_format {
http.bearer_format = Some(format.clone());
}
}
http_scheme
}
SecuritySchemeInfo::ApiKey { location, name } => {
use utoipa::openapi::security::ApiKey;
let api_key = match location.as_str() {
"header" => ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(name)),
"query" => ApiKey::Query(utoipa::openapi::security::ApiKeyValue::new(name)),
"cookie" => ApiKey::Cookie(utoipa::openapi::security::ApiKeyValue::new(name)),
_ => ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(name)),
};
SecurityScheme::ApiKey(api_key)
}
}
}
pub fn generate_openapi_spec(
routes: &[crate::RouteMetadata],
config: &OpenApiConfig,
_schema_registry: &SchemaRegistry,
server_config: Option<&crate::ServerConfig>,
) -> Result<utoipa::openapi::OpenApi, String> {
spec_generation::assemble_openapi_spec(routes, config, server_config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openapi_config_default() {
let config = OpenApiConfig::default();
assert!(!config.enabled);
assert_eq!(config.title, "API");
assert_eq!(config.version, "1.0.0");
assert_eq!(config.swagger_ui_path, "/docs");
assert_eq!(config.redoc_path, "/redoc");
assert_eq!(config.openapi_json_path, "/openapi.json");
}
#[test]
fn test_generate_minimal_spec() {
let config = OpenApiConfig {
enabled: true,
title: "Test API".to_string(),
version: "1.0.0".to_string(),
..Default::default()
};
let routes = vec![];
let registry = SchemaRegistry::new();
let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
assert_eq!(spec.info.title, "Test API");
assert_eq!(spec.info.version, "1.0.0");
}
#[test]
fn test_generate_spec_with_contact() {
let config = OpenApiConfig {
enabled: true,
title: "Test API".to_string(),
version: "1.0.0".to_string(),
contact: Some(ContactInfo {
name: Some("API Team".to_string()),
email: Some("api@example.com".to_string()),
url: Some("https://example.com".to_string()),
}),
..Default::default()
};
let routes = vec![];
let registry = SchemaRegistry::new();
let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
assert!(spec.info.contact.is_some());
let contact = spec.info.contact.unwrap();
assert_eq!(contact.name, Some("API Team".to_string()));
assert_eq!(contact.email, Some("api@example.com".to_string()));
}
#[test]
fn test_generate_spec_with_license() {
let config = OpenApiConfig {
enabled: true,
title: "Test API".to_string(),
version: "1.0.0".to_string(),
license: Some(LicenseInfo {
name: "MIT".to_string(),
url: Some("https://opensource.org/licenses/MIT".to_string()),
}),
..Default::default()
};
let routes = vec![];
let registry = SchemaRegistry::new();
let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
assert!(spec.info.license.is_some());
let license = spec.info.license.unwrap();
assert_eq!(license.name, "MIT");
}
#[test]
fn test_generate_spec_with_servers() {
let config = OpenApiConfig {
enabled: true,
title: "Test API".to_string(),
version: "1.0.0".to_string(),
servers: vec![
ServerInfo {
url: "https://api.example.com".to_string(),
description: Some("Production".to_string()),
},
ServerInfo {
url: "http://localhost:8080".to_string(),
description: Some("Development".to_string()),
},
],
..Default::default()
};
let routes = vec![];
let registry = SchemaRegistry::new();
let spec = generate_openapi_spec(&routes, &config, ®istry, None).unwrap();
assert!(spec.servers.is_some());
let servers = spec.servers.unwrap();
assert_eq!(servers.len(), 2);
assert_eq!(servers[0].url, "https://api.example.com");
assert_eq!(servers[1].url, "http://localhost:8080");
}
#[test]
fn test_security_scheme_http_bearer() {
let scheme_info = SecuritySchemeInfo::Http {
scheme: "bearer".to_string(),
bearer_format: Some("JWT".to_string()),
};
let scheme = security_scheme_info_to_openapi(&scheme_info);
match scheme {
SecurityScheme::Http(http) => {
assert!(matches!(http.scheme, utoipa::openapi::security::HttpAuthScheme::Bearer));
assert_eq!(http.bearer_format, Some("JWT".to_string()));
}
_ => panic!("Expected Http security scheme"),
}
}
#[test]
fn test_security_scheme_api_key() {
let scheme_info = SecuritySchemeInfo::ApiKey {
location: "header".to_string(),
name: "X-API-Key".to_string(),
};
let scheme = security_scheme_info_to_openapi(&scheme_info);
match scheme {
SecurityScheme::ApiKey(api_key) => {
assert!(matches!(api_key, utoipa::openapi::security::ApiKey::Header(_)));
}
_ => panic!("Expected ApiKey security scheme"),
}
}
}