Skip to main content

web_net/
lib.rs

1//! # web_net
2//!
3//! The web layer on serve-net: what a site is written in.
4//!
5//! A site in five lines:
6//!
7//! ```no_run
8//! use web_net::{App, get};
9//!
10//! App::builder()
11//!     .route(get("/", |cx| cx.html(200, "<h1>Hello</h1>")))
12//!     .route(get("/posts/:slug", |cx| cx.text(200, &cx.param("slug")?)))
13//!     .build()?
14//!     .listen("127.0.0.1:8080")?
15//!     .wait();
16//! # Ok::<(), Box<dyn std::error::Error>>(())
17//! ```
18//!
19//! A handler gets a [`Context`] and ends with one reply: [`Context::html`],
20//! [`Context::text`], [`Context::bytes`], [`Context::redirect`], or [`Context::render`] for a
21//! template. `?` turns any error into the right error page.
22//!
23//! A request runs one fixed path, and each step either answers or hands on:
24//!
25//! 1. **site**: the `Host` picks the site (unknown hosts get 421, aliases a
26//!    redirect to the canonical host), and its base path is removed, so a
27//!    site works installed in a folder.
28//! 2. **url**: the path is decoded once, dot segments are removed, empty
29//!    segments collapse, and the trailing-slash policy applies. A path that
30//!    was not spelled canonically is redirected to the spelling that is, so
31//!    two spellings never reach different routes. An encoded slash, an
32//!    encoded backslash, a control character, or bytes that are not UTF-8
33//!    are refused with 400.
34//! 3. **authenticate**: the [`Gate`] says who is asking (or a front that
35//!    authenticated already passes the identity to [`App::respond`]).
36//! 4. **route**: a trie compiled at build time matches the path (404 when
37//!    nothing does).
38//! 5. **method**: `HEAD` follows `GET`, `OPTIONS` is answered for every
39//!    route, anything else the route does not take is a 405 with `Allow`.
40//! 6. **authorize**: the route's rule, checked by the gate. With a gate that
41//!    has rules, every route declares its access,
42//!    [`public`](Route::public) or [`allow`](Route::allow), and a route
43//!    without one stops the build. With [`Open`] (no sign-in) every route is
44//!    public.
45//! 7. **hooks and handler**: registered [`Hook`]s for this route, then the
46//!    handler, which receives a [`Context`]: proof the request passed its gate.
47//! 8. **render**: a [`Render`] engine writes the page from a typed [`View`],
48//!    no JSON in between.
49//! 9. **errors**: every refusal and error is answered by one function, with
50//!    the site's error template or a built-in page, and reported through
51//!    [`Events`].
52//!
53//! ```
54//! use web_net::{App, get};
55//!
56//! let app = App::builder()
57//!     .host("example.com")
58//!     .alias("www.example.com")
59//!     .base("/blog")
60//!     .route(get("/", |cx| cx.html(200, "<h1>Blog</h1>")))
61//!     .route(get("/posts/:slug", |cx| cx.text(200, &cx.param("slug")?)))
62//!     .build()?;
63//!
64//! use serve_net::testing::respond;
65//! let r = respond(&app, b"GET /blog/posts/hello HTTP/1.1\r\nHost: example.com\r\n\r\n");
66//! assert_eq!((r.status, r.text()), (200, "hello"));
67//!
68//! let r = respond(&app, b"GET /blog/./posts//hello/ HTTP/1.1\r\nHost: example.com\r\n\r\n");
69//! assert_eq!((r.status, r.header("location")), (301, Some(&b"/blog/posts/hello"[..])));
70//!
71//! let r = respond(&app, b"GET /blog HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
72//! assert_eq!(r.header("location"), Some(&b"http://example.com/blog/"[..]));
73//!
74//! let r = respond(&app, b"GET / HTTP/1.1\r\nHost: other.example\r\n\r\n");
75//! assert_eq!(r.status, 421);
76//! # Ok::<(), web_net::BuildError>(())
77//! ```
78//!
79//! [`App::listen`] serves one app on one address with serve-net's defaults.
80//! For TLS, several listeners, or other limits, an [`App`] is a
81//! `serve_net::HttpService`: `serve_net::Server::builder()...serve(app)`.
82//!
83//! The reference with every public item is `docs/API.md` in the
84//! repository.
85
86#![doc(html_root_url = "https://docs.rs/web-net")]
87#![cfg_attr(docsrs, feature(doc_cfg))]
88#![forbid(unsafe_code)]
89#![deny(missing_docs)]
90
91mod app;
92mod assets;
93mod cors;
94mod csrf;
95mod cx;
96mod error;
97mod events;
98mod gate;
99mod headers;
100mod policy;
101mod render;
102mod request;
103mod route;
104mod router;
105mod sites;
106mod url;
107
108pub use app::{App, Builder, Classified, DEFAULT_PAGE_LIMIT, Hook, RouteId, Routes};
109pub use assets::{Assets, INLINE_LIMIT, mime_for};
110pub use cors::Cors;
111pub use cx::Context;
112pub use error::{BuildError, Error, Problem, Result};
113pub use events::{Checkpoint, Event, EventKind, Events, NoEvents, Stage};
114pub use gate::{Denial, DenialKind, Gate, Guest, Identity, NoRule, Open};
115pub use headers::{DEFAULT_CSP, SecurityHeaders};
116pub use policy::{CachePolicy, Cacheable, Compression, Served};
117pub use render::{
118    ErrorPage, Fields, Key, List, NoRender, Output, Render, RenderError, SafeHtml, Shape, ShapeId,
119    TemplateId, Value, View, escape_html,
120};
121pub use request::{Ambiguous, Request, RequestId};
122pub use route::{Group, Handler, Route, delete, get, patch, post, put};
123pub use router::MAX_PARAMS;
124pub use sites::Sites;
125pub use url::{TrailingSlash, UrlError};
126
127/// cookie-net, at the version web-net uses, for [`Context::set_cookie`].
128pub use cookie_net;
129/// csrf-sec, at the version web-net uses, for [`Builder::csrf`].
130pub use csrf_sec;
131/// form-net, at the version web-net uses, for [`Context::form`] and its `form!`
132/// macro.
133pub use form_net;
134
135/// Crate version string, populated by Cargo at build time.
136///
137/// ```
138/// assert_eq!(web_net::VERSION.split('.').count(), 3);
139/// ```
140pub const VERSION: &str = env!("CARGO_PKG_VERSION");
141
142/// The README and `docs/API.md` are compiled and run as doctests, so the
143/// reference cannot drift from the API.
144#[cfg(all(doctest, feature = "uploads"))]
145mod doc_tests {
146    #[doc = include_str!("../README.md")]
147    struct Readme;
148    #[doc = include_str!("../docs/API.md")]
149    struct ApiReference;
150}
151
152#[cfg(test)]
153mod tests {
154    use super::VERSION;
155
156    #[test]
157    fn version_matches_manifest() {
158        assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
159        assert_eq!(VERSION.split('.').count(), 3, "expected MAJOR.MINOR.PATCH");
160    }
161}