Skip to main content

max / quasi

6.8 KB · 202 lines History Blame Raw
1 //! The route table, and dispatch through it.
2 //!
3 //! Decision 8 on the wiki note: `Router<S>`, generic over the app's own state.
4 //! quasi owns matching and dispatch, the app owns `S`. Axum-shaped on purpose,
5 //! so that a Rust developer recognises it, and concrete enough that a scaffolder
6 //! has something to generate.
7 //!
8 //! The rejected thin version, where quasi parses paths and each app writes its
9 //! own `match`, would leave this a path library. There has to be something that
10 //! only works if you buy in, or the stack claim is documentation.
11
12 use crate::error::{Class, RouteError};
13 use crate::path::Pattern;
14 use crate::request::{Method, Request};
15 use crate::response::Response;
16
17 /// What a route does.
18 ///
19 /// Decision 6: sync, no `async`. The two renderers that ship at launch decide
20 /// it. egui calls the router inside a frame and the terminal inside an event
21 /// loop, and neither can await without a runtime it does not otherwise need;
22 /// audiofiles has no tokio at all today and this keeps it that way. It also
23 /// matches the store, since the desktop apps moved to rusqlite and took their
24 /// repository traits sync in the same pass. A hosted axum pays a
25 /// `spawn_blocking`, which is what an axum handler over a blocking store pays
26 /// anyway.
27 ///
28 /// A function pointer rather than a boxed closure. Handlers are free functions
29 /// taking the app's state by reference, which is the whole discipline: a
30 /// handler that needed to capture something would be holding state the router
31 /// cannot see. It is also the widenable direction, since an `fn` coerces into a
32 /// `Box<dyn Fn>` and nothing coerces back.
33 pub type Handler<S> = fn(&S, Request) -> Result<Response, RouteError>;
34
35 /// One registered route.
36 struct Route<S> {
37 method: Method,
38 pattern: Pattern,
39 handler: Handler<S>,
40 }
41
42 /// The route table.
43 ///
44 /// Built once at startup and read for the life of the program. Registration
45 /// order does not matter: routes are kept most-specific-first, so `/task/new`
46 /// is tried before `/task/:id` however they were declared.
47 pub struct Router<S> {
48 routes: Vec<Route<S>>,
49 }
50
51 impl<S> Router<S> {
52 /// An empty table.
53 #[must_use]
54 pub fn new() -> Self {
55 Self { routes: Vec::new() }
56 }
57
58 /// Register a read.
59 #[must_use]
60 pub fn get(self, path: &str, handler: Handler<S>) -> Self {
61 self.route(Method::Get, path, handler)
62 }
63
64 /// Register a write.
65 #[must_use]
66 pub fn post(self, path: &str, handler: Handler<S>) -> Self {
67 self.route(Method::Post, path, handler)
68 }
69
70 /// Register a write that removes what is at the address.
71 #[must_use]
72 pub fn delete(self, path: &str, handler: Handler<S>) -> Self {
73 self.route(Method::Delete, path, handler)
74 }
75
76 /// Register a write that replaces what is at the address.
77 #[must_use]
78 pub fn put(self, path: &str, handler: Handler<S>) -> Self {
79 self.route(Method::Put, path, handler)
80 }
81
82 /// Register a route.
83 ///
84 /// # Panics
85 ///
86 /// If the path is malformed, or if the same method and pattern are already
87 /// registered. Both are bugs in a startup literal, and a route table that
88 /// silently keeps the first of two registrations is a bug that presents as
89 /// a screen quietly not updating months later.
90 #[must_use]
91 pub fn route(mut self, method: Method, path: &str, handler: Handler<S>) -> Self {
92 let pattern = Pattern::parse(path);
93
94 assert!(
95 !self
96 .routes
97 .iter()
98 .any(|r| r.method == method && r.pattern == pattern),
99 "route `{method} {path}` is registered twice"
100 );
101
102 // Most specific first, and stable within one specificity so that two
103 // equally specific routes keep the order they were written in.
104 let at = self
105 .routes
106 .partition_point(|r| r.pattern.specificity() >= pattern.specificity());
107 self.routes.insert(
108 at,
109 Route {
110 method,
111 pattern,
112 handler,
113 },
114 );
115 self
116 }
117
118 /// Answer a request.
119 ///
120 /// The host has already split what it parsed: the query string is the view
121 /// the control was offered under, the form body is what the control sent.
122 /// See [`Request`] for why those are two things. Path captures are filled in
123 /// here, into a third bag, because a capture is the route's own and was not
124 /// sent by anybody.
125 pub fn handle(&self, state: &S, request: Request) -> Result<Response, RouteError> {
126 let mut wrong_method = false;
127
128 for route in &self.routes {
129 let Some(captures) = route.pattern.match_path(&request.path) else {
130 continue;
131 };
132 if route.method != request.method {
133 wrong_method = true;
134 continue;
135 }
136
137 return (route.handler)(
138 state,
139 Request {
140 captures,
141 ..request
142 },
143 );
144 }
145
146 let (method, path) = (request.method, &request.path);
147
148 // A path that exists under another verb is still a `NotFound` rather
149 // than an `Internal`, even though reaching it means our own renderer
150 // emitted the wrong verb. The reason is what a host does with the
151 // class: an HTTP adapter answering 500 to a probe turns a scan into a
152 // page, and the message carries the detail an operator needs anyway.
153 Err(RouteError::new(
154 Class::NotFound,
155 if wrong_method {
156 format!("no route for {method} {path}, though the path answers another method")
157 } else {
158 format!("no route for {method} {path}")
159 },
160 ))
161 }
162
163 /// Every registered route, most specific first.
164 ///
165 /// For a scaffolder generating a client, a test asserting the table, and an
166 /// adapter that wants to log what it is serving.
167 pub fn routes(&self) -> impl Iterator<Item = (Method, &str)> {
168 self.routes.iter().map(|r| (r.method, r.pattern.source()))
169 }
170
171 /// How many routes are registered.
172 #[must_use]
173 pub fn len(&self) -> usize {
174 self.routes.len()
175 }
176
177 /// Whether the table is empty.
178 #[must_use]
179 pub fn is_empty(&self) -> bool {
180 self.routes.is_empty()
181 }
182 }
183
184 impl<S> Default for Router<S> {
185 fn default() -> Self {
186 Self::new()
187 }
188 }
189
190 impl<S> std::fmt::Debug for Router<S> {
191 /// The table, without pretending a function pointer is worth printing.
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_list()
194 .entries(
195 self.routes
196 .iter()
197 .map(|r| format!("{} {}", r.method, r.pattern.source())),
198 )
199 .finish()
200 }
201 }
202