use std::collections::BTreeMap;
use std::sync::OnceLock;
use loco_rs::Error;
use serde::{Deserialize, Serialize};
use serde_json::Value;
static OPENAPI_CONFIG: OnceLock<Option<OpenAPIConfig>> = OnceLock::new();
#[derive(Debug)]
pub struct InitializerConfig<'a>(&'a Option<BTreeMap<String, Value>>);
impl<'a> From<&'a Option<BTreeMap<String, Value>>> for InitializerConfig<'a> {
fn from(initializers: &'a Option<BTreeMap<String, Value>>) -> Self {
InitializerConfig(initializers)
}
}
impl<'a> From<InitializerConfig<'a>> for Option<OpenAPIConfig> {
fn from(config: InitializerConfig<'a>) -> Self {
config
.0
.as_ref()
.and_then(|m| m.get("openapi"))
.cloned()
.and_then(|json| serde_json::from_value(json).ok())
}
}
pub fn set_openapi_config(
config: Option<OpenAPIConfig>,
) -> Result<Option<&'static OpenAPIConfig>, Error> {
Ok(OPENAPI_CONFIG.get_or_init(|| config).as_ref())
}
pub fn get_openapi_config() -> Option<&'static OpenAPIConfig> {
OPENAPI_CONFIG.get().unwrap_or(&None).as_ref()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct OpenAPIConfig {
#[cfg(feature = "redoc")]
#[serde(flatten)]
pub redoc: Option<OpenAPIType>,
#[cfg(feature = "scalar")]
#[serde(flatten)]
pub scalar: Option<OpenAPIType>,
#[cfg(feature = "swagger")]
#[serde(flatten)]
pub swagger: Option<OpenAPIType>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum OpenAPIType {
#[cfg(feature = "redoc")]
#[serde(rename = "redoc")]
Redoc {
url: String,
spec_json_url: Option<String>,
spec_yaml_url: Option<String>,
},
#[cfg(feature = "scalar")]
#[serde(rename = "scalar")]
Scalar {
url: String,
spec_json_url: Option<String>,
spec_yaml_url: Option<String>,
},
#[cfg(feature = "swagger")]
#[serde(rename = "swagger")]
Swagger {
url: String,
spec_json_url: String,
spec_yaml_url: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(feature = "swagger", feature = "redoc", feature = "scalar"))]
use serde_json::json;
#[cfg(any(feature = "swagger", feature = "redoc", feature = "scalar"))]
fn create_mock_config() -> BTreeMap<String, Value> {
let mut config = BTreeMap::new();
let mut openapi_config = serde_json::Map::new();
#[cfg(feature = "swagger")]
{
openapi_config.insert(
"swagger".to_string(),
json!({
"url": "/swagger",
"spec_json_url": "/api-docs/openapi.json"
}),
);
}
#[cfg(feature = "redoc")]
{
openapi_config.insert(
"redoc".to_string(),
json!({
"url": "/redoc",
"spec_json_url": "/redoc/openapi.json",
"spec_yaml_url": "/redoc/openapi.yaml"
}),
);
}
#[cfg(feature = "scalar")]
{
openapi_config.insert(
"scalar".to_string(),
json!({
"url": "/scalar",
"spec_json_url": "/scalar/openapi.json",
"spec_yaml_url": "/scalar/openapi.yaml"
}),
);
}
config.insert("openapi".to_string(), Value::Object(openapi_config));
config
}
#[test]
#[cfg(any(feature = "swagger", feature = "redoc", feature = "scalar"))]
fn test_data_conversion() {
let initializers = Some(create_mock_config());
let initializer_config: InitializerConfig = (&initializers).into();
let openapi_config: Option<OpenAPIConfig> = initializer_config.into();
assert!(
openapi_config.is_some(),
"OpenAPIConfig should be created successfully"
);
let config = openapi_config.unwrap();
#[cfg(feature = "swagger")]
{
let swagger = config.swagger.as_ref();
assert!(swagger.is_some(), "Swagger config should be present");
let expected = OpenAPIType::Swagger {
url: "/swagger".to_string(),
spec_json_url: "/api-docs/openapi.json".to_string(),
spec_yaml_url: None,
};
assert_eq!(swagger, Some(&expected));
}
#[cfg(feature = "redoc")]
{
let redoc = config.redoc.as_ref();
assert!(redoc.is_some(), "Redoc config should be present");
let expected = OpenAPIType::Redoc {
url: "/redoc".to_string(),
spec_json_url: Some("/redoc/openapi.json".to_string()),
spec_yaml_url: Some("/redoc/openapi.yaml".to_string()),
};
assert_eq!(redoc, Some(&expected));
}
#[cfg(feature = "scalar")]
{
let scalar = config.scalar.as_ref();
assert!(scalar.is_some(), "Scalar config should be present");
let expected = OpenAPIType::Scalar {
url: "/scalar".to_string(),
spec_json_url: Some("/scalar/openapi.json".to_string()),
spec_yaml_url: Some("/scalar/openapi.yaml".to_string()),
};
assert_eq!(scalar, Some(&expected));
}
}
#[test]
fn test_none_conversion() {
let initializers: Option<BTreeMap<String, Value>> = None;
let openapi_config: Option<OpenAPIConfig> = InitializerConfig::from(&initializers).into();
assert!(openapi_config.is_none(), "OpenAPIConfig should be None");
}
}