use crate::{ApiOperation, DocumentationError, openapi_document};
use axum::Router;
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct VersionError(String);
impl std::fmt::Display for VersionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for VersionError {}
#[derive(Clone)]
pub struct ApiVersion {
name: String,
router: Router,
operations: Vec<ApiOperation>,
}
impl ApiVersion {
pub fn new(
name: &str,
router: Router,
mut operations: Vec<ApiOperation>,
) -> Result<Self, VersionError> {
let number = name
.strip_prefix('v')
.and_then(|value| value.parse::<u32>().ok());
if !number.is_some_and(|value| value > 0 && name == format!("v{value}")) {
return Err(VersionError(
"API version must be canonical v1, v2, etc".into(),
));
}
for operation in &mut operations {
if !operation.path.starts_with('/') {
return Err(VersionError(
"version operation paths must start with /".into(),
));
}
operation.path = format!("/{name}{}", operation.path);
}
openapi_document("API", name, operations.clone())
.map_err(|error| VersionError(error.to_string()))?;
Ok(Self {
name: name.into(),
router,
operations,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn document(&self, title: &str) -> Result<Value, DocumentationError> {
openapi_document(title, &self.name, self.operations.clone())
}
pub fn operations(&self) -> &[ApiOperation] {
&self.operations
}
}
#[derive(Clone, Default)]
pub struct ApiVersions {
versions: Vec<ApiVersion>,
}
impl ApiVersions {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, version: ApiVersion) -> Result<Self, VersionError> {
if self
.versions
.iter()
.any(|existing| existing.name == version.name)
{
return Err(VersionError("duplicate API version".into()));
}
self.versions.push(version);
Ok(self)
}
pub fn versions(&self) -> &[ApiVersion] {
&self.versions
}
pub fn mount(&self, mut router: Router) -> Router {
for version in &self.versions {
router = router.nest(&format!("/{}", version.name), version.router.clone());
}
router
}
pub fn documents(&self, title: &str) -> Result<BTreeMap<String, Value>, DocumentationError> {
self.versions
.iter()
.map(|version| Ok((version.name.clone(), version.document(title)?)))
.collect()
}
}