#![recursion_limit = "1024"]
#![forbid(unsafe_code)]
#[macro_use]
extern crate quote;
extern crate proc_macro;
mod attributes;
mod compress;
mod dynamic;
mod embed;
use attributes::read_attribute_config;
use dynamic::generate_dynamic_impl;
use embed::generate_embed_impl;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use std::{env, path::Path};
use syn::{Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
ast.attrs
.iter()
.filter(|value| value.path().is_ident(attr_name))
.map(|attr| &attr.meta)
.filter_map(|meta| match meta {
Meta::NameValue(MetaNameValue {
value:
Expr::Lit(ExprLit {
lit: Lit::Str(val), ..
}),
..
}) => Some(val.value()),
_ => None,
})
.collect()
}
fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
match ast.data {
Data::Struct(ref data) => match data.fields {
Fields::Unit => {}
_ => {
return Err(syn::Error::new_spanned(
ast,
"RustEmbed can only be derived for unit structs",
))
}
},
_ => {
return Err(syn::Error::new_spanned(
ast,
"RustEmbed can only be derived for unit structs",
))
}
};
let mut folder_paths = find_attribute_values(ast, "folder");
if folder_paths.len() != 1 {
return Err(syn::Error::new_spanned(
ast,
"#[derive(RustEmbed)] must contain one and only one folder attribute",
));
}
let folder_path = folder_paths.remove(0);
#[cfg(feature = "interpolate-folder-path")]
let folder_path = shellexpand::full(&folder_path)
.map_err(|e| {
syn::Error::new_spanned(ast, format!("Could not interpolate folder path: {e}"))
})?
.to_string();
let folder_path = if Path::new(&folder_path).is_relative() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|e| {
syn::Error::new_spanned(
ast,
format!("Could not read the CARGO_MANIFEST_DIR environment variable: {e}"),
)
})?;
Path::new(&manifest_dir)
.join(folder_path)
.to_str()
.ok_or_else(|| {
syn::Error::new_spanned(
ast,
"The folder path does not have a valid string representation",
)
})?
.to_owned()
} else {
folder_path
};
let config = read_attribute_config(ast);
if !Path::new(&folder_path).exists() && !config.allow_missing() {
return Err(syn::Error::new_spanned(
ast,
format!(
"#[derive(RustEmbed)] folder '{folder_path}' does not exist. \
Set `#[allow_missing = true]` to allow a missing folder and \
generate an empty asset set instead."
),
));
}
let prefixes = find_attribute_values(ast, "prefix");
let prefix = if prefixes.is_empty() {
"".to_string()
} else if prefixes.len() == 1 {
prefixes[0].clone()
} else {
return Err(syn::Error::new_spanned(
ast,
"#[derive(RustEmbed)] must have at most one prefix, you supplied several",
));
};
if cfg!(debug_assertions) && !cfg!(feature = "always-embed") {
Ok(generate_dynamic_impl(
&ast.ident,
&config,
&folder_path,
&prefix,
))
} else {
Ok(generate_embed_impl(
&ast.ident,
&config,
&folder_path,
&prefix,
))
}
}
#[proc_macro_derive(
RustEmbed,
attributes(folder, prefix, include, exclude, gzip, br, zstd, allow_missing)
)]
pub fn derive_input_object(input: TokenStream) -> TokenStream {
let ast: DeriveInput = match syn::parse(input) {
Ok(ast) => ast,
Err(e) => return e.to_compile_error().into(),
};
match impl_rust_embed_for_web(&ast) {
Ok(gen) => gen.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[cfg(test)]
mod tests {
use super::impl_rust_embed_for_web;
fn err_message(input: &str) -> String {
let ast: syn::DeriveInput = syn::parse_str(input).unwrap();
impl_rust_embed_for_web(&ast)
.expect_err("expected the derive input to fail")
.to_string()
}
#[test]
fn rejects_enums() {
assert!(err_message("#[folder = \"src\"] enum Bad { A }")
.contains("can only be derived for unit structs"));
}
#[test]
fn rejects_structs_with_fields() {
assert!(err_message("#[folder = \"src\"] struct Bad { field: u32 }")
.contains("can only be derived for unit structs"));
}
#[test]
fn requires_a_folder_attribute() {
assert!(err_message("struct Bad;").contains("one and only one folder attribute"));
}
#[test]
fn rejects_multiple_folder_attributes() {
assert!(
err_message("#[folder = \"src\"] #[folder = \"src\"] struct Bad;")
.contains("one and only one folder attribute")
);
}
#[test]
fn rejects_multiple_prefix_attributes() {
assert!(err_message(
"#[folder = \"src\"] #[prefix = \"a/\"] #[prefix = \"b/\"] struct Bad;"
)
.contains("at most one prefix"));
}
#[test]
fn rejects_a_missing_folder() {
assert!(
err_message("#[folder = \"does-not-exist\"] struct Bad;").contains("does not exist")
);
}
#[test]
fn allows_a_missing_folder_when_opted_in() {
let ast: syn::DeriveInput =
syn::parse_str("#[folder = \"does-not-exist\"] #[allow_missing = true] struct Good;")
.unwrap();
assert!(impl_rust_embed_for_web(&ast).is_ok());
}
#[test]
fn accepts_a_valid_unit_struct() {
let ast: syn::DeriveInput = syn::parse_str("#[folder = \"src\"] struct Good;").unwrap();
assert!(impl_rust_embed_for_web(&ast).is_ok());
}
}