//! The route table, and dispatch through it. //! //! Decision 8 on the wiki note: `Router`, generic over the app's own state. //! quasi owns matching and dispatch, the app owns `S`. Axum-shaped on purpose, //! so that a Rust developer recognises it, and concrete enough that a scaffolder //! has something to generate. //! //! The rejected thin version, where quasi parses paths and each app writes its //! own `match`, would leave this a path library. There has to be something that //! only works if you buy in, or the stack claim is documentation. use crate::error::{Class, RouteError}; use crate::path::Pattern; use crate::request::{Method, Request}; use crate::response::Response; /// What a route does. /// /// Decision 6: sync, no `async`. The two renderers that ship at launch decide /// it. egui calls the router inside a frame and the terminal inside an event /// loop, and neither can await without a runtime it does not otherwise need; /// audiofiles has no tokio at all today and this keeps it that way. It also /// matches the store, since the desktop apps moved to rusqlite and took their /// repository traits sync in the same pass. A hosted axum pays a /// `spawn_blocking`, which is what an axum handler over a blocking store pays /// anyway. /// /// A function pointer rather than a boxed closure. Handlers are free functions /// taking the app's state by reference, which is the whole discipline: a /// handler that needed to capture something would be holding state the router /// cannot see. It is also the widenable direction, since an `fn` coerces into a /// `Box` and nothing coerces back. pub type Handler = fn(&S, Request) -> Result; /// One registered route. struct Route { method: Method, pattern: Pattern, handler: Handler, } /// The route table. /// /// Built once at startup and read for the life of the program. Registration /// order does not matter: routes are kept most-specific-first, so `/task/new` /// is tried before `/task/:id` however they were declared. pub struct Router { routes: Vec>, } impl Router { /// An empty table. #[must_use] pub fn new() -> Self { Self { routes: Vec::new() } } /// Register a read. #[must_use] pub fn get(self, path: &str, handler: Handler) -> Self { self.route(Method::Get, path, handler) } /// Register a write. #[must_use] pub fn post(self, path: &str, handler: Handler) -> Self { self.route(Method::Post, path, handler) } /// Register a write that removes what is at the address. #[must_use] pub fn delete(self, path: &str, handler: Handler) -> Self { self.route(Method::Delete, path, handler) } /// Register a write that replaces what is at the address. #[must_use] pub fn put(self, path: &str, handler: Handler) -> Self { self.route(Method::Put, path, handler) } /// Register a route. /// /// # Panics /// /// If the path is malformed, or if the same method and pattern are already /// registered. Both are bugs in a startup literal, and a route table that /// silently keeps the first of two registrations is a bug that presents as /// a screen quietly not updating months later. #[must_use] pub fn route(mut self, method: Method, path: &str, handler: Handler) -> Self { let pattern = Pattern::parse(path); assert!( !self .routes .iter() .any(|r| r.method == method && r.pattern == pattern), "route `{method} {path}` is registered twice" ); // Most specific first, and stable within one specificity so that two // equally specific routes keep the order they were written in. let at = self .routes .partition_point(|r| r.pattern.specificity() >= pattern.specificity()); self.routes.insert( at, Route { method, pattern, handler, }, ); self } /// Answer a request. /// /// The host has already split what it parsed: the query string is the view /// the control was offered under, the form body is what the control sent. /// See [`Request`] for why those are two things. Path captures are filled in /// here, into a third bag, because a capture is the route's own and was not /// sent by anybody. pub fn handle(&self, state: &S, request: Request) -> Result { let mut wrong_method = false; for route in &self.routes { let Some(captures) = route.pattern.match_path(&request.path) else { continue; }; if route.method != request.method { wrong_method = true; continue; } return (route.handler)( state, Request { captures, ..request }, ); } let (method, path) = (request.method, &request.path); // A path that exists under another verb is still a `NotFound` rather // than an `Internal`, even though reaching it means our own renderer // emitted the wrong verb. The reason is what a host does with the // class: an HTTP adapter answering 500 to a probe turns a scan into a // page, and the message carries the detail an operator needs anyway. Err(RouteError::new( Class::NotFound, if wrong_method { format!("no route for {method} {path}, though the path answers another method") } else { format!("no route for {method} {path}") }, )) } /// Every registered route, most specific first. /// /// For a scaffolder generating a client, a test asserting the table, and an /// adapter that wants to log what it is serving. pub fn routes(&self) -> impl Iterator { self.routes.iter().map(|r| (r.method, r.pattern.source())) } /// How many routes are registered. #[must_use] pub fn len(&self) -> usize { self.routes.len() } /// Whether the table is empty. #[must_use] pub fn is_empty(&self) -> bool { self.routes.is_empty() } } impl Default for Router { fn default() -> Self { Self::new() } } impl std::fmt::Debug for Router { /// The table, without pretending a function pointer is worth printing. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_list() .entries( self.routes .iter() .map(|r| format!("{} {}", r.method, r.pattern.source())), ) .finish() } }