1use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
2use quote::ToTokens;
3use tower_embed_core::headers;
4
5#[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
112struct DeriveEmbed {
114 ident: syn::Ident,
116 attrs: DeriveEmbedAttrs,
118}
119
120struct DeriveEmbedAttrs {
122 folder: String,
124 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}