Skip to main content

tower_serve_embedded/
lib.rs

1//! Embed content-hashed static web assets into your binary and serve them with `tower`.
2//!
3//! `tower-serve-embedded` is similar to [`rust-embed`](https://docs.rs/rust-embed) but tailored
4//! for *serving* web assets from a `tower`/`axum` stack: ordinary files are embedded and exposed at
5//! a content-hashed URL that mirrors their location in your crate
6//! (`assets/css/style.css` → `/assets/css/style.9f3a1c2b.css`), so they can be served `immutable`
7//! with a one-year cache and still update instantly when their content changes.
8//!
9//! # How it works
10//!
11//! The heavy lifting (walking the directory, hashing, MIME detection, codegen) happens at build
12//! time in [`tower-serve-embedded-build`](https://docs.rs/tower-serve-embedded-build), called
13//! from your `build.rs`. There are **no proc macros** — the generated code is plain data plus a
14//! tiny `macro_rules!`, so IDE support stays excellent.
15//!
16//! ```ignore
17//! // build.rs
18//! fn main() {
19//!     tower_serve_embedded_build::Builder::new("assets").emit().unwrap();
20//! }
21//! ```
22//!
23//! ```ignore
24//! // src/main.rs
25//! tower_serve_embedded::embed!(); // pulls in `ASSETS` and the `asset!` macro
26//!
27//! // Reference assets by their path relative to the crate root. Resolved at compile time —
28//! // typos are compile errors:
29//! //   link rel="stylesheet" href=(asset!("assets/css/style.css"))
30//! //   => "/assets/css/style.9f3a1c2b.css"
31//!
32//! // Serve them: generated asset URLs are already full paths, so mount as a fallback.
33//! //   Router::new().fallback_service(ASSETS.service())
34//! ```
35//!
36//! See `examples/` in the repository for complete, runnable setups (`axum`, `actix`, `warp`).
37
38mod service;
39
40pub use service::ServeEmbedded;
41
42/// The `Cache-Control` value sent for content-hashed URLs and for assets in an `immutable_dir`:
43/// a one-year, `public`, `immutable` cache. The bytes behind such a URL never change.
44pub const IMMUTABLE_CACHE_CONTROL: &str = "public, max-age=31536000, immutable";
45
46/// A single embedded asset: its content plus the metadata needed to serve it.
47///
48/// Values of this type are generated at build time and stored in a `static` slice; you do not
49/// construct them by hand.
50///
51/// Every file records its stable, non-hashed [`url`](Self::url). Files that are content-hashed
52/// (the default) are served at their cache-busted [`hashed_url`](Self::hashed_url); files in an
53/// `immutable_dir` are not re-hashed, have `hashed_url == None`, and are served at their plain
54/// [`url`](Self::url).
55#[derive(Debug, Clone, Copy)]
56pub struct EmbeddedFile {
57    /// The stable, non-hashed URL mirroring the file's path under the crate root with a leading
58    /// slash, e.g. `/assets/css/style.css`. This URL is served only for assets in an
59    /// `immutable_dir`; ordinary assets are served only at their [`hashed_url`](Self::hashed_url).
60    pub url: &'static str,
61    /// The full, content-hashed, cache-busted URL, e.g. `/assets/css/style.9f3a1c2b.css`, served
62    /// `immutable`. `None` for assets in an `immutable_dir` (already immutable, so not re-hashed).
63    /// When present, this is what the generated `asset!` macro and [`Assets::url`] prefer.
64    pub hashed_url: Option<&'static str>,
65    /// The original path of the file relative to the **crate root**, e.g. `assets/css/style.css`
66    /// — the key you pass to `asset!` and [`Assets::get_logical`].
67    pub logical_path: &'static str,
68    /// The raw file contents (via `include_bytes!`).
69    pub bytes: &'static [u8],
70    /// The guessed MIME type, e.g. `text/css`.
71    pub content_type: &'static str,
72    /// A strong, quoted `ETag` derived from the content hash, e.g. `"\"9f3a1c2b…\""`.
73    pub etag: &'static str,
74    /// The hex content hash used for cache busting and the `ETag`.
75    pub hash: &'static str,
76}
77
78/// A served URL mapped to the file that answers it and the `Cache-Control` to send.
79///
80/// One generated file produces one route: its [`hashed_url`](EmbeddedFile::hashed_url) for
81/// ordinary assets, or its plain [`url`](EmbeddedFile::url) for assets in an `immutable_dir`.
82/// Generated at build time and sorted by `url` for binary search; you do not construct these by
83/// hand outside of tests.
84#[derive(Debug, Clone, Copy)]
85pub struct Route {
86    /// The served URL this route matches.
87    pub url: &'static str,
88    /// Index into the [`Assets`] file slice of the file that answers this URL.
89    pub file: usize,
90    /// The `Cache-Control` value to send. Generated routes use
91    /// `Some(`[`IMMUTABLE_CACHE_CONTROL`]`)` because every served generated URL is immutable.
92    pub cache_control: Option<&'static str>,
93}
94
95/// The outcome of resolving a request path against an [`Assets`] set: which file to serve and the
96/// `Cache-Control` to send with it. Returned by [`Assets::resolve`].
97#[derive(Debug, Clone, Copy)]
98pub struct Resolved {
99    /// The matched file.
100    pub file: &'static EmbeddedFile,
101    /// The `Cache-Control` to set, or `None` to send none for custom route tables.
102    pub cache_control: Option<&'static str>,
103}
104
105/// A collection of [`EmbeddedFile`]s plus the [`Route`] table that maps served URLs to them.
106///
107/// You normally get one via the generated `ASSETS` static (see [`embed!`]), not by calling
108/// [`Assets::new`] yourself.
109#[derive(Debug, Clone, Copy)]
110pub struct Assets {
111    files: &'static [EmbeddedFile],
112    /// Sorted by [`Route::url`] so [`resolve`](Self::resolve) can binary-search.
113    routes: &'static [Route],
114}
115
116impl Assets {
117    /// Construct an asset set from its files and route table. `routes` **must** be sorted by
118    /// [`Route::url`], and each [`Route::file`] must index into `files`; the build script
119    /// guarantees both for the generated `ASSETS` static.
120    pub const fn new(files: &'static [EmbeddedFile], routes: &'static [Route]) -> Self {
121        Self { files, routes }
122    }
123
124    /// Resolve a served URL — a [`hashed_url`](EmbeddedFile::hashed_url) like
125    /// `/assets/css/style.9f3a1c2b.css`, or the plain [`url`](EmbeddedFile::url) of an
126    /// `immutable_dir` asset — to the file that answers it and the `Cache-Control` to send. This is
127    /// the lookup [`ServeEmbedded`] uses; hand-rolled (non-`tower`) integrations call it to build a
128    /// response themselves.
129    pub fn resolve(&self, url: &str) -> Option<Resolved> {
130        let files = self.files;
131        let i = self.routes.binary_search_by_key(&url, |r| r.url).ok()?;
132        let route = self.routes[i];
133        Some(Resolved {
134            file: &files[route.file],
135            cache_control: route.cache_control,
136        })
137    }
138
139    /// Look up a file by its crate-root-relative [`logical_path`](EmbeddedFile::logical_path),
140    /// e.g. `assets/css/style.css`.
141    pub fn get_logical(&self, logical: &str) -> Option<&'static EmbeddedFile> {
142        self.files.iter().find(|f| f.logical_path == logical)
143    }
144
145    /// The URL to reference an asset by, given its crate-root-relative path, e.g.
146    /// `url("assets/css/style.css")` → `Some("/assets/css/style.9f3a1c2b.css")`. Returns the
147    /// cache-busted [`hashed_url`](EmbeddedFile::hashed_url) when there is one, otherwise the plain
148    /// [`url`](EmbeddedFile::url) (for `immutable_dir` assets).
149    ///
150    /// This is the runtime equivalent of the `asset!` macro, for cases where the asset name isn't
151    /// known at compile time. Prefer `asset!` when you can — it's checked at compile time.
152    pub fn url(&self, logical: &str) -> Option<&'static str> {
153        self.get_logical(logical)
154            .map(|f| f.hashed_url.unwrap_or(f.url))
155    }
156
157    /// Iterate over every embedded file.
158    pub fn iter(&self) -> impl Iterator<Item = &'static EmbeddedFile> {
159        self.files.iter()
160    }
161
162    /// How many files are embedded.
163    pub fn len(&self) -> usize {
164        self.files.len()
165    }
166
167    /// Whether there are no embedded files.
168    pub fn is_empty(&self) -> bool {
169        self.files.is_empty()
170    }
171
172    /// A [`tower::Service`](tower_service::Service) that serves these assets.
173    ///
174    /// Generated asset URLs are already full paths, so mount the service as a fallback (no nesting
175    /// or prefix stripping):
176    ///
177    /// ```ignore
178    /// Router::new().fallback_service(ASSETS.service())
179    /// ```
180    pub fn service(&'static self) -> ServeEmbedded {
181        ServeEmbedded::new(self)
182    }
183}
184
185/// Pull in the assets generated by `tower-serve-embedded-build`.
186///
187/// Call this once at module scope (typically at the crate root in `main.rs` or `lib.rs`). It
188/// expands to an `include!` of the build script's generated file, bringing into scope:
189///
190/// - `pub static ASSETS: tower_serve_embedded::Assets` — the embedded asset set, and
191/// - `asset!` — a crate-local compile-time macro mapping a crate-root-relative path to its URL.
192///
193/// The generated `asset!` macro is deliberately not emitted with `#[macro_export]`, because Rust
194/// treats `macro_export` macros generated by another macro as future-incompatible when they are
195/// referenced by absolute paths. If you invoke `embed!()` at the crate root, you can use
196/// `crate::asset!(...)` from other modules.
197///
198/// ```ignore
199/// tower_serve_embedded::embed!();
200///
201/// fn css() -> &'static str { crate::asset!("assets/css/style.css") } // "/assets/css/style.9f3a1c2b.css"
202/// ```
203#[macro_export]
204macro_rules! embed {
205    () => {
206        include!(concat!(env!("OUT_DIR"), "/embed_assets.rs"));
207    };
208}