tower-serve-embedded 0.1.0

Embed content-hashed, cache-busted static web assets into your binary and serve them as a tower::Service, with a compile-time asset! macro for SSR templates.
Documentation
//! Embed content-hashed static web assets into your binary and serve them with `tower`.
//!
//! `tower-serve-embedded` is similar to [`rust-embed`](https://docs.rs/rust-embed) but tailored
//! for *serving* web assets from a `tower`/`axum` stack: every file is embedded and exposed at a
//! content-hashed URL that mirrors its location in your crate
//! (`assets/css/style.css` → `/assets/css/style.9f3a1c2b.css`), so it can be served `immutable`
//! with a one-year cache and still update instantly when its content changes.
//!
//! # How it works
//!
//! The heavy lifting (walking the directory, hashing, MIME detection, codegen) happens at build
//! time in [`tower-serve-embedded-build`](https://docs.rs/tower-serve-embedded-build), called
//! from your `build.rs`. There are **no proc macros** — the generated code is plain data plus a
//! tiny `macro_rules!`, so IDE support stays excellent.
//!
//! ```ignore
//! // build.rs
//! fn main() {
//!     tower_serve_embedded_build::Builder::new("assets").emit().unwrap();
//! }
//! ```
//!
//! ```ignore
//! // src/main.rs
//! tower_serve_embedded::embed!(); // pulls in `ASSETS` and the `asset!` macro
//!
//! // Reference assets by their path relative to the crate root. Resolved at compile time —
//! // typos are compile errors:
//! //   link rel="stylesheet" href=(asset!("assets/css/style.css"))
//! //   => "/assets/css/style.9f3a1c2b.css"
//!
//! // Serve them: the embedded path is already the full URL, so mount the service as a fallback.
//! //   Router::new().fallback_service(ASSETS.service())
//! ```
//!
//! See `examples/` in the repository for complete, runnable setups (`axum`, `actix`, `warp`).

mod service;

pub use service::ServeEmbedded;

/// A single embedded asset: its content plus the metadata needed to serve it.
///
/// Values of this type are generated at build time and stored in a `static` slice; you do not
/// construct them by hand.
#[derive(Debug, Clone, Copy)]
pub struct EmbeddedFile {
    /// The full, content-hashed URL the file is served at, mirroring its path under the crate
    /// root, e.g. `/assets/css/style.9f3a1c2b.css`. This is what [`ServeEmbedded`] matches the
    /// request path against, and what the generated `asset!` macro expands to.
    pub path: &'static str,
    /// The original path of the file relative to the **crate root**, e.g. `assets/css/style.css`
    /// — the key you pass to `asset!` and [`Assets::get_logical`].
    pub logical_path: &'static str,
    /// The raw file contents (via `include_bytes!`).
    pub bytes: &'static [u8],
    /// The guessed MIME type, e.g. `text/css`.
    pub content_type: &'static str,
    /// A strong, quoted `ETag` derived from the content hash, e.g. `"\"9f3a1c2b…\""`.
    pub etag: &'static str,
    /// The hex content hash used for cache busting (the same value embedded in [`path`](Self::path)).
    pub hash: &'static str,
}

/// A collection of [`EmbeddedFile`]s.
///
/// You normally get one via the generated `ASSETS` static (see [`embed!`]), not by calling
/// [`Assets::new`] yourself.
#[derive(Debug, Clone, Copy)]
pub struct Assets {
    /// Sorted by [`EmbeddedFile::path`] so [`get`](Self::get) can binary-search.
    files: &'static [EmbeddedFile],
}

impl Assets {
    /// Construct an asset set. `files` **must** be sorted by [`EmbeddedFile::path`]; the build
    /// script guarantees this for the generated `ASSETS` static.
    pub const fn new(files: &'static [EmbeddedFile]) -> Self {
        Self { files }
    }

    /// Look up a file by its served [`path`](EmbeddedFile::path), e.g.
    /// `/assets/css/style.9f3a1c2b.css`. This is the lookup [`ServeEmbedded`] uses.
    pub fn get(&self, path: &str) -> Option<&'static EmbeddedFile> {
        self.files
            .binary_search_by_key(&path, |f| f.path)
            .ok()
            .map(|i| &self.files[i])
    }

    /// Look up a file by its crate-root-relative [`logical_path`](EmbeddedFile::logical_path),
    /// e.g. `assets/css/style.css`.
    pub fn get_logical(&self, logical: &str) -> Option<&'static EmbeddedFile> {
        self.files.iter().find(|f| f.logical_path == logical)
    }

    /// The served URL for an asset given its crate-root-relative path, e.g.
    /// `url("assets/css/style.css")` → `Some("/assets/css/style.9f3a1c2b.css")`.
    ///
    /// This is the runtime equivalent of the `asset!` macro, for cases where the asset name isn't
    /// known at compile time. Prefer `asset!` when you can — it's checked at compile time.
    pub fn url(&self, logical: &str) -> Option<&'static str> {
        self.get_logical(logical).map(|f| f.path)
    }

    /// Iterate over every embedded file.
    pub fn iter(&self) -> impl Iterator<Item = &'static EmbeddedFile> {
        self.files.iter()
    }

    /// How many files are embedded.
    pub fn len(&self) -> usize {
        self.files.len()
    }

    /// Whether there are no embedded files.
    pub fn is_empty(&self) -> bool {
        self.files.is_empty()
    }

    /// A [`tower::Service`](tower_service::Service) that serves these assets.
    ///
    /// Each embedded `path` is already the full URL, so mount the service as a fallback (no
    /// nesting or prefix stripping):
    ///
    /// ```ignore
    /// Router::new().fallback_service(ASSETS.service())
    /// ```
    pub fn service(&'static self) -> ServeEmbedded {
        ServeEmbedded::new(self)
    }
}

/// Pull in the assets generated by `tower-serve-embedded-build`.
///
/// Call this once at module scope (typically in `main.rs`). It expands to an `include!` of the
/// build script's generated file, bringing into scope:
///
/// - `pub static ASSETS: tower_serve_embedded::Assets` — the embedded asset set, and
/// - `macro_rules! asset` — a compile-time macro mapping a crate-root-relative path to its URL.
///
/// ```ignore
/// tower_serve_embedded::embed!();
///
/// fn css() -> &'static str { asset!("assets/css/style.css") } // "/assets/css/style.9f3a1c2b.css"
/// ```
#[macro_export]
macro_rules! embed {
    () => {
        include!(concat!(env!("OUT_DIR"), "/embed_assets.rs"));
    };
}