Skip to main content

tower_embed_impl/
lib.rs

1use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
2use quote::ToTokens;
3use tower_embed_core::headers;
4
5/// Derive the `Embed` trait for unit struct, embedding assets from a folder.
6///
7/// ## Usage
8///
9/// Apply `#[derive(Embed)]` to a unit struct and specify the folder to embed using the `#[embed(folder = "...")]` attribute.
10///
11/// Optionally, specify the crate path with `#[embed(crate = path)]`. This is applicable when
12/// invoking re-exported derive from a public macro in a different crate.
13#[proc_macro_derive(Embed, attributes(embed))]
14pub fn derive_embed(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
15    let input = syn::parse_macro_input!(input as syn::DeriveInput);
16
17    expand_derive_embed(input)
18        .unwrap_or_else(|err| err.to_compile_error())
19        .into()
20}
21
22fn expand_derive_embed(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
23    let DeriveEmbed { ident, attrs } = DeriveEmbed::from_ast(&input)?;
24    let DeriveEmbedAttrs { folder, crate_path } = attrs;
25
26    let root = root_absolute_path(&folder);
27    let embedded_files = get_files(&root).map(|file| {
28        let last_modified = tower_embed_core::last_modified(file.absolute_path.as_std_path())
29            .ok()
30            .and_then(|headers::LastModified(time)| {
31                time.duration_since(std::time::UNIX_EPOCH)
32                    .map(|duration| duration.as_secs())
33                    .ok()
34            });
35        let last_modified = match last_modified {
36            Some(secs) => quote::quote! { headers::LastModified::from_unix_timestamp(#secs) },
37            None => quote::quote! { None },
38        };
39
40        let relative_path = file.relative_path.as_str();
41        let absolute_path = file.absolute_path.as_str();
42
43        quote::quote! {{
44            let content = include_bytes!(#absolute_path).as_slice();
45            let metadata = Metadata {
46                content_type: #crate_path::core::content_type(Path::new(#relative_path)),
47                etag: Some(#crate_path::core::etag(content)),
48                last_modified: #last_modified,
49            };
50            (#relative_path, (content, metadata))
51        }}
52    });
53
54    let root = root.as_str();
55
56    let expanded = quote::quote! {
57        impl #crate_path::Embed for #ident {
58            #[cfg(not(debug_assertions))]
59            fn get(path: &str) -> impl Future<Output = std::io::Result<#crate_path::core::Embedded>> + Send + 'static {
60                use std::{collections::HashMap, sync::LazyLock, path::Path};
61
62                use #crate_path::core::{Content, Embedded, Metadata, headers};
63
64                const FILES: LazyLock<HashMap<&'static str, (&'static [u8], Metadata)>> = LazyLock::new(|| {
65                    let mut m = HashMap::new();
66                    #({
67                        let (key, value) = #embedded_files;
68                        m.insert(key, value);
69                    })*
70                    m
71                });
72
73                let output = match FILES.get(path) {
74                    Some((bytes, metadata)) => Ok(Embedded {
75                        content: Content::from_static(bytes),
76                        metadata: metadata.clone(),
77                    }),
78                    None => Err(std::io::ErrorKind::NotFound.into()),
79                };
80                std::future::ready(output)
81            }
82
83            #[cfg(debug_assertions)]
84            fn get(path: &str) -> impl Future<Output = std::io::Result<#crate_path::core::Embedded>> + Send + 'static {
85                use std::path::Path;
86
87                use #crate_path::core::{Content, Embedded, Metadata};
88
89                const ROOT: &str = #root;
90
91                let metadata = Metadata {
92                    content_type: #crate_path::core::content_type(Path::new(path)),
93                    etag: None,
94                    last_modified: None,
95                };
96                let filename = Path::new(ROOT).join(path.trim_start_matches('/'));
97                async move {
98                    #crate_path::file::File::open(&filename).await.map(|file| {
99                        Embedded {
100                            content: Content::from_stream(file),
101                            metadata,
102                        }
103                    })
104                }
105            }
106        }
107    };
108
109    Ok(expanded)
110}
111
112/// A source data annotated with `#[derive(Embed)]``
113struct DeriveEmbed {
114    /// The struct name
115    ident: syn::Ident,
116    /// Attributes of structure
117    attrs: DeriveEmbedAttrs,
118}
119
120/// Attributes for `Embed` derive macro.
121struct DeriveEmbedAttrs {
122    /// The folder to embed
123    folder: String,
124    /// The path to the crate `tower_embed`
125    crate_path: syn::Path,
126}
127
128impl DeriveEmbed {
129    fn from_ast(input: &syn::DeriveInput) -> syn::Result<Self> {
130        let syn::Data::Struct(data) = &input.data else {
131            return Err(syn::Error::new_spanned(
132                input,
133                "`Embed` can only be derived for unit structs",
134            ));
135        };
136
137        if !matches!(&data.fields, syn::Fields::Unit) {
138            return Err(syn::Error::new_spanned(
139                &data.fields,
140                "`Embed` can only be derived for unit structs",
141            ));
142        }
143
144        let ident = input.ident.clone();
145        let attrs = DeriveEmbedAttrs::from_ast(input)?;
146
147        Ok(Self { ident, attrs })
148    }
149}
150
151impl DeriveEmbedAttrs {
152    fn from_ast(input: &syn::DeriveInput) -> syn::Result<Self> {
153        let mut folder = None;
154        let mut crate_path = None;
155
156        for attr in &input.attrs {
157            if !attr.path().is_ident("embed") {
158                continue;
159            }
160
161            let list = attr.meta.require_list()?;
162            if list.tokens.is_empty() {
163                continue;
164            }
165
166            list.parse_nested_meta(|meta| {
167                if meta.path.is_ident("folder") {
168                    let value: syn::LitStr = meta.value()?.parse()?;
169                    folder = Some(value.value());
170                } else if meta.path.is_ident("crate") {
171                    let value: syn::Path = meta.value()?.parse()?;
172                    crate_path = Some(value);
173                } else {
174                    let name = meta.path.to_token_stream();
175                    return Err(syn::Error::new_spanned(
176                        meta.path,
177                        format_args!("unknown `embed` attribute for `{}`", name),
178                    ));
179                }
180                Ok(())
181            })?;
182        }
183
184        let Some(folder) = folder else {
185            return Err(syn::Error::new_spanned(
186                input,
187                "#[derive(Embed)] requires `folder` attribute",
188            ));
189        };
190
191        let crate_path = crate_path.unwrap_or_else(|| syn::parse_quote! { tower_embed });
192
193        Ok(Self { folder, crate_path })
194    }
195}
196
197fn root_absolute_path(folder: &str) -> PathBuf {
198    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
199        .expect("missing CARGO_MANIFEST_DIR environment variable");
200
201    Path::new(&manifest_dir).join(folder)
202}
203
204fn get_files(root: &Path) -> impl Iterator<Item = File> {
205    walkdir::WalkDir::new(root)
206        .follow_links(true)
207        .sort_by_file_name()
208        .into_iter()
209        .filter_map(Result::ok)
210        .filter(|entry| entry.file_type().is_file())
211        .map(move |entry| {
212            let absolute_path: &Path = entry.path().try_into().unwrap();
213            let absolute_path = absolute_path.to_path_buf();
214
215            let relative_path = absolute_path
216                .canonicalize_utf8()
217                .unwrap()
218                .strip_prefix(root)
219                .unwrap()
220                .to_path_buf();
221
222            File {
223                relative_path,
224                absolute_path,
225            }
226        })
227}
228
229struct File {
230    relative_path: PathBuf,
231    absolute_path: PathBuf,
232}