use std::{
borrow::Cow,
collections::HashMap,
ops::{Index, IndexMut},
};
use http::Method;
use crate::{Path, RouteIndex};
const STANDARD_METHODS: [Method; 9] = [
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::PATCH,
Method::HEAD,
Method::OPTIONS,
Method::TRACE,
Method::CONNECT,
];
const GET: usize = 0;
const HEAD: usize = 5;
fn standard_slot(method: &Method) -> Option<usize> {
match method.as_str() {
"GET" => Some(GET),
"POST" => Some(1),
"PUT" => Some(2),
"DELETE" => Some(3),
"PATCH" => Some(4),
"HEAD" => Some(HEAD),
"OPTIONS" => Some(6),
"TRACE" => Some(7),
"CONNECT" => Some(8),
_ => None,
}
}
#[derive(Debug)]
pub struct Endpoint {
standard: [Option<RouteIndex>; STANDARD_METHODS.len()],
other: HashMap<Method, RouteIndex>,
any: Option<RouteIndex>,
path: Box<str>,
}
impl Endpoint {
pub(crate) fn new(path: &Path) -> Self {
Self {
standard: [None; STANDARD_METHODS.len()],
other: HashMap::new(),
any: None,
path: path.as_str().into(),
}
}
pub(crate) fn with_path(&self, path: &Path) -> Self {
Self {
standard: self.standard,
other: self.other.clone(),
any: self.any,
path: path.as_str().into(),
}
}
#[must_use]
pub fn path(&self) -> &Path {
Path::new_unchecked(&self.path)
}
pub(crate) fn get(&self, method: &Method) -> Option<RouteIndex> {
match standard_slot(method) {
Some(slot) => self.standard[slot],
None => self.other.get(method).copied(),
}
}
pub(crate) fn any(&self) -> Option<RouteIndex> {
self.any
}
pub(crate) fn insert(&mut self, method: Method, index: RouteIndex) {
match standard_slot(&method) {
Some(slot) => self.standard[slot] = Some(index),
None => {
self.other.insert(method, index);
}
}
}
pub(crate) fn insert_any(&mut self, index: RouteIndex) {
self.any = Some(index);
}
pub(crate) fn alias_head_to_get(&mut self) {
if self.standard[HEAD].is_none() {
self.standard[HEAD] = self.standard[GET];
}
}
pub fn methods(&self) -> impl Iterator<Item = &Method> {
STANDARD_METHODS
.iter()
.enumerate()
.filter(|(slot, _)| self.standard[*slot].is_some())
.map(|(_, method)| method)
.chain(self.other.keys())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct EndpointIndex(usize);
#[derive(Default)]
pub(crate) struct Endpoints {
endpoints: Vec<Endpoint>,
matcher: matchit::Router<EndpointIndex>,
}
impl Endpoints {
#[track_caller]
pub(crate) fn push(&mut self, path: Cow<'static, str>, endpoint: Endpoint) -> EndpointIndex {
match self.try_push(path, endpoint) {
Ok(index) => index,
Err(with) => {
panic!("failed to register route: conflicts with registered route `{with}`")
}
}
}
pub(crate) fn try_push(
&mut self,
path: Cow<'static, str>,
endpoint: Endpoint,
) -> Result<EndpointIndex, String> {
let index = EndpointIndex(self.endpoints.len());
match self.matcher.insert(path, index) {
Ok(()) => {
self.endpoints.push(endpoint);
Ok(index)
}
Err(matchit::InsertError::Conflict { with }) => Err(with),
Err(error) => panic!("failed to register route: {error}"),
}
}
pub(crate) fn indices(&self) -> impl Iterator<Item = EndpointIndex> + use<> {
(0..self.endpoints.len()).map(EndpointIndex)
}
pub(crate) fn at<'s, 'url>(
&'s self,
url: &'url str,
) -> Option<(EndpointIndex, &'s Endpoint, matchit::Params<'s, 'url>)> {
let matched = self.matcher.at(url).ok()?;
let index = *matched.value;
Some((index, &self.endpoints[index.0], matched.params))
}
}
impl Index<EndpointIndex> for Endpoints {
type Output = Endpoint;
fn index(&self, EndpointIndex(index): EndpointIndex) -> &Self::Output {
&self.endpoints[index]
}
}
impl IndexMut<EndpointIndex> for Endpoints {
fn index_mut(&mut self, EndpointIndex(index): EndpointIndex) -> &mut Self::Output {
&mut self.endpoints[index]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn route(index: usize) -> RouteIndex {
RouteIndex::new(index)
}
fn empty() -> Endpoint {
Endpoint::new(Path::new("/x"))
}
#[test]
fn empty_endpoint_has_no_routes() {
let endpoint = empty();
assert_eq!(endpoint.get(&Method::GET), None);
assert_eq!(endpoint.get(&Method::POST), None);
assert_eq!(endpoint.methods().count(), 0);
}
#[test]
fn inserts_and_reads_back_standard_methods() {
let mut endpoint = empty();
endpoint.insert(Method::GET, route(0));
endpoint.insert(Method::POST, route(1));
endpoint.insert(Method::DELETE, route(2));
assert_eq!(endpoint.get(&Method::GET), Some(route(0)));
assert_eq!(endpoint.get(&Method::POST), Some(route(1)));
assert_eq!(endpoint.get(&Method::DELETE), Some(route(2)));
assert_eq!(endpoint.get(&Method::PUT), None);
}
#[test]
fn insert_overwrites_the_same_method() {
let mut endpoint = empty();
endpoint.insert(Method::GET, route(0));
endpoint.insert(Method::GET, route(5));
assert_eq!(endpoint.get(&Method::GET), Some(route(5)));
}
#[test]
fn inserts_and_reads_back_extension_methods() {
let purge = Method::from_bytes(b"PURGE").unwrap();
let mut endpoint = empty();
endpoint.insert(purge.clone(), route(3));
assert_eq!(endpoint.get(&purge), Some(route(3)));
assert_eq!(endpoint.get(&Method::GET), None);
}
#[test]
fn any_is_absent_by_default() {
let endpoint = empty();
assert_eq!(endpoint.any(), None);
}
#[test]
fn insert_any_does_not_affect_per_method_lookups() {
let mut endpoint = empty();
endpoint.insert_any(route(7));
assert_eq!(endpoint.any(), Some(route(7)));
assert_eq!(endpoint.get(&Method::GET), None);
assert_eq!(endpoint.methods().count(), 0);
}
#[test]
fn alias_points_head_at_get() {
let mut endpoint = empty();
endpoint.insert(Method::GET, route(4));
endpoint.alias_head_to_get();
assert_eq!(endpoint.get(&Method::HEAD), Some(route(4)));
}
#[test]
fn alias_does_not_override_explicit_head() {
let mut endpoint = empty();
endpoint.insert(Method::GET, route(4));
endpoint.insert(Method::HEAD, route(9));
endpoint.alias_head_to_get();
assert_eq!(endpoint.get(&Method::HEAD), Some(route(9)));
}
#[test]
fn alias_without_get_leaves_head_absent() {
let mut endpoint = empty();
endpoint.alias_head_to_get();
assert_eq!(endpoint.get(&Method::HEAD), None);
}
#[test]
fn methods_lists_standard_then_extension() {
let purge = Method::from_bytes(b"PURGE").unwrap();
let mut endpoint = empty();
endpoint.insert(Method::POST, route(1));
endpoint.insert(Method::GET, route(0));
endpoint.insert(purge.clone(), route(2));
let methods: Vec<&Method> = endpoint.methods().collect();
assert_eq!(methods, vec![&Method::GET, &Method::POST, &purge]);
}
fn endpoint_at(path: &'static str) -> (Cow<'static, str>, Endpoint) {
let path = Path::new(path);
(path.to_matchit_path(), Endpoint::new(path))
}
#[test]
fn push_assigns_sequential_indices() {
let mut endpoints = Endpoints::default();
let (path_x, x) = endpoint_at("/x");
let (path_y, y) = endpoint_at("/y");
assert_eq!(endpoints.push(path_x, x), EndpointIndex(0));
assert_eq!(endpoints.push(path_y, y), EndpointIndex(1));
}
#[test]
fn at_matches_a_url_to_its_endpoint() {
let mut endpoints = Endpoints::default();
let (path, endpoint) = endpoint_at("/users/{id}");
let pushed = endpoints.push(path, endpoint);
let (index, endpoint, params) = endpoints.at("/users/42").unwrap();
assert_eq!(index, pushed);
assert_eq!(endpoint.path(), Path::new("/users/{id}"));
assert_eq!(params.get("id"), Some("42"));
assert_eq!(endpoints[index].path(), Path::new("/users/{id}"));
}
#[test]
fn at_returns_none_for_an_unmatched_url() {
let mut endpoints = Endpoints::default();
let (path, endpoint) = endpoint_at("/x");
endpoints.push(path, endpoint);
assert!(endpoints.at("/missing").is_none());
}
#[test]
#[should_panic(expected = "failed to register route")]
fn push_rejects_conflicting_paths() {
let mut endpoints = Endpoints::default();
let (path, endpoint) = endpoint_at("/users/{id}");
endpoints.push(path, endpoint);
let (path, endpoint) = endpoint_at("/users/{user_id}");
endpoints.push(path, endpoint);
}
}