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