mod pattern;
mod route;
mod service;
pub(crate) use self::pattern::Pattern;
pub use self::route::Path;
pub(crate) use self::route::Route;
use crate::endpoint::Endpoint;
use crate::middleware::Middleware;
use crate::{Request, Response};
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::watch;
pub struct Router {
regex: regex::RegexSet,
routes: Vec<Arc<Route>>,
middleware: Vec<Pin<Box<dyn Middleware>>>,
fallback: Option<Pin<Box<dyn Endpoint>>>,
terminate: Option<watch::Receiver<bool>>,
}
impl Default for Router {
fn default() -> Self {
Router {
regex: regex::RegexSet::empty(),
middleware: vec![],
routes: vec![],
fallback: None,
terminate: None,
}
}
}
impl Router {
#[allow(clippy::missing_panics_doc)]
pub fn prepare(&mut self) {
let patterns = self
.routes
.iter()
.map(|route| route.pattern.regex().as_str());
let set = regex::RegexSet::new(patterns).unwrap();
self.regex = set;
}
pub(crate) fn routes(&self) -> &[Arc<Route>] {
&self.routes[..]
}
pub fn at<P: AsRef<str>>(&mut self, prefix: P) -> Path<'_> {
Path::new(join_paths("", prefix.as_ref()), &mut self.routes)
}
pub fn under<P: AsRef<str>, F: FnOnce(&mut Path<'_>)>(
&mut self,
prefix: P,
build: F,
) -> &mut Self {
let mut path = Path::new(join_paths("", prefix.as_ref()), &mut self.routes);
build(&mut path);
self
}
pub fn with<M: Middleware>(&mut self, middleware: M) -> &mut Self {
self.middleware.push(Box::pin(middleware));
self
}
pub fn fallback<E: Endpoint>(&mut self, endpoint: E) -> &mut Self {
self.fallback = Some(Box::pin(endpoint));
self
}
pub fn termination_signal(&mut self) -> watch::Sender<bool> {
let (tx, rx) = watch::channel(false);
self.terminate = Some(rx);
tx
}
pub async fn handle(&self, request: Request) -> Result<Response, anyhow::Error> {
Pin::new(self).apply(request).await
}
pub(crate) fn lookup(&self, path: &str, method: &http::Method) -> Option<Arc<Route>> {
self.regex
.matches(path)
.into_iter()
.map(|i| &self.routes[i])
.filter(|r| r.matches(method))
.next_back()
.cloned()
}
fn fallback_endpoint(&self) -> Option<Pin<&dyn Endpoint>> {
self.fallback.as_ref().map(Pin::as_ref)
}
}
#[async_trait]
impl crate::Endpoint for Router {
async fn apply(self: Pin<&Self>, mut request: Request) -> Result<Response, anyhow::Error> {
let route = self.lookup(request.uri().path(), request.method());
if let Some(route) = route.clone() {
if let Some(fragment) =
crate::request::fragment::Fragment::new(request.uri().path(), &route)
{
request.extensions_mut().insert(fragment);
}
request.extensions_mut().insert(route);
}
let endpoint = {
let route_endpoint = || route.as_ref().map(|e| e.endpoint().as_ref());
let fallback_endpoint = || self.fallback_endpoint();
route_endpoint()
.or_else(fallback_endpoint)
.unwrap_or_else(default_endpoint)
};
log::trace!("{} {} --> {:?}", request.method(), request.uri(), endpoint);
let next = crate::middleware::Next::new(&self.middleware[..], endpoint);
next.apply(request).await
}
}
impl std::fmt::Debug for Router {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Router")
.field("regex", &self.regex)
.field("routes", &self.routes)
.finish()
}
}
lazy_static::lazy_static! {
static ref DEFAULT_ENDPOINT: crate::endpoints::SyncEndpoint<fn(Request) -> Response> = crate::endpoints::SyncEndpoint::new(|_| Response::empty_500());
static ref DEFAULT_ENDPOINT_PIN: Pin<&'static (dyn Endpoint + Unpin + 'static)> = Pin::new(&*DEFAULT_ENDPOINT);
}
pub(crate) fn default_endpoint<'r>() -> Pin<&'r dyn Endpoint> {
*DEFAULT_ENDPOINT_PIN
}
fn join_paths(base: &str, extend: &str) -> String {
let mut buffer = String::with_capacity(base.len() + extend.len());
buffer.push_str(base);
match (base.ends_with('/'), extend.starts_with('/')) {
(true, true) => {
buffer.push_str(&extend[1..]);
}
(false, true) | (true, false) => {
buffer.push_str(extend);
}
(false, false) => {
buffer.push('/');
buffer.push_str(extend);
}
}
buffer.shrink_to_fit();
buffer
}
#[cfg(test)]
mod test {
use super::*;
use crate::request::Request;
use crate::response::Response;
use crate::UnderError;
#[allow(clippy::unused_async)]
async fn simple_endpoint(_: Request) -> Result<Response, UnderError> {
unimplemented!()
}
fn simple_router() -> Router {
let mut router = Router::default();
router.at("/").get(simple_endpoint);
router.at("/alpha").get(simple_endpoint);
router.at("/beta/{id}").get(simple_endpoint);
router.at("/gamma/{all:path}").get(simple_endpoint);
router.prepare();
router
}
#[test]
fn test_join_paths() {
assert_eq!(join_paths("", "/id"), "/id");
assert_eq!(join_paths("", "id"), "/id");
assert_eq!(join_paths("/user", "/id"), "/user/id");
assert_eq!(join_paths("/user/", "/id"), "/user/id");
assert_eq!(join_paths("/user/", "id"), "/user/id");
}
#[test]
fn test_build() {
simple_router();
}
#[test]
fn test_basic_match() {
let router = simple_router();
dbg!(&router);
let result = router.lookup("/", &http::Method::GET);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!("/", &result.path);
}
#[test]
fn test_simple_match() {
let router = simple_router();
let result = router.lookup("/beta/4444", &http::Method::GET);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!("/beta/{id}", &result.path);
}
#[test]
fn test_multi_match() {
let router = simple_router();
let result = router.lookup("/gamma/a/b/c", &http::Method::GET);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!("/gamma/{all:path}", &result.path);
}
#[test]
fn test_missing_match() {
let router = simple_router();
let result = router.lookup("/omega/aaa", &http::Method::GET);
assert!(result.is_none());
}
#[test]
fn test_correct_method() {
let router = simple_router();
let result = router.lookup("/alpha", &http::Method::POST);
assert!(result.is_none());
}
}