Skip to main content

web_net/
route.rs

1//! Route declarations: a path, its methods and handlers, and its access.
2
3use std::sync::Arc;
4
5use serve_net::{Method, WorkClass};
6
7use crate::{Cx, Result};
8
9/// A request handler. Any `Fn(&mut Cx<'_, I>) -> web_net::Result` is one.
10pub trait Handler<I>: Send + Sync + 'static {
11    /// Handles one cleared request.
12    ///
13    /// # Errors
14    ///
15    /// An [`Error`](crate::Error), answered with the site's error page for
16    /// its status.
17    fn call(&self, cx: &mut Cx<'_, I>) -> Result;
18}
19
20impl<I, F> Handler<I> for F
21where
22    F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
23{
24    #[inline]
25    fn call(&self, cx: &mut Cx<'_, I>) -> Result {
26        self(cx)
27    }
28}
29
30/// Who may use a route.
31#[derive(Debug)]
32pub(crate) enum Access<R> {
33    /// Not declared: the build refuses the route.
34    Undeclared,
35    /// Anyone, guests included; the gate's `authorize` is not called.
36    Public,
37    /// Those the rule allows.
38    Rule(Arc<R>),
39}
40
41impl<R> Clone for Access<R> {
42    fn clone(&self) -> Self {
43        match self {
44            Self::Undeclared => Self::Undeclared,
45            Self::Public => Self::Public,
46            Self::Rule(r) => Self::Rule(Arc::clone(r)),
47        }
48    }
49}
50
51pub(crate) type BoxHandler<I> = Arc<dyn Handler<I>>;
52
53/// A route: a path pattern, one handler per method, and who may use it.
54///
55/// Patterns are made of literal segments, parameters (`:id`, one segment)
56/// and one wildcard at the end (`*rest`, the rest of the path). Literals are
57/// written decoded (`/café`); web-net compares them with the canonical
58/// request path.
59///
60/// ```
61/// use web_net::{App, get, Route};
62///
63/// let app = App::builder()
64///     .route(get("/", |cx| { cx.html(200, "<h1>Home</h1>"); Ok(()) }).public())
65///     .route(
66///         Route::at("/contact")
67///             .get(|cx| { cx.html(200, "<form method=post></form>"); Ok(()) })
68///             .post(|cx| { cx.see_other("/contact/thanks") })
69///             .public()
70///             // A real form gets CSRF protection: Builder::csrf and a gate
71///             // whose identities carry a binding.
72///             .csrf_exempt("example: the POST changes nothing"),
73///     )
74///     .build()?;
75/// # Ok::<(), web_net::BuildError>(())
76/// ```
77///
78/// A closure that reads fields of a custom identity types its parameter,
79/// because the closure is checked before the builder fixes the type (a
80/// named `fn` handler needs nothing extra):
81///
82/// ```
83/// use web_net::{Cx, Route, get};
84///
85/// struct User { name: String }
86/// impl web_net::Identity for User {
87///     fn is_guest(&self) -> bool { self.name.is_empty() }
88/// }
89/// enum Rule { SignedIn }
90///
91/// let account: Route<User, Rule> = get("/account", |cx: &mut Cx<'_, User>| {
92///     let name = cx.identity().name.clone();
93///     cx.text(200, &name);
94///     Ok(())
95/// })
96/// .allow(Rule::SignedIn);
97/// # let _ = account;
98/// ```
99pub struct Route<I, R> {
100    pub(crate) pattern: String,
101    pub(crate) handlers: Vec<(Method, BoxHandler<I>)>,
102    pub(crate) access: Access<R>,
103    pub(crate) name: Option<&'static str>,
104    pub(crate) work: WorkClass,
105    pub(crate) template: Option<&'static str>,
106    pub(crate) csrf_exempt: Option<&'static str>,
107    pub(crate) form_limits: form_net::Limits,
108    pub(crate) cors: Option<Arc<crate::Cors>>,
109    pub(crate) cache: crate::CachePolicy,
110}
111
112impl<I: 'static, R> Route<I, R> {
113    /// A route at `pattern` with no methods yet.
114    #[must_use]
115    pub fn at(pattern: impl Into<String>) -> Self {
116        Self {
117            pattern: pattern.into(),
118            handlers: Vec::new(),
119            access: Access::Undeclared,
120            name: None,
121            work: WorkClass::Quick,
122            template: None,
123            csrf_exempt: None,
124            form_limits: form_net::Limits::DEFAULT,
125            cors: None,
126            cache: crate::CachePolicy::Unset,
127        }
128    }
129
130    /// Adds a handler for `method`. `HEAD` follows `GET` unless it has its
131    /// own; `OPTIONS` is answered for every route unless it has its own.
132    #[must_use]
133    pub fn on(mut self, method: Method, handler: impl Handler<I>) -> Self {
134        self.handlers.push((method, Arc::new(handler)));
135        self
136    }
137
138    /// Adds a `GET` handler.
139    #[must_use]
140    pub fn get<F>(self, f: F) -> Self
141    where
142        F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
143    {
144        self.on(Method::Get, f)
145    }
146
147    /// Adds a `POST` handler.
148    #[must_use]
149    pub fn post<F>(self, f: F) -> Self
150    where
151        F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
152    {
153        self.on(Method::Post, f)
154    }
155
156    /// Adds a `PUT` handler.
157    #[must_use]
158    pub fn put<F>(self, f: F) -> Self
159    where
160        F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
161    {
162        self.on(Method::Put, f)
163    }
164
165    /// Adds a `PATCH` handler.
166    #[must_use]
167    pub fn patch<F>(self, f: F) -> Self
168    where
169        F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
170    {
171        self.on(Method::Patch, f)
172    }
173
174    /// Adds a `DELETE` handler.
175    #[must_use]
176    pub fn delete<F>(self, f: F) -> Self
177    where
178        F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
179    {
180        self.on(Method::Delete, f)
181    }
182
183    /// Anyone may use the route, guests included. Public is a declaration,
184    /// never a default: a route with neither `public` nor `allow` stops the
185    /// build.
186    #[must_use]
187    pub fn public(mut self) -> Self {
188        self.access = Access::Public;
189        self
190    }
191
192    /// Those `rule` allows may use the route; the gate's `authorize` decides
193    /// on every request.
194    #[must_use]
195    pub fn allow(mut self, rule: R) -> Self {
196        self.access = Access::Rule(Arc::new(rule));
197        self
198    }
199
200    /// A name for logs, events, and hooks.
201    #[must_use]
202    pub fn name(mut self, name: &'static str) -> Self {
203        self.name = Some(name);
204        self
205    }
206
207    /// How the handlers behave: `Quick` (the default), `Io`, or `Cpu`. A
208    /// dispatcher in front of web-net reads it through
209    /// [`App::classify`](crate::App::classify) to move slow work off the
210    /// connection's worker.
211    #[must_use]
212    pub fn work(mut self, work: WorkClass) -> Self {
213        self.work = work;
214        self
215    }
216
217    /// Takes the route out of CSRF protection, with the reason (a webhook
218    /// signed by its sender, an API authenticated by a bearer token that a
219    /// browser never sends on its own). The reason is required and must not
220    /// be empty: an exemption is a decision, never a default.
221    #[must_use]
222    pub fn csrf_exempt(mut self, reason: &'static str) -> Self {
223        self.csrf_exempt = Some(reason);
224        self
225    }
226
227    /// The budgets for reading this route's forms (field count, sizes, files;
228    /// default `form_net::Limits::DEFAULT`). Keep them to what the form
229    /// needs: they bound what one request can make the server hold.
230    #[must_use]
231    pub fn form_limits(mut self, limits: form_net::Limits) -> Self {
232        self.form_limits = limits;
233        self
234    }
235
236    /// The route's CORS policy: preflights answered, replies to allowed
237    /// origins readable by their scripts.
238    #[must_use]
239    pub fn cors(mut self, cors: crate::Cors) -> Self {
240        self.cors = Some(Arc::new(cors));
241        self
242    }
243
244    /// How the route's pages may be cached.
245    #[must_use]
246    pub fn cache(mut self, policy: crate::CachePolicy) -> Self {
247        self.cache = policy;
248        self
249    }
250
251    /// The template [`Cx::render`](crate::Cx::render) uses. Resolved when the
252    /// application is built: an unknown name stops the build.
253    #[must_use]
254    pub fn template(mut self, name: &'static str) -> Self {
255        self.template = Some(name);
256        self
257    }
258}
259
260impl<I, R> std::fmt::Debug for Route<I, R> {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        f.debug_struct("Route")
263            .field("pattern", &self.pattern)
264            .field(
265                "methods",
266                &self.handlers.iter().map(|(m, _)| *m).collect::<Vec<_>>(),
267            )
268            .field("name", &self.name)
269            .finish_non_exhaustive()
270    }
271}
272
273macro_rules! method_fn {
274    ($(#[$doc:meta])* $fn:ident, $m:ident) => {
275        $(#[$doc])*
276        #[must_use]
277        pub fn $fn<I: 'static, R, F>(pattern: impl Into<String>, f: F) -> Route<I, R>
278        where
279            F: Fn(&mut Cx<'_, I>) -> Result + Send + Sync + 'static,
280        {
281            Route::at(pattern).on(Method::$m, f)
282        }
283    };
284}
285
286method_fn!(
287    /// A route with a `GET` handler (and so `HEAD`).
288    get, Get
289);
290method_fn!(
291    /// A route with a `POST` handler.
292    post, Post
293);
294method_fn!(
295    /// A route with a `PUT` handler.
296    put, Put
297);
298method_fn!(
299    /// A route with a `PATCH` handler.
300    patch, Patch
301);
302method_fn!(
303    /// A route with a `DELETE` handler.
304    delete, Delete
305);
306
307/// Routes sharing a path prefix and a default access.
308///
309/// A route that declares its own access keeps it; one that declares none
310/// takes the group's.
311///
312/// ```
313/// use web_net::{App, Group, get};
314///
315/// let app = App::builder()
316///     .group(
317///         Group::new("/docs")
318///             .public()
319///             .route(get("/", |cx| { cx.text(200, "index"); Ok(()) }))
320///             .route(get("/:page", |cx| { cx.text(200, "page"); Ok(()) })),
321///     )
322///     .build()?;
323/// # Ok::<(), web_net::BuildError>(())
324/// ```
325pub struct Group<I, R> {
326    pub(crate) prefix: String,
327    pub(crate) access: Access<R>,
328    pub(crate) routes: Vec<Route<I, R>>,
329    pub(crate) cors: Option<Arc<crate::Cors>>,
330    pub(crate) cache: crate::CachePolicy,
331}
332
333impl<I: 'static, R> Group<I, R> {
334    /// A group under `prefix` (`/admin`).
335    #[must_use]
336    pub fn new(prefix: impl Into<String>) -> Self {
337        Self {
338            prefix: prefix.into(),
339            access: Access::Undeclared,
340            routes: Vec::new(),
341            cors: None,
342            cache: crate::CachePolicy::Unset,
343        }
344    }
345
346    /// The CORS policy of routes that declare none.
347    #[must_use]
348    pub fn cors(mut self, cors: crate::Cors) -> Self {
349        self.cors = Some(Arc::new(cors));
350        self
351    }
352
353    /// The cache policy of routes that declare none.
354    #[must_use]
355    pub fn cache(mut self, policy: crate::CachePolicy) -> Self {
356        self.cache = policy;
357        self
358    }
359
360    /// Routes without their own access are public.
361    #[must_use]
362    pub fn public(mut self) -> Self {
363        self.access = Access::Public;
364        self
365    }
366
367    /// Routes without their own access need `rule`.
368    #[must_use]
369    pub fn allow(mut self, rule: R) -> Self {
370        self.access = Access::Rule(Arc::new(rule));
371        self
372    }
373
374    /// Adds a route; its pattern is joined to the prefix.
375    #[must_use]
376    pub fn route(mut self, route: Route<I, R>) -> Self {
377        self.routes.push(route);
378        self
379    }
380
381    /// Adds a nested group; its prefix is joined to this one, and it takes
382    /// this group's access when it declares none.
383    #[must_use]
384    pub fn group(mut self, group: Group<I, R>) -> Self {
385        let inherited = match &group.access {
386            Access::Undeclared => self.access.clone(),
387            a => a.clone(),
388        };
389        let cors = group.cors.or_else(|| self.cors.clone());
390        let cache = if group.cache == crate::CachePolicy::Unset {
391            self.cache
392        } else {
393            group.cache
394        };
395        for mut r in group.routes {
396            r.pattern = join(&group.prefix, &r.pattern);
397            if matches!(r.access, Access::Undeclared) {
398                r.access = inherited.clone();
399            }
400            if r.cors.is_none() {
401                r.cors.clone_from(&cors);
402            }
403            if r.cache == crate::CachePolicy::Unset {
404                r.cache = cache;
405            }
406            self.routes.push(r);
407        }
408        self
409    }
410
411    pub(crate) fn into_routes(self) -> impl Iterator<Item = Route<I, R>> {
412        let prefix = self.prefix;
413        let access = self.access;
414        let cors = self.cors;
415        let cache = self.cache;
416        self.routes.into_iter().map(move |mut r| {
417            r.pattern = join(&prefix, &r.pattern);
418            if matches!(r.access, Access::Undeclared) {
419                r.access = access.clone();
420            }
421            if r.cors.is_none() {
422                r.cors.clone_from(&cors);
423            }
424            if r.cache == crate::CachePolicy::Unset {
425                r.cache = cache;
426            }
427            r
428        })
429    }
430}
431
432impl<I, R> std::fmt::Debug for Group<I, R> {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        f.debug_struct("Group")
435            .field("prefix", &self.prefix)
436            .field("routes", &self.routes)
437            .finish_non_exhaustive()
438    }
439}
440
441/// `/admin` + `/` = `/admin`; `/admin` + `/users` = `/admin/users`.
442pub(crate) fn join(prefix: &str, pattern: &str) -> String {
443    let p = prefix.trim_end_matches('/');
444    if pattern == "/" || pattern.is_empty() {
445        if p.is_empty() { "/".into() } else { p.into() }
446    } else if pattern.starts_with('/') {
447        format!("{p}{pattern}")
448    } else {
449        format!("{p}/{pattern}")
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::join;
456
457    #[test]
458    fn test_join() {
459        assert_eq!(join("/admin", "/"), "/admin");
460        assert_eq!(join("/admin/", "/users"), "/admin/users");
461        assert_eq!(join("/", "/x"), "/x");
462        assert_eq!(join("", "/"), "/");
463        assert_eq!(join("/a", "b"), "/a/b");
464    }
465}