use super::router::Router;
use crate::{
errors::HttpResult,
middleware::versioning::{ApiVersion, VersioningConfig},
request::ElifRequest,
response::IntoElifResponse,
};
use std::collections::HashMap;
use std::future::Future;
#[derive(Debug)]
pub struct VersionedRouter<S = ()>
where
S: Clone + Send + Sync + 'static,
{
pub version_routers: HashMap<String, Router<S>>,
pub versioning_config: VersioningConfig,
pub global_router: Option<Router<S>>,
pub base_path: String,
}
impl<S> Default for VersionedRouter<S>
where
S: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<S> VersionedRouter<S>
where
S: Clone + Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
version_routers: HashMap::new(),
versioning_config: VersioningConfig::builder().build().unwrap(),
global_router: None,
base_path: "/api".to_string(),
}
}
pub fn version(mut self, version: &str, router: Router<S>) -> Self {
self.version_routers.insert(version.to_string(), router);
self.versioning_config.add_version(
version.to_string(),
ApiVersion {
version: version.to_string(),
deprecated: false,
deprecation_message: None,
sunset_date: None,
is_default: self.version_routers.len() == 1, },
);
self
}
pub fn deprecate_version(
mut self,
version: &str,
message: Option<&str>,
sunset_date: Option<&str>,
) -> Self {
self.versioning_config.deprecate_version(
version,
message.map(|s| s.to_string()),
sunset_date.map(|s| s.to_string()),
);
self
}
pub fn default_version(mut self, version: &str) -> Self {
let (
versions,
strategy,
_,
include_deprecation_headers,
version_header_name,
version_param_name,
strict_validation,
) = self.versioning_config.clone_config();
let mut new_config = VersioningConfig::builder()
.versions(versions)
.strategy(strategy)
.include_deprecation_headers(include_deprecation_headers)
.version_header_name(version_header_name)
.version_param_name(version_param_name)
.strict_validation(strict_validation)
.default_version(Some(version.to_string()))
.build()
.unwrap();
new_config.add_version(
version.to_string(),
ApiVersion {
version: version.to_string(),
deprecated: false,
deprecation_message: None,
sunset_date: None,
is_default: true,
},
);
self.versioning_config = new_config;
self
}
pub fn strategy(mut self, strategy: crate::middleware::versioning::VersionStrategy) -> Self {
let (
versions,
_,
default_version,
include_deprecation_headers,
version_header_name,
version_param_name,
strict_validation,
) = self.versioning_config.clone_config();
self.versioning_config = VersioningConfig::builder()
.versions(versions)
.strategy(strategy)
.include_deprecation_headers(include_deprecation_headers)
.version_header_name(version_header_name)
.version_param_name(version_param_name)
.strict_validation(strict_validation)
.default_version(default_version)
.build()
.unwrap();
self
}
pub fn global(mut self, router: Router<S>) -> Self {
self.global_router = Some(router);
self
}
pub fn build(self) -> Router<S> {
let mut final_router = Router::new();
if let Some(global_router) = self.global_router {
final_router = final_router.merge(global_router);
}
for (version, version_router) in self.version_routers {
let version_path = match self.versioning_config.get_strategy() {
crate::middleware::versioning::VersionStrategy::UrlPath => {
format!("{}/{}", self.base_path, version)
}
_ => {
self.base_path.clone()
}
};
final_router = final_router.nest(&version_path, version_router);
}
let versioning_layer =
crate::middleware::versioning::versioning_layer(self.versioning_config);
let axum_router = final_router.into_axum_router();
let layered_router = axum_router.layer(versioning_layer);
Router::new().merge_axum(layered_router)
}
pub fn version_builder<'a>(&'a mut self, version: &str) -> VersionedRouteBuilder<'a, S> {
VersionedRouteBuilder::new(version, self)
}
}
pub struct VersionedRouteBuilder<'a, S>
where
S: Clone + Send + Sync + 'static,
{
version: String,
router: &'a mut VersionedRouter<S>,
current_router: Router<S>,
}
impl<'a, S> VersionedRouteBuilder<'a, S>
where
S: Clone + Send + Sync + 'static,
{
fn new(version: &str, router: &'a mut VersionedRouter<S>) -> Self {
Self {
version: version.to_string(),
router,
current_router: Router::new(),
}
}
pub fn get<F, Fut, R>(mut self, path: &str, handler: F) -> Self
where
F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
Fut: Future<Output = HttpResult<R>> + Send + 'static,
R: IntoElifResponse + Send + 'static,
{
self.current_router = self.current_router.get(path, handler);
self
}
pub fn post<F, Fut, R>(mut self, path: &str, handler: F) -> Self
where
F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
Fut: Future<Output = HttpResult<R>> + Send + 'static,
R: IntoElifResponse + Send + 'static,
{
self.current_router = self.current_router.post(path, handler);
self
}
pub fn put<F, Fut, R>(mut self, path: &str, handler: F) -> Self
where
F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
Fut: Future<Output = HttpResult<R>> + Send + 'static,
R: IntoElifResponse + Send + 'static,
{
self.current_router = self.current_router.put(path, handler);
self
}
pub fn delete<F, Fut, R>(mut self, path: &str, handler: F) -> Self
where
F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
Fut: Future<Output = HttpResult<R>> + Send + 'static,
R: IntoElifResponse + Send + 'static,
{
self.current_router = self.current_router.delete(path, handler);
self
}
pub fn patch<F, Fut, R>(mut self, path: &str, handler: F) -> Self
where
F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
Fut: Future<Output = HttpResult<R>> + Send + 'static,
R: IntoElifResponse + Send + 'static,
{
self.current_router = self.current_router.patch(path, handler);
self
}
pub fn finish(self) {
self.router
.version_routers
.insert(self.version.clone(), self.current_router);
}
}
pub fn versioned_router<S>() -> VersionedRouter<S>
where
S: Clone + Send + Sync + 'static,
{
VersionedRouter::<S>::new()
}
pub fn path_versioned_router<S>() -> VersionedRouter<S>
where
S: Clone + Send + Sync + 'static,
{
VersionedRouter::<S> {
version_routers: HashMap::new(),
versioning_config: VersioningConfig::builder()
.strategy(crate::middleware::versioning::VersionStrategy::UrlPath)
.build()
.unwrap(),
global_router: None,
base_path: "/api".to_string(),
}
}
pub fn header_versioned_router<S>(header_name: &str) -> VersionedRouter<S>
where
S: Clone + Send + Sync + 'static,
{
VersionedRouter::<S> {
version_routers: HashMap::new(),
versioning_config: VersioningConfig::builder()
.strategy(crate::middleware::versioning::VersionStrategy::Header(
header_name.to_string(),
))
.build()
.unwrap(),
global_router: None,
base_path: "/api".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::response::ElifJson;
#[tokio::test]
async fn test_versioned_router_creation() {
let router = VersionedRouter::<()>::new()
.version("v1", Router::new())
.version("v2", Router::new())
.default_version("v1")
.deprecate_version("v1", Some("Please use v2"), Some("2024-12-31"));
assert_eq!(router.version_routers.len(), 2);
assert!(router.version_routers.contains_key("v1"));
assert!(router.version_routers.contains_key("v2"));
let v1_version = router.versioning_config.get_version("v1").unwrap();
assert!(v1_version.deprecated);
assert_eq!(
v1_version.deprecation_message,
Some("Please use v2".to_string())
);
}
#[tokio::test]
async fn test_version_builder() {
let mut router = VersionedRouter::<()>::new();
router
.version_builder("v1")
.get("/users", |_req| async { Ok(ElifJson("users v1")) })
.post("/users", |_req| async { Ok(ElifJson("create user v1")) })
.finish();
assert!(router.version_routers.contains_key("v1"));
}
#[test]
fn test_convenience_functions() {
let _path_router = path_versioned_router::<()>();
let _header_router = header_versioned_router::<()>("Api-Version");
let _versioned_router = versioned_router::<()>();
}
}