Skip to main content

anvil_core/
embedded.rs

1//! Compile-time-embedded static assets — the runtime hook for the
2//! `embed-assets` feature.
3//!
4//! Disk-served `public/` mounts continue to work via `tower_http::ServeDir`.
5//! When an app wants to ship as a single executable, it derives a
6//! `rust_embed::RustEmbed` struct on its `public/` folder and registers a
7//! fetcher here; `server::mount_static` then consults this registry before
8//! falling back to disk.
9//!
10//! Registration is global because the user's `RustEmbed` type lives in their
11//! crate, not in the framework, and we need a way to bridge across that
12//! boundary without leaking generics through every config struct.
13
14use std::borrow::Cow;
15use std::collections::HashMap;
16
17use once_cell::sync::OnceCell;
18use parking_lot::RwLock;
19
20/// A single embedded file's payload + metadata. Mirrors what `rust_embed::File`
21/// exposes, but kept framework-owned so the runtime API stays stable if we
22/// later swap the embedder.
23pub struct EmbeddedAsset {
24    pub data: Cow<'static, [u8]>,
25    /// MIME type. Caller is expected to pre-resolve this (e.g. via
26    /// `mime_guess::from_path`) so the framework doesn't have to guess.
27    pub content_type: String,
28    /// Optional strong validator. When present, the framework emits an `ETag`
29    /// header and short-circuits matching `If-None-Match` requests with 304.
30    pub etag: Option<String>,
31    /// Optional Unix-seconds last-modified timestamp from the embedder.
32    pub last_modified: Option<u64>,
33}
34
35/// Function pointer signature that backs an embedded mount. Takes a path
36/// relative to the mount root (`foo/bar.css`, no leading slash) and returns
37/// the file if it exists in the embedded set.
38pub type EmbeddedAssetFetcher = fn(&str) -> Option<EmbeddedAsset>;
39
40static REGISTRY: OnceCell<RwLock<HashMap<String, EmbeddedAssetFetcher>>> = OnceCell::new();
41
42fn registry() -> &'static RwLock<HashMap<String, EmbeddedAssetFetcher>> {
43    REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
44}
45
46/// Register an embedded-asset fetcher for the given URL prefix (e.g. `"/assets"`).
47/// Call this from app bootstrap before `serve()` runs. Re-registering the same
48/// prefix replaces the previous fetcher — last writer wins.
49pub fn register(prefix: impl Into<String>, fetcher: EmbeddedAssetFetcher) {
50    let mut map = registry().write();
51    map.insert(prefix.into(), fetcher);
52}
53
54/// Look up the fetcher for a mount prefix. Returns `None` when the mount has
55/// no embedded backing and the caller should fall through to disk serving.
56pub fn lookup(prefix: &str) -> Option<EmbeddedAssetFetcher> {
57    registry().read().get(prefix).copied()
58}
59
60/// Resolve a file path's MIME type via `mime_guess`, defaulting to
61/// `application/octet-stream` when the extension is unknown. Provided as a
62/// convenience for fetcher implementations.
63pub fn guess_mime(path: &str) -> String {
64    mime_guess::from_path(path)
65        .first_or_octet_stream()
66        .essence_str()
67        .to_string()
68}
69
70/// Pull an asset out of a `rust_embed::RustEmbed` impl and wrap it as an
71/// `EmbeddedAsset`. The user crate's generated wrapper just delegates here so
72/// they don't have to learn the `RustEmbed::get` shape themselves.
73#[cfg(feature = "embed-assets")]
74pub fn fetcher_from<E: rust_embed::RustEmbed + ?Sized>(path: &str) -> Option<EmbeddedAsset> {
75    let file = E::get(path)?;
76    let etag = file
77        .metadata
78        .sha256_hash()
79        .iter()
80        .map(|b| format!("{b:02x}"))
81        .collect::<String>();
82    Some(EmbeddedAsset {
83        content_type: guess_mime(path),
84        etag: Some(etag),
85        last_modified: file.metadata.last_modified(),
86        data: file.data,
87    })
88}
89
90/// One-liner for app authors: derive a `RustEmbed` struct on a folder and
91/// register it as the backing store for a URL mount. Expands to a struct +
92/// fetcher + a `register()` fn the bootstrap calls.
93///
94/// ```ignore
95/// // src/embedded_assets.rs
96/// anvil_core::embed_static!(PublicAssets, "/assets", "public");
97///
98/// // src/main.rs (inside bootstrap):
99/// embedded_assets::register();
100/// ```
101#[cfg(feature = "embed-assets")]
102#[macro_export]
103macro_rules! embed_static {
104    ($struct_name:ident, $prefix:expr, $folder:expr) => {
105        #[derive($crate::rust_embed::RustEmbed)]
106        #[folder = $folder]
107        pub struct $struct_name;
108
109        pub fn fetcher(path: &str) -> ::core::option::Option<$crate::embedded::EmbeddedAsset> {
110            $crate::embedded::fetcher_from::<$struct_name>(path)
111        }
112
113        pub fn register() {
114            $crate::embedded::register($prefix, fetcher);
115        }
116    };
117}