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}