Skip to main content

tower_embed_impl/
lib.rs

1use std::borrow::Cow;
2
3use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
4use quote::ToTokens;
5use tower_embed_core::headers;
6
7/// Derive the `Embed` trait for unit struct, embedding assets from a folder.
8///
9/// ## Usage
10///
11/// Apply `#[derive(Embed)]` to a unit struct and specify the folder to embed using the
12/// `#[embed(folder = "...")]` attribute.
13///
14/// Optionally, specify the crate path with `#[embed(crate = path)]`. This is applicable when
15/// invoking re-exported derive from a public macro in a different crate.
16///
17/// The name of file to serve as index for directories can be customized using #[embed(index =
18/// "...")], the default is "index.html".
19///
20/// If the `astro` feature is enabled, you can enable Astro support using the attributes `astro`.
21/// In such case, if `folder` is not specified, the project root used is the manifest folder. For
22/// astro projects, the `index` attribute cannot be used to customize the index for directories.
23#[proc_macro_derive(Embed, attributes(embed))]
24pub fn derive_embed(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
25    let input = syn::parse_macro_input!(input as syn::DeriveInput);
26
27    expand_derive_embed(input)
28        .unwrap_or_else(|err| err.to_compile_error())
29        .into()
30}
31
32fn expand_derive_embed(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
33    let input = DeriveEmbedFolder::from_ast(&input)?;
34
35    let static_embed = expand_static_embed(&input)?;
36    let dynamic_embed = expand_dynamic_embed(&input);
37
38    let expanded = quote::quote! {
39        #[cfg(not(debug_assertions))]
40        #static_embed
41
42        #[cfg(debug_assertions)]
43        #dynamic_embed
44    };
45
46    Ok(expanded)
47}
48
49fn expand_static_embed(input: &DeriveEmbedFolder) -> syn::Result<proc_macro2::TokenStream> {
50    let DeriveEmbedFolder { ident, attrs } = input;
51    let DeriveEmbedFolderAttrs {
52        folder,
53        crate_path,
54        index,
55        ..
56    } = attrs;
57
58    let root = root_absolute_path(folder);
59
60    #[cfg(feature = "astro")]
61    let root = if attrs.astro {
62        tower_embed_core::astro::sync(root.as_std_path())
63            .and_then(|_| tower_embed_core::astro::build_project(root.as_std_path()))
64            .map_err(|err| {
65                syn::Error::new_spanned(ident, format!("Failed to build Astro project: {err}"))
66            })?
67            .try_into()
68            .unwrap()
69    } else {
70        root
71    };
72
73    let embedded_files = get_files(&root, index).map(|file| {
74        let last_modified = tower_embed_core::last_modified(file.absolute_path.as_std_path())
75            .ok()
76            .and_then(|headers::LastModified(time)| {
77                time.duration_since(std::time::UNIX_EPOCH)
78                    .map(|duration| duration.as_secs())
79                    .ok()
80            });
81        let last_modified = match last_modified {
82            Some(secs) => quote::quote! { headers::LastModified::from_unix_timestamp(#secs) },
83            None => quote::quote! { None },
84        };
85
86        let relative_path = file.relative_path.as_str();
87        let absolute_path = file.absolute_path.as_str();
88        let redirect_path = format!("{relative_path}/{index}");
89        let redirect_path = redirect_path.trim_start_matches('/');
90
91        match file.kind {
92            FileKind::File => quote::quote! {{
93                let content = include_bytes!(#absolute_path).as_slice();
94                let metadata = Metadata {
95                    content_type: #crate_path::core::content_type(Path::new(#relative_path)),
96                    etag: Some(#crate_path::core::etag(content)),
97                    last_modified: #last_modified,
98                };
99                [(#relative_path, Entry::File(content, metadata))]
100            }},
101            FileKind::Dir => quote::quote! {{
102                [
103                    (#relative_path, Entry::Redirect(#redirect_path)),
104                    (concat!(#relative_path, "/"), Entry::Redirect(#redirect_path)),
105                ]
106            }},
107        }
108    });
109
110    Ok(quote::quote! {
111        impl #crate_path::core::Embed for #ident {
112            fn forward(
113                req: #crate_path::core::http::Request<()>,
114            ) -> impl Future<Output = #crate_path::core::http::Response<#crate_path::core::Body>> + Send + 'static
115            {
116                use std::{collections::HashMap, sync::LazyLock, path::Path};
117                use #crate_path::core::{Content, Embedded, EmbeddedExt, Metadata, headers};
118
119                enum Entry {
120                    File(&'static [u8], Metadata),
121                    Redirect(&'static str),
122                }
123
124                static FILES: LazyLock<HashMap<&'static str, Entry>> = LazyLock::new(|| {
125                    let mut m = HashMap::new();
126                    #(m.extend(#embedded_files);)*
127                    m
128                });
129
130                let mut path = req.uri().path().trim_start_matches('/');
131                let output = loop {
132                    match FILES.get(path) {
133                        Some(Entry::File(bytes, metadata)) => break Ok(Embedded {
134                            content: Content::from_static(bytes),
135                            metadata: metadata.clone(),
136                        }),
137                        Some(Entry::Redirect(redirect)) => {
138                            path = redirect;
139                        }
140                        None => break Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
141                    };
142                };
143                std::future::ready(output.into_response(req))
144            }
145
146        }
147    })
148}
149
150fn expand_dynamic_embed(input: &DeriveEmbedFolder) -> proc_macro2::TokenStream {
151    let DeriveEmbedFolder { ident, attrs } = input;
152    let DeriveEmbedFolderAttrs {
153        folder,
154        crate_path,
155        index,
156        astro,
157    } = attrs;
158
159    let root = root_absolute_path(folder);
160    let root = root.as_str();
161
162    if *astro {
163        quote::quote! {
164            impl #crate_path::core::Embed for #ident {
165                fn forward(
166                    req: #crate_path::core::http::Request<()>,
167                ) -> impl Future<Output = #crate_path::core::http::Response<#crate_path::core::Body>> + Send + 'static
168                {
169                    use std::{path::Path, sync::LazyLock};
170                    use #crate_path::core::astro::AstroProxy;
171
172                    static ASTRO: LazyLock<AstroProxy> = LazyLock::new(|| {
173                        AstroProxy::new(&Path::new(#root)).expect("Failed to start Astro dev server")
174                    });
175
176                    ASTRO.send_request(req)
177                }
178            }
179        }
180    } else {
181        quote::quote! {
182            impl #crate_path::core::Embed for #ident {
183                fn forward(
184                    req: #crate_path::core::http::Request<()>,
185                ) -> impl Future<Output = #crate_path::core::http::Response<#crate_path::core::Body>> + Send + 'static
186                {
187                    let path = req.uri().path().trim_start_matches('/').to_string();
188                    async move {
189                        use #crate_path::core::EmbeddedExt;
190                        #crate_path::core::Embedded::load_file(path, #root, #index).await.into_response(req)
191                    }
192                }
193            }
194        }
195    }
196}
197
198/// A source data annotated with `#[derive(Embed)]``
199struct DeriveEmbedFolder {
200    /// The struct name
201    ident: syn::Ident,
202    /// Attributes of structure
203    attrs: DeriveEmbedFolderAttrs,
204}
205
206/// Attributes for `Embed` derive macro.
207struct DeriveEmbedFolderAttrs {
208    /// The folder to embed
209    folder: String,
210    /// The path to the crate `tower_embed`
211    crate_path: syn::Path,
212    /// The index file name
213    index: Cow<'static, str>,
214    /// Enable support to Astro
215    astro: bool,
216}
217
218impl DeriveEmbedFolder {
219    fn from_ast(input: &syn::DeriveInput) -> syn::Result<Self> {
220        let syn::Data::Struct(data) = &input.data else {
221            return Err(syn::Error::new_spanned(
222                input,
223                "`Embed` can only be derived for unit structs",
224            ));
225        };
226
227        if !matches!(&data.fields, syn::Fields::Unit) {
228            return Err(syn::Error::new_spanned(
229                &data.fields,
230                "`Embed` can only be derived for unit structs",
231            ));
232        }
233
234        let ident = input.ident.clone();
235        let attrs = DeriveEmbedFolderAttrs::from_ast(input)?;
236
237        Ok(Self { ident, attrs })
238    }
239}
240
241impl DeriveEmbedFolderAttrs {
242    fn from_ast(input: &syn::DeriveInput) -> syn::Result<Self> {
243        let mut folder = None;
244        let mut crate_path = None;
245        let mut index = None;
246        let mut astro = false;
247
248        for attr in &input.attrs {
249            if !attr.path().is_ident("embed") {
250                continue;
251            }
252
253            let list = attr.meta.require_list()?;
254            if list.tokens.is_empty() {
255                continue;
256            }
257
258            list.parse_nested_meta(|meta| {
259                if meta.path.is_ident("folder") {
260                    let value: syn::LitStr = meta.value()?.parse()?;
261                    folder = Some(value.value());
262                } else if meta.path.is_ident("crate") {
263                    let value: syn::Path = meta.value()?.parse()?;
264                    crate_path = Some(value);
265                } else if meta.path.is_ident("index") {
266                    let value: syn::LitStr = meta.value()?.parse()?;
267                    index = Some(Cow::Owned(value.value()));
268                } else if meta.path.is_ident("astro") {
269                    if cfg!(not(feature = "astro")) {
270                        return Err(syn::Error::new_spanned(
271                            meta.path,
272                            "`astro` feature is not enabled",
273                        ));
274                    } else {
275                        astro = true;
276                    }
277                } else {
278                    let name = meta.path.to_token_stream();
279                    return Err(syn::Error::new_spanned(
280                        meta.path,
281                        format_args!("unknown `{}` attribute for `embed`", name),
282                    ));
283                }
284                Ok(())
285            })?;
286        }
287
288        // If astro is enabled and folder is not specified, use CARGO_MANIFEST_DIR as project root
289        if astro && folder.is_none() {
290            folder = Some(manifest_dir().to_string());
291        }
292
293        if astro && index.is_some() {
294            return Err(syn::Error::new_spanned(
295                input,
296                "`index` attribute cannot be used with `astro` attribute",
297            ));
298        }
299
300        let Some(folder) = folder else {
301            return Err(syn::Error::new_spanned(
302                input,
303                "#[derive(Embed)] requires `folder` attribute",
304            ));
305        };
306
307        let crate_path = crate_path.unwrap_or_else(|| syn::parse_quote! { tower_embed });
308        let index = index.unwrap_or(Cow::Borrowed("index.html"));
309
310        Ok(Self {
311            folder,
312            crate_path,
313            index,
314            astro,
315        })
316    }
317}
318
319fn manifest_dir() -> PathBuf {
320    PathBuf::from(
321        std::env::var("CARGO_MANIFEST_DIR")
322            .expect("missing CARGO_MANIFEST_DIR environment variable"),
323    )
324}
325
326fn root_absolute_path(folder: &str) -> PathBuf {
327    Path::new(&manifest_dir()).join(folder)
328}
329
330fn get_files(root: &Path, index: &str) -> impl Iterator<Item = File> {
331    walkdir::WalkDir::new(root)
332        .follow_links(true)
333        .sort_by_file_name()
334        .into_iter()
335        .filter_map(Result::ok)
336        .filter_map(move |entry| {
337            let kind = if entry.file_type().is_file() {
338                FileKind::File
339            } else if entry.file_type().is_dir() {
340                if !entry.path().join(index).is_file() {
341                    return None;
342                }
343
344                FileKind::Dir
345            } else {
346                return None;
347            };
348
349            let absolute_path: &Path = entry.path().try_into().unwrap();
350            let absolute_path = absolute_path.to_path_buf();
351
352            let relative_path = absolute_path
353                .canonicalize_utf8()
354                .unwrap()
355                .strip_prefix(root)
356                .unwrap()
357                .to_path_buf();
358
359            Some(File {
360                kind,
361                relative_path,
362                absolute_path,
363            })
364        })
365}
366
367struct File {
368    kind: FileKind,
369    relative_path: PathBuf,
370    absolute_path: PathBuf,
371}
372
373enum FileKind {
374    File,
375    Dir,
376}