Added assetserver and macro
This commit is contained in:
8
crates/assetserver/Cargo.toml
Normal file
8
crates/assetserver/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "assetserver"
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
16
crates/assetserver/src/lib.rs
Normal file
16
crates/assetserver/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// A static asset with compile-time embedded data.
|
||||||
|
pub trait Asset {
|
||||||
|
/// The common URL prefix for all assets (e.g., "/assets")
|
||||||
|
const URL_PREFIX: &'static str;
|
||||||
|
|
||||||
|
/// The specific URL path for this asset (e.g., "/logo.png")
|
||||||
|
const URL_POSTFIX: &'static str;
|
||||||
|
|
||||||
|
/// The full URL for this asset (e.g., "/assets/logo.png")
|
||||||
|
const URL: &'static str;
|
||||||
|
|
||||||
|
/// The embedded file contents as a byte slice
|
||||||
|
const BYTES: &'static [u8];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: image manipulation
|
||||||
15
crates/macro-assets/Cargo.toml
Normal file
15
crates/macro-assets/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "macro-assets"
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { workspace = true }
|
||||||
|
quote = { workspace = true }
|
||||||
239
crates/macro-assets/src/lib.rs
Normal file
239
crates/macro-assets/src/lib.rs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{
|
||||||
|
Ident, LitStr, Result, Token, braced,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
parse_macro_input,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A macro for generating static asset handlers with compile-time embedding.
|
||||||
|
///
|
||||||
|
/// This macro generates:
|
||||||
|
/// - Individual structs for each asset that implement the `assetserver::Asset` trait
|
||||||
|
/// - Compile-time embedding of asset files using `include_bytes!`
|
||||||
|
/// - Optionally, an Axum router function that serves all assets
|
||||||
|
///
|
||||||
|
/// # Syntax
|
||||||
|
///
|
||||||
|
/// ```notrust
|
||||||
|
/// assets! {
|
||||||
|
/// prefix: "/assets"
|
||||||
|
/// router: router_function_name()
|
||||||
|
///
|
||||||
|
/// AssetName {
|
||||||
|
/// source: "path/to/file.ext",
|
||||||
|
/// target: "/public-url.ext"
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// AnotherAsset {
|
||||||
|
/// source: "path/to/another.ext",
|
||||||
|
/// target: "/another-url.ext"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `prefix`: The URL prefix for all assets (e.g., "/assets")
|
||||||
|
/// - `router`: (Optional) The name of a function to generate that returns `(&'static str, Router<()>)`
|
||||||
|
/// with routes for all assets
|
||||||
|
/// - Asset blocks: Each block defines an asset with:
|
||||||
|
/// - A name (identifier) for the generated struct
|
||||||
|
/// - `source`: The file system path to the asset (relative to the current file)
|
||||||
|
/// - `target`: The URL path where the asset will be served
|
||||||
|
///
|
||||||
|
/// # Generated Code
|
||||||
|
///
|
||||||
|
/// For each asset, the macro generates:
|
||||||
|
/// - A struct with the specified name
|
||||||
|
/// - An `assetserver::Asset` trait implementation containing:
|
||||||
|
/// - `URL_PREFIX`: The common prefix for all assets
|
||||||
|
/// - `URL`: The specific URL path for this asset
|
||||||
|
/// - `BYTES`: The embedded file contents as a byte slice
|
||||||
|
/// - Documentation showing the original asset definition
|
||||||
|
///
|
||||||
|
/// If `router` is specified, also generates a function that returns an Axum router
|
||||||
|
/// with all assets mounted at their target URLs.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```notrust
|
||||||
|
/// assets! {
|
||||||
|
/// prefix: "/static"
|
||||||
|
/// router: static_router()
|
||||||
|
///
|
||||||
|
/// Logo {
|
||||||
|
/// source: "../images/logo.png",
|
||||||
|
/// target: "/logo.png"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This generates structs implementing `assetserver::Asset` and optionally a router function:
|
||||||
|
///
|
||||||
|
/// ```notrust
|
||||||
|
/// pub fn static_router() -> (&'static str, ::axum::Router<()>) {
|
||||||
|
/// let router = ::axum::Router::new()
|
||||||
|
/// .route(Logo::URL, ::axum::routing::get(|| async {
|
||||||
|
/// (::axum::http::StatusCode::OK, Logo::BYTES)
|
||||||
|
/// }));
|
||||||
|
/// ("/static", router)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn assets(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as AssetsInput);
|
||||||
|
let prefix = &input.prefix;
|
||||||
|
|
||||||
|
let asset_impls = input.assets.iter().map(|asset| {
|
||||||
|
let name = &asset.name;
|
||||||
|
let source = &asset.source;
|
||||||
|
let target = &asset.target;
|
||||||
|
|
||||||
|
// Generate documentation showing the original asset definition
|
||||||
|
let doc = format!(
|
||||||
|
"This is an `asset!`\n```notrust\n{} {{\n\tsource: \"{}\",\n\ttarget: \"{}\"\n}}\n```",
|
||||||
|
name, source, target
|
||||||
|
);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[expect(clippy::allow_attributes)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[doc = #doc]
|
||||||
|
pub struct #name {}
|
||||||
|
|
||||||
|
impl ::assetserver::Asset for #name {
|
||||||
|
const URL_PREFIX: &'static str = #prefix;
|
||||||
|
const URL_POSTFIX: &'static str = #target;
|
||||||
|
const URL: &'static str = concat!(#prefix, #target);
|
||||||
|
const BYTES: &'static [u8] = include_bytes!(#source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate the router function if specified
|
||||||
|
let router_fn = if let Some(router_name) = &input.router {
|
||||||
|
let route_definitions = input.assets.iter().map(|asset| {
|
||||||
|
let name = &asset.name;
|
||||||
|
quote! {
|
||||||
|
.route(#name::URL_POSTFIX, ::axum::routing::get(|| async {
|
||||||
|
(::axum::http::StatusCode::OK, #name::BYTES)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let router_doc = format!(
|
||||||
|
"Generated router function that serves {} asset(s) with prefix \"{}\"",
|
||||||
|
input.assets.len(),
|
||||||
|
prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[doc = #router_doc]
|
||||||
|
pub fn #router_name() -> (&'static str, ::axum::Router<()>) {
|
||||||
|
let router = ::axum::Router::new()
|
||||||
|
#(#route_definitions)*;
|
||||||
|
(#prefix, router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
#(#asset_impls)*
|
||||||
|
|
||||||
|
#router_fn
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the complete input to the `assets!` macro
|
||||||
|
struct AssetsInput {
|
||||||
|
prefix: String,
|
||||||
|
router: Option<Ident>,
|
||||||
|
assets: Vec<AssetDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a single asset definition within the macro
|
||||||
|
struct AssetDefinition {
|
||||||
|
name: Ident,
|
||||||
|
source: String,
|
||||||
|
target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for AssetsInput {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
// Parse "prefix:"
|
||||||
|
let _prefix_ident: Ident = input.parse()?;
|
||||||
|
let _colon: Token![:] = input.parse()?;
|
||||||
|
let prefix_lit: LitStr = input.parse()?;
|
||||||
|
let prefix = prefix_lit.value();
|
||||||
|
|
||||||
|
// Try to parse optional "router:" parameter
|
||||||
|
let router = if input.peek(Ident) {
|
||||||
|
let peek_ident: Ident = input.fork().parse()?;
|
||||||
|
if peek_ident == "router" {
|
||||||
|
let _router_ident: Ident = input.parse()?;
|
||||||
|
let _colon: Token![:] = input.parse()?;
|
||||||
|
let router_name: Ident = input.parse()?;
|
||||||
|
// Parse the parentheses after the function name
|
||||||
|
let _paren_content;
|
||||||
|
syn::parenthesized!(_paren_content in input);
|
||||||
|
Some(router_name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut assets = Vec::new();
|
||||||
|
|
||||||
|
// Parse asset definitions until we reach the end
|
||||||
|
while !input.is_empty() {
|
||||||
|
let asset = input.parse::<AssetDefinition>()?;
|
||||||
|
assets.push(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AssetsInput {
|
||||||
|
prefix,
|
||||||
|
router,
|
||||||
|
assets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for AssetDefinition {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
// Parse the asset name
|
||||||
|
let name: Ident = input.parse()?;
|
||||||
|
|
||||||
|
// Parse the braced content
|
||||||
|
let content;
|
||||||
|
braced!(content in input);
|
||||||
|
|
||||||
|
// Parse "source:"
|
||||||
|
let _source_ident: Ident = content.parse()?;
|
||||||
|
let _colon1: Token![:] = content.parse()?;
|
||||||
|
let source_lit: LitStr = content.parse()?;
|
||||||
|
let source = source_lit.value();
|
||||||
|
let _comma1: Token![,] = content.parse()?;
|
||||||
|
|
||||||
|
// Parse "target:"
|
||||||
|
let _target_ident: Ident = content.parse()?;
|
||||||
|
let _colon2: Token![:] = content.parse()?;
|
||||||
|
let target_lit: LitStr = content.parse()?;
|
||||||
|
let target = target_lit.value();
|
||||||
|
|
||||||
|
// Optional trailing comma
|
||||||
|
let _ = content.parse::<Token![,]>();
|
||||||
|
|
||||||
|
Ok(AssetDefinition {
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user