1use std::borrow::Cow;
2
3use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
4use quote::ToTokens;
5use tower_embed_core::headers;
6
7#[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
198struct DeriveEmbedFolder {
200 ident: syn::Ident,
202 attrs: DeriveEmbedFolderAttrs,
204}
205
206struct DeriveEmbedFolderAttrs {
208 folder: String,
210 crate_path: syn::Path,
212 index: Cow<'static, str>,
214 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 && 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}