use std::{
borrow::Cow,
collections::HashMap,
num::NonZeroUsize,
ops::Index,
pin::Pin,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use topcoat_core::{context::Cx, error::Result};
use crate::{
Body, EndpointIndex, HrefTarget, IntoPath, Layer, Methods, OwnedMethods, Path,
response::Response, route_endpoint,
};
pub type RouteFuture<'cx> = Pin<Box<dyn Future<Output = Result<Response>> + Send + 'cx>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RouteId(usize);
impl RouteId {
#[must_use]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
static NEXT: AtomicUsize = AtomicUsize::new(0);
Self(NEXT.fetch_add(1, Ordering::Relaxed))
}
}
pub trait Route: Send + Sync + 'static {
fn id(&self) -> RouteId;
fn methods(&self) -> Methods<'_>;
fn path(&self) -> &Path;
fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx>;
}
impl<R: Route + ?Sized> Route for &'static R {
fn id(&self) -> RouteId {
(**self).id()
}
fn methods(&self) -> Methods<'_> {
(**self).methods()
}
fn path(&self) -> &Path {
(**self).path()
}
fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
(**self).handle(cx, body)
}
}
#[cfg(feature = "discover")]
inventory::collect!(&'static dyn Route);
pub type RouteHandlerFn = for<'cx> fn(cx: &'cx Cx, body: Body) -> RouteFuture<'cx>;
#[derive(Debug, Clone)]
pub struct RouteFn {
id: RouteId,
methods: OwnedMethods,
path: Cow<'static, Path>,
handle: RouteHandlerFn,
}
impl RouteFn {
#[track_caller]
pub fn new(
methods: impl Into<OwnedMethods>,
path: impl IntoPath,
handle: RouteHandlerFn,
) -> Self {
Self {
id: RouteId::new(),
methods: methods.into(),
path: path.into_path(),
handle,
}
}
}
impl Route for RouteFn {
fn id(&self) -> RouteId {
self.id
}
fn methods(&self) -> Methods<'_> {
self.methods.as_methods()
}
fn path(&self) -> &Path {
&self.path
}
fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
(self.handle)(cx, body)
}
}
impl HrefTarget for RouteFn {
#[track_caller]
fn path<'cx>(&self, cx: &'cx Cx) -> &'cx Path {
match route_endpoint(cx, self.id) {
Some(endpoint) => endpoint.path(),
None => panic!(
"route `{}` is not registered on the router serving this request",
self.path
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RouteIndex(NonZeroUsize);
impl RouteIndex {
pub(crate) fn new(index: usize) -> Self {
Self(NonZeroUsize::new(index.wrapping_add(1)).expect("route index overflow"))
}
pub(crate) fn get(self) -> usize {
self.0.get() - 1
}
}
pub(crate) struct RegisteredRoute {
pub(crate) route: Box<dyn Route>,
pub(crate) endpoint: EndpointIndex,
pub(crate) layers: Box<[Arc<dyn Layer>]>,
}
#[derive(Default)]
pub(crate) struct Routes {
routes: Vec<RegisteredRoute>,
by_id: HashMap<RouteId, RouteIndex>,
}
impl Routes {
pub(crate) fn push(
&mut self,
route: Box<dyn Route>,
endpoint: EndpointIndex,
layers: Box<[Arc<dyn Layer>]>,
) -> RouteIndex {
let index = RouteIndex::new(self.routes.len());
self.by_id.insert(route.id(), index);
self.routes.push(RegisteredRoute {
route,
endpoint,
layers,
});
index
}
pub(crate) fn index_of(&self, id: RouteId) -> Option<RouteIndex> {
self.by_id.get(&id).copied()
}
}
impl Index<RouteIndex> for Routes {
type Output = RegisteredRoute;
fn index(&self, index: RouteIndex) -> &Self::Output {
&self.routes[index.get()]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_index_wraps_and_unwraps() {
let index = RouteIndex::new(7);
assert_eq!(index.get(), 7);
}
#[test]
fn route_index_zero_is_a_real_index() {
let index = RouteIndex::new(0);
assert_eq!(index.get(), 0);
}
#[test]
fn option_route_index_stays_one_word() {
assert_eq!(
std::mem::size_of::<Option<RouteIndex>>(),
std::mem::size_of::<usize>()
);
}
}