Skip to main content

topcoat_router/
route.rs

1#[cfg(feature = "fs")]
2mod directory;
3
4use std::{
5    borrow::Cow,
6    collections::HashMap,
7    num::NonZeroUsize,
8    ops::Index,
9    pin::Pin,
10    sync::{
11        Arc,
12        atomic::{AtomicUsize, Ordering},
13    },
14};
15
16#[cfg(feature = "fs")]
17pub use directory::*;
18use topcoat_core::{context::Cx, error::Result};
19
20use crate::{
21    Body, EndpointIndex, HrefTarget, IntoPath, Layer, Methods, Next, OwnedMethods, Path, Terminal,
22    response::Response, route, route_endpoint,
23};
24
25/// The future returned by [`Route::handle`]: a boxed, `Send` future borrowing
26/// the route and its request context.
27pub type RouteFuture<'cx> = Pin<Box<dyn Future<Output = Result<Response>> + Send + 'cx>>;
28
29/// The identity of a registered handler.
30///
31/// Ids are drawn from a process-wide counter with [`new`](RouteId::new), so
32/// every handler in an application gets a distinct one.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct RouteId(usize);
35
36impl RouteId {
37    /// Draws the next id from the process-wide counter.
38    ///
39    /// A handler calls this once and keeps the result as its identity.
40    #[must_use]
41    #[allow(clippy::new_without_default)]
42    pub fn new() -> Self {
43        static NEXT: AtomicUsize = AtomicUsize::new(0);
44        Self(NEXT.fetch_add(1, Ordering::Relaxed))
45    }
46}
47
48/// A single routable endpoint: a set of HTTP methods, a URL path, and a
49/// handler.
50///
51/// This is the core primitive a [`Router`](crate::Router) dispatches to.
52/// Register any `Route` with [`RouterBuilder::route`](crate::RouterBuilder::route).
53pub trait Route: Send + Sync + 'static {
54    /// The identity of this route's handler.
55    fn id(&self) -> RouteId;
56
57    /// The HTTP methods this route responds to.
58    fn methods(&self) -> Methods<'_>;
59
60    /// The URL path this route handles.
61    fn path(&self) -> &Path;
62
63    /// Handles a request, producing a response.
64    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx>;
65
66    /// Returns whether this route handles the current request.
67    ///
68    /// Only the handler is compared, so a route is current for every value its
69    /// path parameters take, whatever the request's query or fragment.
70    ///
71    /// # Panics
72    ///
73    /// Panics if the request matched no route: either its path matched no
74    /// endpoint, or the endpoint holds no route for the request's method.
75    fn is_current(&self, cx: &Cx) -> bool {
76        route(cx).id() == self.id()
77    }
78}
79
80impl<R: Route + ?Sized> Route for &'static R {
81    fn id(&self) -> RouteId {
82        (**self).id()
83    }
84
85    fn methods(&self) -> Methods<'_> {
86        (**self).methods()
87    }
88
89    fn path(&self) -> &Path {
90        (**self).path()
91    }
92
93    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
94        (**self).handle(cx, body)
95    }
96}
97
98#[cfg(feature = "discover")]
99inventory::collect!(&'static dyn Route);
100
101/// The async handler function backing a [`RouteFn`].
102pub type RouteHandlerFn = for<'cx> fn(cx: &'cx Cx, body: Body) -> RouteFuture<'cx>;
103
104/// A [`Route`] backed by a plain handler function.
105///
106/// Turns a function into a route without implementing [`Route`] on a struct,
107/// pairing it with the methods and path it serves.
108#[derive(Debug, Clone)]
109pub struct RouteFn {
110    /// The identity of this route's handler.
111    id: RouteId,
112    /// The HTTP methods this route responds to.
113    methods: OwnedMethods,
114    /// The URL path this route handles.
115    path: Cow<'static, Path>,
116    /// The handler function that produces the response.
117    handle: RouteHandlerFn,
118}
119
120impl RouteFn {
121    /// Creates a new route with explicit methods, path, and handler function.
122    ///
123    /// The methods are anything convertible into [`OwnedMethods`]: a single
124    /// [`Method`](crate::Method), a `&'static [Method]`, a `Vec<Method>`, or
125    /// [`Methods::Any`] to respond to every method.
126    ///
127    /// ```rust
128    /// use topcoat::{
129    ///     context::Cx,
130    ///     router::{Body, Method, RouteFn, RouteFuture},
131    /// };
132    ///
133    /// fn handler(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
134    ///     Box::pin(async move { unimplemented!() })
135    /// }
136    ///
137    /// let form = RouteFn::new(&[Method::GET, Method::POST], "/form", handler);
138    /// ```
139    ///
140    /// # Panics
141    ///
142    /// Panics if `path` is a string that is not a well-formed route path.
143    #[track_caller]
144    pub fn new(
145        methods: impl Into<OwnedMethods>,
146        path: impl IntoPath,
147        handle: RouteHandlerFn,
148    ) -> Self {
149        Self {
150            id: RouteId::new(),
151            methods: methods.into(),
152            path: path.into_path(),
153            handle,
154        }
155    }
156}
157
158impl Route for RouteFn {
159    fn id(&self) -> RouteId {
160        self.id
161    }
162
163    fn methods(&self) -> Methods<'_> {
164        self.methods.as_methods()
165    }
166
167    fn path(&self) -> &Path {
168        &self.path
169    }
170
171    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
172        (self.handle)(cx, body)
173    }
174}
175
176impl HrefTarget for RouteFn {
177    #[track_caller]
178    fn path<'cx>(&self, cx: &'cx Cx) -> &'cx Path {
179        match route_endpoint(cx, self.id) {
180            Some(endpoint) => endpoint.path(),
181            None => panic!(
182                "route `{}` is not registered on the router serving this request",
183                self.path
184            ),
185        }
186    }
187}
188
189/// The position of a route in a router's [`Routes`] table.
190///
191/// Stored offset by one in a [`NonZeroUsize`] so that `Option<RouteIndex>`
192/// occupies a single word, keeping an endpoint's per-method table dense.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub(crate) struct RouteIndex(NonZeroUsize);
195
196impl RouteIndex {
197    /// Wraps a route's position in the table.
198    pub(crate) fn new(index: usize) -> Self {
199        Self(NonZeroUsize::new(index.wrapping_add(1)).expect("route index overflow"))
200    }
201
202    /// Returns the wrapped position.
203    pub(crate) fn get(self) -> usize {
204        self.0.get() - 1
205    }
206}
207
208/// A route paired with the layers that wrap it.
209pub(crate) struct RouteWithLayers {
210    /// The route itself.
211    route: Box<dyn Route>,
212    /// The layers wrapping this route, precomputed at build time from the
213    /// route's path (group segments included) and ordered from least- to
214    /// most-specific so the outermost layer runs first.
215    layers: Box<[Arc<dyn Layer>]>,
216}
217
218impl Route for RouteWithLayers {
219    fn id(&self) -> RouteId {
220        self.route.id()
221    }
222
223    fn methods(&self) -> Methods<'_> {
224        self.route.methods()
225    }
226
227    fn path(&self) -> &Path {
228        self.route.path()
229    }
230
231    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
232        Next::new(&self.layers, Terminal::Route(&*self.route)).run(cx, body)
233    }
234}
235
236/// The routes registered on a router, in registration order, indexed by
237/// [`RouteIndex`].
238///
239/// Routes are [`push`](Self::push)ed as the router is built, then only
240/// queried: [`endpoint`](Self::endpoint) resolves a route's [`RouteId`] to the
241/// endpoint serving it, and indexing by [`RouteIndex`] resolves a position
242/// back to the route and its layers.
243#[derive(Default)]
244pub(crate) struct Routes {
245    routes: Vec<RouteWithLayers>,
246    endpoint_lookup: HashMap<RouteId, EndpointIndex>,
247}
248
249impl Routes {
250    /// Registers `route` as served by `endpoint` and wrapped by `layers`,
251    /// returning the [`RouteIndex`] that now identifies the registration.
252    pub(crate) fn push(
253        &mut self,
254        route: Box<dyn Route>,
255        endpoint: EndpointIndex,
256        layers: Box<[Arc<dyn Layer>]>,
257    ) -> RouteIndex {
258        let index = RouteIndex::new(self.routes.len());
259        self.endpoint_lookup.insert(route.id(), endpoint);
260        self.routes.push(RouteWithLayers { route, layers });
261        index
262    }
263
264    /// Returns the endpoint serving the route registered under `id`, or `None` if
265    /// this router holds no route with that identity.
266    pub(crate) fn endpoint(&self, id: RouteId) -> Option<EndpointIndex> {
267        self.endpoint_lookup.get(&id).copied()
268    }
269}
270
271impl Index<RouteIndex> for Routes {
272    type Output = RouteWithLayers;
273
274    fn index(&self, index: RouteIndex) -> &Self::Output {
275        &self.routes[index.get()]
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    // -- RouteIndex --
284
285    #[test]
286    fn route_index_wraps_and_unwraps() {
287        let index = RouteIndex::new(7);
288        assert_eq!(index.get(), 7);
289    }
290
291    #[test]
292    fn route_index_zero_is_a_real_index() {
293        // The offset keeps index 0 representable despite the non-zero backing.
294        let index = RouteIndex::new(0);
295        assert_eq!(index.get(), 0);
296    }
297
298    #[test]
299    fn option_route_index_stays_one_word() {
300        assert_eq!(
301            std::mem::size_of::<Option<RouteIndex>>(),
302            std::mem::size_of::<usize>()
303        );
304    }
305}