use std::sync::Arc;
use topcoat_core::context::Cx;
use crate::{
Body, Endpoint, Endpoints, Layer, Methods, Path, PathBuf, PathSegment, Route, RouteFuture,
RouteId, Routes, error::redirect_permanent, request::uri,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TrailingSlash {
#[default]
Redirect,
Serve,
Strict,
}
impl TrailingSlash {
pub(crate) fn register_twins(
self,
endpoints: &mut Endpoints,
routes: &mut Routes,
always_layers: &[Arc<dyn Layer>],
) {
if self == Self::Strict {
return;
}
for index in endpoints.indices() {
let endpoint = &endpoints[index];
let Some(path) = twin(endpoint.path()) else {
continue;
};
let twin = match self {
Self::Serve => endpoint.with_path(&path),
_ => Endpoint::new(&path),
};
let Ok(twin_index) = endpoints.try_push(path.to_matchit_path(), twin) else {
continue;
};
if self == Self::Redirect {
let route = Box::new(RedirectRoute::new(path));
let route = routes.push(route, twin_index, always_layers.into());
endpoints[twin_index].insert_any(route);
}
}
}
}
fn twin(path: &Path) -> Option<PathBuf> {
let mut segments = path.segments();
match segments.next_back()? {
PathSegment::CatchAll(_) => None,
PathSegment::Static("") => Some(segments.collect()),
_ => {
let mut twin = path.to_owned();
twin += PathSegment::Static("");
Some(twin)
}
}
}
struct RedirectRoute {
id: RouteId,
path: PathBuf,
}
impl RedirectRoute {
fn new(path: PathBuf) -> Self {
Self {
id: RouteId::new(),
path,
}
}
}
impl Route for RedirectRoute {
fn id(&self) -> RouteId {
self.id
}
fn methods(&self) -> Methods<'_> {
Methods::Any
}
fn path(&self) -> &Path {
&self.path
}
fn handle<'cx>(&'cx self, cx: &'cx Cx, _body: Body) -> RouteFuture<'cx> {
let request = uri(cx);
let mut target = request.path().to_owned();
if self.path.has_trailing_slash() {
target.pop();
} else {
target.push('/');
}
if let Some(query) = request.query() {
target.push('?');
target.push_str(query);
}
Box::pin(async move { Err(redirect_permanent(target).into()) })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn twin_of(path: &'static str) -> Option<String> {
twin(Path::new(path)).map(|twin| twin.to_string())
}
#[test]
fn twin_adds_a_trailing_slash() {
assert_eq!(twin_of("/users").as_deref(), Some("/users/"));
assert_eq!(twin_of("/users/{id}").as_deref(), Some("/users/{id}/"));
}
#[test]
fn twin_removes_a_trailing_slash() {
assert_eq!(twin_of("/users/").as_deref(), Some("/users"));
assert_eq!(twin_of("/users/{id}/").as_deref(), Some("/users/{id}"));
}
#[test]
fn the_root_has_no_twin() {
assert_eq!(twin_of("/"), None);
}
#[test]
fn a_catch_all_has_no_twin() {
assert_eq!(twin_of("/files/{*rest}"), None);
}
}