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::{Context, NoRender, Render, Result};
8
9/// A request handler. Any `Fn(&mut Context<'_, I, E>) -> web_net::Result` is one.
10///
11/// ```
12/// use serve_net::Method;
13/// use web_net::{App, Context, Handler, Render, Result, Route};
14///
15/// // A handler that carries its own data.
16/// struct Greeting(&'static str);
17/// impl<I, E: Render> Handler<I, E> for Greeting {
18///     fn call(&self, cx: &mut Context<'_, I, E>) -> Result {
19///         cx.text(200, self.0)
20///     }
21/// }
22///
23/// let app = App::builder().route(Route::at("/").on(Method::Get, Greeting("hello"))).build()?;
24/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
25/// assert_eq!(r.text(), "hello");
26/// # Ok::<(), web_net::BuildError>(())
27/// ```
28pub trait Handler<I, E: Render = NoRender>: Send + Sync + 'static {
29    /// Handles one cleared request.
30    ///
31    /// # Errors
32    ///
33    /// An [`Error`](crate::Error), answered with the site's error page for
34    /// its status.
35    fn call(&self, cx: &mut Context<'_, I, E>) -> Result;
36}
37
38impl<I, E: Render, F> Handler<I, E> for F
39where
40    F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
41{
42    #[inline]
43    fn call(&self, cx: &mut Context<'_, I, E>) -> Result {
44        self(cx)
45    }
46}
47
48/// Who may use a route.
49#[derive(Debug)]
50pub(crate) enum Access<R> {
51    /// Not declared: the build refuses the route.
52    Undeclared,
53    /// Anyone, guests included; the gate's `authorize` is not called.
54    Public,
55    /// Those every rule allows: the groups' rules, outermost first, then the
56    /// route's own.
57    Rules(Vec<Arc<R>>),
58    /// Declared public inside a group with a rule: the build refuses it.
59    PublicInRuled,
60}
61
62impl<R> Clone for Access<R> {
63    fn clone(&self) -> Self {
64        match self {
65            Self::Undeclared => Self::Undeclared,
66            Self::Public => Self::Public,
67            Self::Rules(r) => Self::Rules(r.clone()),
68            Self::PublicInRuled => Self::PublicInRuled,
69        }
70    }
71}
72
73impl<R> Access<R> {
74    /// This access for a route inside a group whose access is `outer`: a
75    /// group's rule always holds, and a route's own rule adds to it.
76    fn within(self, outer: &Self) -> Self {
77        match (outer, self) {
78            (Self::Rules(o), Self::Undeclared) => Self::Rules(o.clone()),
79            (Self::Rules(o), Self::Rules(own)) => {
80                Self::Rules(o.iter().cloned().chain(own).collect())
81            }
82            (Self::Rules(_), Self::Public | Self::PublicInRuled) => Self::PublicInRuled,
83            (Self::Public, Self::Undeclared) => Self::Public,
84            (_, own) => own,
85        }
86    }
87}
88
89pub(crate) type BoxHandler<I, E> = Arc<dyn Handler<I, E>>;
90
91/// A route: a path pattern, one handler per method, and who may use it.
92///
93/// Patterns are made of literal segments, parameters (`:id`, one segment)
94/// and one wildcard at the end (`*rest`, the rest of the path). Literals are
95/// written decoded (`/café`); web-net compares them with the canonical
96/// request path.
97///
98/// ```
99/// use web_net::{App, get, Route};
100///
101/// let app = App::builder()
102///     .route(get("/", |cx| cx.html(200, "<h1>Home</h1>")))
103///     .route(
104///         Route::at("/contact")
105///             .get(|cx| cx.html(200, "<form method=post></form>"))
106///             .post(|cx| cx.redirect(303, "/contact/thanks"))
107///             // A real form gets CSRF protection: Builder::csrf and a gate
108///             // whose identities carry a binding.
109///             .csrf_exempt("example: the POST changes nothing"),
110///     )
111///     .build()?;
112/// # Ok::<(), web_net::BuildError>(())
113/// ```
114///
115/// A closure that reads fields of a custom identity types its parameter,
116/// because the closure is checked before the builder fixes the type (a
117/// named `fn` handler needs nothing extra):
118///
119/// ```
120/// use web_net::{Context, Route, get};
121///
122/// struct User { name: String }
123/// impl web_net::Identity for User {
124///     fn is_guest(&self) -> bool { self.name.is_empty() }
125/// }
126/// enum Rule { SignedIn }
127///
128/// let account: Route<User, Rule> = get("/account", |cx: &mut Context<'_, User>| {
129///     let name = cx.identity().name.clone();
130///     cx.text(200, &name)
131/// })
132/// .allow(Rule::SignedIn);
133/// # let _ = account;
134/// ```
135pub struct Route<I, R, E: Render = NoRender> {
136    pub(crate) pattern: String,
137    pub(crate) handlers: Vec<(Method, BoxHandler<I, E>)>,
138    pub(crate) access: Access<R>,
139    pub(crate) name: Option<&'static str>,
140    pub(crate) work: WorkClass,
141    pub(crate) template: Option<&'static str>,
142    pub(crate) csrf_exempt: Option<&'static str>,
143    pub(crate) form_limits: form_net::Limits,
144    pub(crate) cors: Option<Arc<crate::Cors>>,
145    pub(crate) cache: crate::CachePolicy,
146}
147
148impl<I: 'static, R, E: Render> Route<I, R, E> {
149    /// A route at `pattern` with no methods yet.
150    ///
151    /// ```
152    /// use web_net::{App, Route};
153    ///
154    /// let app = App::builder()
155    ///     .route(Route::at("/about").get(|cx| cx.text(200, "about")))
156    ///     .build()?;
157    /// let r = serve_net::testing::respond(&app, b"GET /about HTTP/1.1\r\nHost: localhost\r\n\r\n");
158    /// assert_eq!(r.text(), "about");
159    /// # Ok::<(), web_net::BuildError>(())
160    /// ```
161    #[must_use]
162    pub fn at(pattern: impl Into<String>) -> Self {
163        Self {
164            pattern: pattern.into(),
165            handlers: Vec::new(),
166            access: Access::Undeclared,
167            name: None,
168            work: WorkClass::Quick,
169            template: None,
170            csrf_exempt: None,
171            form_limits: form_net::Limits::DEFAULT,
172            cors: None,
173            cache: crate::CachePolicy::Unset,
174        }
175    }
176
177    /// Adds a handler for `method`. `HEAD` follows `GET` unless it has its
178    /// own; `OPTIONS` is answered for every route unless it has its own.
179    ///
180    /// ```
181    /// use serve_net::Method;
182    /// use web_net::{App, Context, Guest, Route};
183    ///
184    /// fn store(cx: &mut Context<'_, Guest>) -> web_net::Result {
185    ///     cx.text(200, "stored")
186    /// }
187    ///
188    /// let app = App::builder()
189    ///     .route(Route::at("/items").on(Method::Put, store).csrf_exempt("example: no browser form"))
190    ///     .build()?;
191    /// let r = serve_net::testing::respond(
192    ///     &app,
193    ///     b"PUT /items HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
194    /// );
195    /// assert_eq!(r.text(), "stored");
196    /// # Ok::<(), web_net::BuildError>(())
197    /// ```
198    #[must_use]
199    pub fn on(mut self, method: Method, handler: impl Handler<I, E>) -> Self {
200        self.handlers.push((method, Arc::new(handler)));
201        self
202    }
203
204    /// Adds a `GET` handler.
205    ///
206    /// ```
207    /// use web_net::{App, Route};
208    ///
209    /// let app = App::builder()
210    ///     .route(Route::at("/").get(|cx| cx.text(200, "home")))
211    ///     .build()?;
212    /// // HEAD follows GET.
213    /// let r = serve_net::testing::respond(&app, b"HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n");
214    /// assert_eq!(r.status, 200);
215    /// # Ok::<(), web_net::BuildError>(())
216    /// ```
217    #[must_use]
218    pub fn get<F>(self, f: F) -> Self
219    where
220        F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
221    {
222        self.on(Method::Get, f)
223    }
224
225    /// Adds a `POST` handler.
226    ///
227    /// ```
228    /// use web_net::{App, Route};
229    ///
230    /// let app = App::builder()
231    ///     .route(Route::at("/items/:id").post(|cx| cx.text(200, "created")).csrf_exempt("example: no browser form"))
232    ///     .build()?;
233    /// let r = serve_net::testing::respond(
234    ///     &app,
235    ///     b"POST /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
236    /// );
237    /// assert_eq!(r.text(), "created");
238    /// # Ok::<(), web_net::BuildError>(())
239    /// ```
240    #[must_use]
241    pub fn post<F>(self, f: F) -> Self
242    where
243        F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
244    {
245        self.on(Method::Post, f)
246    }
247
248    /// Adds a `PUT` handler.
249    ///
250    /// ```
251    /// use web_net::{App, Route};
252    ///
253    /// let app = App::builder()
254    ///     .route(Route::at("/items/:id").put(|cx| cx.text(200, "replaced")).csrf_exempt("example: no browser form"))
255    ///     .build()?;
256    /// let r = serve_net::testing::respond(
257    ///     &app,
258    ///     b"PUT /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
259    /// );
260    /// assert_eq!(r.text(), "replaced");
261    /// # Ok::<(), web_net::BuildError>(())
262    /// ```
263    #[must_use]
264    pub fn put<F>(self, f: F) -> Self
265    where
266        F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
267    {
268        self.on(Method::Put, f)
269    }
270
271    /// Adds a `PATCH` handler.
272    ///
273    /// ```
274    /// use web_net::{App, Route};
275    ///
276    /// let app = App::builder()
277    ///     .route(Route::at("/items/:id").patch(|cx| cx.text(200, "changed")).csrf_exempt("example: no browser form"))
278    ///     .build()?;
279    /// let r = serve_net::testing::respond(
280    ///     &app,
281    ///     b"PATCH /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
282    /// );
283    /// assert_eq!(r.text(), "changed");
284    /// # Ok::<(), web_net::BuildError>(())
285    /// ```
286    #[must_use]
287    pub fn patch<F>(self, f: F) -> Self
288    where
289        F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
290    {
291        self.on(Method::Patch, f)
292    }
293
294    /// Adds a `DELETE` handler.
295    ///
296    /// ```
297    /// use web_net::{App, Route};
298    ///
299    /// let app = App::builder()
300    ///     .route(Route::at("/items/:id").delete(|cx| cx.text(200, "deleted")).csrf_exempt("example: no browser form"))
301    ///     .build()?;
302    /// let r = serve_net::testing::respond(
303    ///     &app,
304    ///     b"DELETE /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
305    /// );
306    /// assert_eq!(r.text(), "deleted");
307    /// # Ok::<(), web_net::BuildError>(())
308    /// ```
309    #[must_use]
310    pub fn delete<F>(self, f: F) -> Self
311    where
312        F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
313    {
314        self.on(Method::Delete, f)
315    }
316
317    /// Anyone may use the route, guests included. With a gate that has
318    /// rules, public is a declaration, never a default: a route with neither
319    /// `public` nor `allow` stops the build. With [`Open`](crate::Open),
320    /// whose rule type has no values, every route is public and needs
321    /// neither.
322    ///
323    /// ```
324    /// use web_net::{App, get};
325    /// # use web_net::{Denial, Gate, Guest, Request};
326    /// # enum Rule { Staff }
327    /// # struct NoStaff;
328    /// # impl Gate for NoStaff {
329    /// #     type Identity = Guest;
330    /// #     type Rule = Rule;
331    /// #     fn authenticate(&self, _: &Request<'_>) -> Result<Guest, Denial> { Ok(Guest) }
332    /// #     fn authorize(&self, _: &Guest, _: &Rule, _: &Request<'_>) -> Result<(), Denial> {
333    /// #         Err(Denial::forbidden("staff.only"))
334    /// #     }
335    /// # }
336    /// // `NoStaff` is a gate that denies `Rule::Staff` to everyone.
337    /// let app = App::with_gate(NoStaff)
338    ///     .route(get("/", |cx| cx.text(200, "home")).public())
339    ///     .build()?;
340    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
341    /// assert_eq!(r.status, 200);
342    /// # Ok::<(), web_net::BuildError>(())
343    /// ```
344    #[must_use]
345    pub fn public(mut self) -> Self {
346        self.access = Access::Public;
347        self
348    }
349
350    /// Those `rule` allows may use the route; the gate's `authorize` decides
351    /// on every request. Inside a group with a rule, both rules hold: the
352    /// group's first, then this one.
353    ///
354    /// ```
355    /// use web_net::{App, get};
356    /// # use web_net::{Denial, Gate, Guest, Request};
357    /// # enum Rule { Staff }
358    /// # struct NoStaff;
359    /// # impl Gate for NoStaff {
360    /// #     type Identity = Guest;
361    /// #     type Rule = Rule;
362    /// #     fn authenticate(&self, _: &Request<'_>) -> Result<Guest, Denial> { Ok(Guest) }
363    /// #     fn authorize(&self, _: &Guest, _: &Rule, _: &Request<'_>) -> Result<(), Denial> {
364    /// #         Err(Denial::forbidden("staff.only"))
365    /// #     }
366    /// # }
367    /// // `NoStaff` is a gate that denies `Rule::Staff` to everyone.
368    /// let app = App::with_gate(NoStaff)
369    ///     .route(get("/admin", |cx| cx.text(200, "admin")).allow(Rule::Staff))
370    ///     .build()?;
371    /// let r = serve_net::testing::respond(&app, b"GET /admin HTTP/1.1\r\nHost: localhost\r\n\r\n");
372    /// assert_eq!(r.status, 403);
373    /// # Ok::<(), web_net::BuildError>(())
374    /// ```
375    #[must_use]
376    pub fn allow(mut self, rule: R) -> Self {
377        self.access = Access::Rules(vec![Arc::new(rule)]);
378        self
379    }
380
381    /// A name for logs, events, and hooks.
382    ///
383    /// ```
384    /// use web_net::{App, Hook, Routes, get};
385    ///
386    /// let app = App::builder()
387    ///     .hook(Hook::Finish, Routes::named(&["home"]), |cx| {
388    ///         cx.set_header("x-page", "home");
389    ///         Ok(())
390    ///     })
391    ///     .route(get("/", |cx| cx.text(200, "home")).name("home"))
392    ///     .build()?;
393    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
394    /// assert_eq!(r.header("x-page"), Some(&b"home"[..]));
395    /// # Ok::<(), web_net::BuildError>(())
396    /// ```
397    #[must_use]
398    pub fn name(mut self, name: &'static str) -> Self {
399        self.name = Some(name);
400        self
401    }
402
403    /// How the handlers behave: `Quick` (the default), `Io`, or `Cpu`. A
404    /// dispatcher in front of web-net reads it through
405    /// [`App::classify`](crate::App::classify) to move slow work off the
406    /// connection's worker.
407    ///
408    /// ```
409    /// use serve_net::WorkClass;
410    /// use web_net::{App, get};
411    ///
412    /// // The report waits on a database: a dispatcher can run it off the
413    /// // connection's worker.
414    /// let app = App::builder()
415    ///     .route(get("/report", |cx| cx.text(200, "report")).work(WorkClass::Io))
416    ///     .build()?;
417    /// # let _ = app;
418    /// # Ok::<(), web_net::BuildError>(())
419    /// ```
420    #[must_use]
421    pub fn work(mut self, work: WorkClass) -> Self {
422        self.work = work;
423        self
424    }
425
426    /// Takes the route out of CSRF protection, with the reason (a webhook
427    /// signed by its sender, an API authenticated by a bearer token that a
428    /// browser never sends on its own). The reason is required and must not
429    /// be empty: an exemption is a decision, never a default.
430    ///
431    /// ```
432    /// use web_net::{App, post};
433    ///
434    /// let app = App::builder()
435    ///     .route(post("/hooks/build", |cx| cx.text(200, "queued")).csrf_exempt("signed by the CI server"))
436    ///     .build()?;
437    /// let r = serve_net::testing::respond(
438    ///     &app,
439    ///     b"POST /hooks/build HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
440    /// );
441    /// assert_eq!(r.text(), "queued");
442    /// # Ok::<(), web_net::BuildError>(())
443    /// ```
444    #[must_use]
445    pub fn csrf_exempt(mut self, reason: &'static str) -> Self {
446        self.csrf_exempt = Some(reason);
447        self
448    }
449
450    /// The budgets for reading this route's forms (field count, sizes, files;
451    /// default `form_net::Limits::DEFAULT`). Keep them to what the form
452    /// needs: they bound what one request can make the server hold.
453    ///
454    /// ```
455    /// use web_net::{App, form_net, post};
456    ///
457    /// let limits = form_net::Limits::DEFAULT.with_max_fields(4);
458    /// let app = App::builder()
459    ///     .route(
460    ///         post("/search", |cx| cx.text(200, "results"))
461    ///             .form_limits(limits)
462    ///             .csrf_exempt("example: changes nothing"),
463    ///     )
464    ///     .build()?;
465    /// # let _ = app;
466    /// # Ok::<(), web_net::BuildError>(())
467    /// ```
468    #[must_use]
469    pub fn form_limits(mut self, limits: form_net::Limits) -> Self {
470        self.form_limits = limits;
471        self
472    }
473
474    /// The route's CORS policy: preflights answered, replies to allowed
475    /// origins readable by their scripts.
476    ///
477    /// ```
478    /// use web_net::{App, Cors, get};
479    ///
480    /// let app = App::builder()
481    ///     .route(
482    ///         get("/api/items", |cx| cx.bytes(200, "application/json", b"[]"))
483    ///             .cors(Cors::new().origin("https://app.example.com")),
484    ///     )
485    ///     .build()?;
486    /// let r = serve_net::testing::respond(
487    ///     &app,
488    ///     b"GET /api/items HTTP/1.1\r\nHost: localhost\r\nOrigin: https://app.example.com\r\n\r\n",
489    /// );
490    /// assert_eq!(r.header("access-control-allow-origin"), Some(&b"https://app.example.com"[..]));
491    /// # Ok::<(), web_net::BuildError>(())
492    /// ```
493    #[must_use]
494    pub fn cors(mut self, cors: crate::Cors) -> Self {
495        self.cors = Some(Arc::new(cors));
496        self
497    }
498
499    /// How the route's pages may be cached.
500    ///
501    /// ```
502    /// use web_net::{App, CachePolicy, get};
503    ///
504    /// let app = App::builder()
505    ///     .route(get("/account", |cx| cx.text(200, "yours")).cache(CachePolicy::NoStore))
506    ///     .build()?;
507    /// let r = serve_net::testing::respond(&app, b"GET /account HTTP/1.1\r\nHost: localhost\r\n\r\n");
508    /// assert_eq!(r.header("cache-control"), Some(&b"no-store"[..]));
509    /// # Ok::<(), web_net::BuildError>(())
510    /// ```
511    #[must_use]
512    pub fn cache(mut self, policy: crate::CachePolicy) -> Self {
513        self.cache = policy;
514        self
515    }
516
517    /// The template [`Context::render`](crate::Context::render) uses. Resolved when the
518    /// application is built: an unknown name stops the build.
519    ///
520    /// ```
521    /// use web_net::{App, Context, Guest, get};
522    /// # use web_net::{Output, Render, RenderError, TemplateId};
523    /// # struct Pages;
524    /// # impl Render for Pages {
525    /// #     type View<'a> = str;
526    /// #     fn template(&self, name: &str) -> Option<TemplateId> {
527    /// #         (name == "page").then_some(TemplateId(0))
528    /// #     }
529    /// #     fn render(&self, _: TemplateId, text: &str, out: &mut Output<'_>) -> Result<(), RenderError> {
530    /// #         out.write_str(text)
531    /// #     }
532    /// # }
533    /// // `Pages` is an engine whose one template, "page", writes the view (a `str`).
534    /// let app = App::builder()
535    ///     .render(Pages)
536    ///     .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("Home")).template("page"))
537    ///     .build()?;
538    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
539    /// assert_eq!(r.text(), "Home");
540    /// # Ok::<(), web_net::BuildError>(())
541    /// ```
542    #[must_use]
543    pub fn template(mut self, name: &'static str) -> Self {
544        self.template = Some(name);
545        self
546    }
547}
548
549impl<I, R, E: Render> std::fmt::Debug for Route<I, R, E> {
550    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551        f.debug_struct("Route")
552            .field("pattern", &self.pattern)
553            .field(
554                "methods",
555                &self.handlers.iter().map(|(m, _)| *m).collect::<Vec<_>>(),
556            )
557            .field("name", &self.name)
558            .finish_non_exhaustive()
559    }
560}
561
562macro_rules! method_fn {
563    ($(#[$doc:meta])* $fn:ident, $m:ident) => {
564        $(#[$doc])*
565        #[must_use]
566        pub fn $fn<I: 'static, R, E: Render, F>(pattern: impl Into<String>, f: F) -> Route<I, R, E>
567        where
568            F: Fn(&mut Context<'_, I, E>) -> Result + Send + Sync + 'static,
569        {
570            Route::at(pattern).on(Method::$m, f)
571        }
572    };
573}
574
575method_fn!(
576    /// A route with a `GET` handler (and so `HEAD`).
577    ///
578    /// ```
579    /// use web_net::{App, get};
580    ///
581    /// let app = App::builder().route(get("/", |cx| cx.text(200, "home"))).build()?;
582    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
583    /// assert_eq!(r.text(), "home");
584    /// # Ok::<(), web_net::BuildError>(())
585    /// ```
586    get, Get
587);
588method_fn!(
589    /// A route with a `POST` handler.
590    ///
591    /// ```
592    /// use web_net::{App, post};
593    ///
594    /// let app = App::builder()
595    ///     .route(post("/items/:id", |cx| cx.text(200, "created")).csrf_exempt("example: no browser form"))
596    ///     .build()?;
597    /// let r = serve_net::testing::respond(
598    ///     &app,
599    ///     b"POST /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
600    /// );
601    /// assert_eq!(r.text(), "created");
602    /// # Ok::<(), web_net::BuildError>(())
603    /// ```
604    post, Post
605);
606method_fn!(
607    /// A route with a `PUT` handler.
608    ///
609    /// ```
610    /// use web_net::{App, put};
611    ///
612    /// let app = App::builder()
613    ///     .route(put("/items/:id", |cx| cx.text(200, "replaced")).csrf_exempt("example: no browser form"))
614    ///     .build()?;
615    /// let r = serve_net::testing::respond(
616    ///     &app,
617    ///     b"PUT /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
618    /// );
619    /// assert_eq!(r.text(), "replaced");
620    /// # Ok::<(), web_net::BuildError>(())
621    /// ```
622    put, Put
623);
624method_fn!(
625    /// A route with a `PATCH` handler.
626    ///
627    /// ```
628    /// use web_net::{App, patch};
629    ///
630    /// let app = App::builder()
631    ///     .route(patch("/items/:id", |cx| cx.text(200, "changed")).csrf_exempt("example: no browser form"))
632    ///     .build()?;
633    /// let r = serve_net::testing::respond(
634    ///     &app,
635    ///     b"PATCH /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
636    /// );
637    /// assert_eq!(r.text(), "changed");
638    /// # Ok::<(), web_net::BuildError>(())
639    /// ```
640    patch, Patch
641);
642method_fn!(
643    /// A route with a `DELETE` handler.
644    ///
645    /// ```
646    /// use web_net::{App, delete};
647    ///
648    /// let app = App::builder()
649    ///     .route(delete("/items/:id", |cx| cx.text(200, "deleted")).csrf_exempt("example: no browser form"))
650    ///     .build()?;
651    /// let r = serve_net::testing::respond(
652    ///     &app,
653    ///     b"DELETE /items/1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
654    /// );
655    /// assert_eq!(r.text(), "deleted");
656    /// # Ok::<(), web_net::BuildError>(())
657    /// ```
658    delete, Delete
659);
660
661/// Routes sharing a path prefix and an access boundary.
662///
663/// A group's rule holds for every route in it: a route's own rule adds to
664/// it (both are checked, the group's first), and a route declared public
665/// inside a group with a rule stops the build. A route that declares
666/// nothing takes the group's access.
667///
668/// ```
669/// use web_net::{App, Group, get};
670///
671/// let app = App::builder()
672///     .group(
673///         Group::at("/docs")
674///             .route(get("/", |cx| cx.text(200, "index")))
675///             .route(get("/:page", |cx| cx.text(200, "page"))),
676///     )
677///     .build()?;
678/// # Ok::<(), web_net::BuildError>(())
679/// ```
680pub struct Group<I, R, E: Render = NoRender> {
681    pub(crate) prefix: String,
682    pub(crate) access: Access<R>,
683    pub(crate) routes: Vec<Route<I, R, E>>,
684    pub(crate) cors: Option<Arc<crate::Cors>>,
685    pub(crate) cache: crate::CachePolicy,
686}
687
688impl<I: 'static, R, E: Render> Group<I, R, E> {
689    /// A group under `prefix` (`/admin`).
690    ///
691    /// ```
692    /// use web_net::{App, Group, get};
693    ///
694    /// let app = App::builder()
695    ///     .group(Group::at("/admin").route(get("/", |cx| cx.text(200, "dashboard"))))
696    ///     .build()?;
697    /// let r = serve_net::testing::respond(&app, b"GET /admin HTTP/1.1\r\nHost: localhost\r\n\r\n");
698    /// assert_eq!(r.text(), "dashboard");
699    /// # Ok::<(), web_net::BuildError>(())
700    /// ```
701    #[must_use]
702    pub fn at(prefix: impl Into<String>) -> Self {
703        Self {
704            prefix: prefix.into(),
705            access: Access::Undeclared,
706            routes: Vec::new(),
707            cors: None,
708            cache: crate::CachePolicy::Unset,
709        }
710    }
711
712    /// The CORS policy of routes that declare none.
713    ///
714    /// ```
715    /// use web_net::{App, Cors, Group, get};
716    ///
717    /// let api = Group::at("/api")
718    ///     .cors(Cors::new().origin("https://app.example.com"))
719    ///     .route(get("/items", |cx| cx.bytes(200, "application/json", b"[]")));
720    /// let app = App::builder().group(api).build()?;
721    /// let r = serve_net::testing::respond(
722    ///     &app,
723    ///     b"GET /api/items HTTP/1.1\r\nHost: localhost\r\nOrigin: https://app.example.com\r\n\r\n",
724    /// );
725    /// assert_eq!(r.header("access-control-allow-origin"), Some(&b"https://app.example.com"[..]));
726    /// # Ok::<(), web_net::BuildError>(())
727    /// ```
728    #[must_use]
729    pub fn cors(mut self, cors: crate::Cors) -> Self {
730        self.cors = Some(Arc::new(cors));
731        self
732    }
733
734    /// The cache policy of routes that declare none.
735    ///
736    /// ```
737    /// use web_net::{App, CachePolicy, Group, get};
738    ///
739    /// let docs = Group::at("/docs")
740    ///     .cache(CachePolicy::Guests { max_age: 60 })
741    ///     .route(get("/intro", |cx| cx.text(200, "intro")));
742    /// let app = App::builder().group(docs).build()?;
743    /// let r = serve_net::testing::respond(&app, b"GET /docs/intro HTTP/1.1\r\nHost: localhost\r\n\r\n");
744    /// assert_eq!(r.header("cache-control"), Some(&b"public, max-age=60"[..]));
745    /// # Ok::<(), web_net::BuildError>(())
746    /// ```
747    #[must_use]
748    pub fn cache(mut self, policy: crate::CachePolicy) -> Self {
749        self.cache = policy;
750        self
751    }
752
753    /// Routes that declare nothing are public.
754    ///
755    /// ```
756    /// use web_net::{App, Group, get};
757    /// # use web_net::{Denial, Gate, Guest, Request};
758    /// # enum Rule { Staff }
759    /// # struct NoStaff;
760    /// # impl Gate for NoStaff {
761    /// #     type Identity = Guest;
762    /// #     type Rule = Rule;
763    /// #     fn authenticate(&self, _: &Request<'_>) -> Result<Guest, Denial> { Ok(Guest) }
764    /// #     fn authorize(&self, _: &Guest, _: &Rule, _: &Request<'_>) -> Result<(), Denial> {
765    /// #         Err(Denial::forbidden("staff.only"))
766    /// #     }
767    /// # }
768    /// // `NoStaff` is a gate that denies `Rule::Staff` to everyone.
769    /// let app = App::with_gate(NoStaff)
770    ///     .group(Group::at("/docs").public().route(get("/intro", |cx| cx.text(200, "intro"))))
771    ///     .build()?;
772    /// let r = serve_net::testing::respond(&app, b"GET /docs/intro HTTP/1.1\r\nHost: localhost\r\n\r\n");
773    /// assert_eq!(r.status, 200);
774    /// # Ok::<(), web_net::BuildError>(())
775    /// ```
776    #[must_use]
777    pub fn public(mut self) -> Self {
778        self.access = Access::Public;
779        self
780    }
781
782    /// Every route in the group needs `rule`, as well as its own.
783    ///
784    /// ```
785    /// use web_net::{App, Group, get};
786    /// # use web_net::{Denial, Gate, Guest, Request};
787    /// # enum Rule { Staff }
788    /// # struct NoStaff;
789    /// # impl Gate for NoStaff {
790    /// #     type Identity = Guest;
791    /// #     type Rule = Rule;
792    /// #     fn authenticate(&self, _: &Request<'_>) -> Result<Guest, Denial> { Ok(Guest) }
793    /// #     fn authorize(&self, _: &Guest, _: &Rule, _: &Request<'_>) -> Result<(), Denial> {
794    /// #         Err(Denial::forbidden("staff.only"))
795    /// #     }
796    /// # }
797    /// // `NoStaff` is a gate that denies `Rule::Staff` to everyone.
798    /// let app = App::with_gate(NoStaff)
799    ///     .group(Group::at("/admin").allow(Rule::Staff).route(get("/users", |cx| cx.text(200, "users"))))
800    ///     .build()?;
801    /// let r = serve_net::testing::respond(&app, b"GET /admin/users HTTP/1.1\r\nHost: localhost\r\n\r\n");
802    /// assert_eq!(r.status, 403);
803    /// # Ok::<(), web_net::BuildError>(())
804    /// ```
805    #[must_use]
806    pub fn allow(mut self, rule: R) -> Self {
807        self.access = Access::Rules(vec![Arc::new(rule)]);
808        self
809    }
810
811    /// Adds a route; its pattern is joined to the prefix.
812    ///
813    /// ```
814    /// use web_net::{App, Group, get};
815    ///
816    /// let blog = Group::at("/blog").route(get("/:slug", |cx| {
817    ///     let slug: String = cx.param_as("slug")?;
818    ///     cx.text(200, &slug)
819    /// }));
820    /// let app = App::builder().group(blog).build()?;
821    /// let r = serve_net::testing::respond(&app, b"GET /blog/hello HTTP/1.1\r\nHost: localhost\r\n\r\n");
822    /// assert_eq!(r.text(), "hello");
823    /// # Ok::<(), web_net::BuildError>(())
824    /// ```
825    #[must_use]
826    pub fn route(mut self, route: Route<I, R, E>) -> Self {
827        self.routes.push(route);
828        self
829    }
830
831    /// Adds a nested group; its prefix is joined to this one, and its
832    /// access holds inside this group's (both rules apply).
833    ///
834    /// ```
835    /// use web_net::{App, Group, get};
836    ///
837    /// let v1 = Group::at("/v1").route(get("/items", |cx| cx.text(200, "items")));
838    /// let app = App::builder().group(Group::at("/api").group(v1)).build()?;
839    /// let r = serve_net::testing::respond(&app, b"GET /api/v1/items HTTP/1.1\r\nHost: localhost\r\n\r\n");
840    /// assert_eq!(r.text(), "items");
841    /// # Ok::<(), web_net::BuildError>(())
842    /// ```
843    #[must_use]
844    pub fn group(mut self, group: Group<I, R, E>) -> Self {
845        let inherited = group.access.within(&self.access);
846        let cors = group.cors.or_else(|| self.cors.clone());
847        let cache = if group.cache == crate::CachePolicy::Unset {
848            self.cache
849        } else {
850            group.cache
851        };
852        for mut r in group.routes {
853            r.pattern = join(&group.prefix, &r.pattern);
854            r.access = std::mem::replace(&mut r.access, Access::Undeclared).within(&inherited);
855            if r.cors.is_none() {
856                r.cors.clone_from(&cors);
857            }
858            if r.cache == crate::CachePolicy::Unset {
859                r.cache = cache;
860            }
861            self.routes.push(r);
862        }
863        self
864    }
865
866    pub(crate) fn into_routes(self) -> impl Iterator<Item = Route<I, R, E>> {
867        let prefix = self.prefix;
868        let access = self.access;
869        let cors = self.cors;
870        let cache = self.cache;
871        self.routes.into_iter().map(move |mut r| {
872            r.pattern = join(&prefix, &r.pattern);
873            r.access = std::mem::replace(&mut r.access, Access::Undeclared).within(&access);
874            if r.cors.is_none() {
875                r.cors.clone_from(&cors);
876            }
877            if r.cache == crate::CachePolicy::Unset {
878                r.cache = cache;
879            }
880            r
881        })
882    }
883}
884
885impl<I, R, E: Render> std::fmt::Debug for Group<I, R, E> {
886    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
887        f.debug_struct("Group")
888            .field("prefix", &self.prefix)
889            .field("routes", &self.routes)
890            .finish_non_exhaustive()
891    }
892}
893
894/// `/admin` + `/` = `/admin`; `/admin` + `/users` = `/admin/users`.
895pub(crate) fn join(prefix: &str, pattern: &str) -> String {
896    let p = prefix.trim_end_matches('/');
897    if pattern == "/" || pattern.is_empty() {
898        if p.is_empty() { "/".into() } else { p.into() }
899    } else if pattern.starts_with('/') {
900        format!("{p}{pattern}")
901    } else {
902        format!("{p}/{pattern}")
903    }
904}
905
906#[cfg(test)]
907mod tests {
908    use super::join;
909
910    #[test]
911    fn test_join() {
912        assert_eq!(join("/admin", "/"), "/admin");
913        assert_eq!(join("/admin/", "/users"), "/admin/users");
914        assert_eq!(join("/", "/x"), "/x");
915        assert_eq!(join("", "/"), "/");
916        assert_eq!(join("/a", "b"), "/a/b");
917    }
918}