Skip to main content

rust_embed_for_web_impl/
lib.rs

1//! This crate contains the implementation of the `RustEmbed` macro for
2//! `rust-embed-for-web`.
3//!
4//! You generally don't want to use this crate directly, `rust-embed-for-web`
5//! re-exports any necessary parts from this crate.
6#![recursion_limit = "1024"]
7#![forbid(unsafe_code)]
8#[macro_use]
9extern crate quote;
10extern crate proc_macro;
11
12mod attributes;
13mod compress;
14mod dynamic;
15mod embed;
16
17use attributes::read_attribute_config;
18use dynamic::generate_dynamic_impl;
19use embed::generate_embed_impl;
20use proc_macro::TokenStream;
21use proc_macro2::TokenStream as TokenStream2;
22use std::{env, path::Path};
23use syn::{Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
24
25/// Find all pairs of the `name = "value"` attribute from the derive input
26fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
27    ast.attrs
28        .iter()
29        .filter(|value| value.path().is_ident(attr_name))
30        .map(|attr| &attr.meta)
31        .filter_map(|meta| match meta {
32            Meta::NameValue(MetaNameValue {
33                value:
34                    Expr::Lit(ExprLit {
35                        lit: Lit::Str(val), ..
36                    }),
37                ..
38            }) => Some(val.value()),
39            _ => None,
40        })
41        .collect()
42}
43
44fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
45    match ast.data {
46        Data::Struct(ref data) => match data.fields {
47            Fields::Unit => {}
48            _ => {
49                return Err(syn::Error::new_spanned(
50                    ast,
51                    "RustEmbed can only be derived for unit structs",
52                ))
53            }
54        },
55        _ => {
56            return Err(syn::Error::new_spanned(
57                ast,
58                "RustEmbed can only be derived for unit structs",
59            ))
60        }
61    };
62
63    let mut folder_paths = find_attribute_values(ast, "folder");
64    if folder_paths.len() != 1 {
65        return Err(syn::Error::new_spanned(
66            ast,
67            "#[derive(RustEmbed)] must contain one and only one folder attribute",
68        ));
69    }
70    let folder_path = folder_paths.remove(0);
71    #[cfg(feature = "interpolate-folder-path")]
72    let folder_path = shellexpand::full(&folder_path)
73        .map_err(|e| {
74            syn::Error::new_spanned(ast, format!("Could not interpolate folder path: {e}"))
75        })?
76        .to_string();
77
78    // Base relative paths on the Cargo.toml location
79    let folder_path = if Path::new(&folder_path).is_relative() {
80        let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|e| {
81            syn::Error::new_spanned(
82                ast,
83                format!("Could not read the CARGO_MANIFEST_DIR environment variable: {e}"),
84            )
85        })?;
86        Path::new(&manifest_dir)
87            .join(folder_path)
88            .to_str()
89            .ok_or_else(|| {
90                syn::Error::new_spanned(
91                    ast,
92                    "The folder path does not have a valid string representation",
93                )
94            })?
95            .to_owned()
96    } else {
97        folder_path
98    };
99
100    let config = read_attribute_config(ast);
101
102    // If the folder does not exist, either fail the build or, when the
103    // `allow_missing` attribute is set, generate an empty asset set.
104    if !Path::new(&folder_path).exists() && !config.allow_missing() {
105        return Err(syn::Error::new_spanned(
106            ast,
107            format!(
108                "#[derive(RustEmbed)] folder '{folder_path}' does not exist. \
109                 Set `#[allow_missing = true]` to allow a missing folder and \
110                 generate an empty asset set instead."
111            ),
112        ));
113    }
114
115    let prefixes = find_attribute_values(ast, "prefix");
116    let prefix = if prefixes.is_empty() {
117        "".to_string()
118    } else if prefixes.len() == 1 {
119        prefixes[0].clone()
120    } else {
121        return Err(syn::Error::new_spanned(
122            ast,
123            "#[derive(RustEmbed)] must have at most one prefix, you supplied several",
124        ));
125    };
126
127    if cfg!(debug_assertions) && !cfg!(feature = "always-embed") {
128        Ok(generate_dynamic_impl(
129            &ast.ident,
130            &config,
131            &folder_path,
132            &prefix,
133        ))
134    } else {
135        Ok(generate_embed_impl(
136            &ast.ident,
137            &config,
138            &folder_path,
139            &prefix,
140        ))
141    }
142}
143
144#[proc_macro_derive(
145    RustEmbed,
146    attributes(folder, prefix, include, exclude, gzip, br, zstd, allow_missing)
147)]
148/// A folder that is embedded into your program.
149///
150/// For example:
151///
152/// ```ignore
153/// #[derive(RustEmbed)]
154/// #[folder = "examples/public"]
155/// struct MyEmbeddedFiles;
156/// ```
157///
158/// The `folder` is relative to where your `Cargo.toml` file is located. This
159/// example will embed the files under `<your-workspace>/examples/public` into
160/// your program.
161///
162/// Please check the package readme for more details.
163pub fn derive_input_object(input: TokenStream) -> TokenStream {
164    let ast: DeriveInput = match syn::parse(input) {
165        Ok(ast) => ast,
166        Err(e) => return e.to_compile_error().into(),
167    };
168    match impl_rust_embed_for_web(&ast) {
169        Ok(gen) => gen.into(),
170        Err(e) => e.to_compile_error().into(),
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::impl_rust_embed_for_web;
177
178    fn err_message(input: &str) -> String {
179        let ast: syn::DeriveInput = syn::parse_str(input).unwrap();
180        impl_rust_embed_for_web(&ast)
181            .expect_err("expected the derive input to fail")
182            .to_string()
183    }
184
185    #[test]
186    fn rejects_enums() {
187        assert!(err_message("#[folder = \"src\"] enum Bad { A }")
188            .contains("can only be derived for unit structs"));
189    }
190
191    #[test]
192    fn rejects_structs_with_fields() {
193        assert!(err_message("#[folder = \"src\"] struct Bad { field: u32 }")
194            .contains("can only be derived for unit structs"));
195    }
196
197    #[test]
198    fn requires_a_folder_attribute() {
199        assert!(err_message("struct Bad;").contains("one and only one folder attribute"));
200    }
201
202    #[test]
203    fn rejects_multiple_folder_attributes() {
204        assert!(
205            err_message("#[folder = \"src\"] #[folder = \"src\"] struct Bad;")
206                .contains("one and only one folder attribute")
207        );
208    }
209
210    #[test]
211    fn rejects_multiple_prefix_attributes() {
212        assert!(err_message(
213            "#[folder = \"src\"] #[prefix = \"a/\"] #[prefix = \"b/\"] struct Bad;"
214        )
215        .contains("at most one prefix"));
216    }
217
218    #[test]
219    fn rejects_a_missing_folder() {
220        assert!(
221            err_message("#[folder = \"does-not-exist\"] struct Bad;").contains("does not exist")
222        );
223    }
224
225    #[test]
226    fn allows_a_missing_folder_when_opted_in() {
227        let ast: syn::DeriveInput =
228            syn::parse_str("#[folder = \"does-not-exist\"] #[allow_missing = true] struct Good;")
229                .unwrap();
230        assert!(impl_rust_embed_for_web(&ast).is_ok());
231    }
232
233    #[test]
234    fn accepts_a_valid_unit_struct() {
235        // `src` exists relative to this crate's Cargo.toml, so the derive
236        // resolves the folder and emits an implementation.
237        let ast: syn::DeriveInput = syn::parse_str("#[folder = \"src\"] struct Good;").unwrap();
238        assert!(impl_rust_embed_for_web(&ast).is_ok());
239    }
240}