embeddy_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use std::{fs::canonicalize, path::Path};
4use syn::{parse_macro_input, DeriveInput, Expr, ExprLit, Ident, Lit, Meta, MetaNameValue};
5use walkdir::WalkDir;
6
7/// Derive macro for the [`embeddy-derive::Embedded`] trait
8#[proc_macro_derive(Embedded, attributes(folder))]
9pub fn derive_embeddy(input: TokenStream) -> TokenStream {
10    let input: DeriveInput = parse_macro_input!(input);
11    let ident: Ident = input.ident;
12
13    // Find the #[folder = ""] attribute form the derive input
14    let folder: String = input
15        .attrs
16        .iter()
17        .find(|attr| attr.path().is_ident("folder"))
18        .and_then(|attr| match &attr.meta {
19            Meta::NameValue(MetaNameValue {
20                value:
21                    Expr::Lit(ExprLit {
22                        lit: Lit::Str(val), ..
23                    }),
24                ..
25            }) => Some(val.value()),
26            _ => None,
27        })
28        .expect("Missing folder attribute");
29
30    // Path to the folder relative to the Cargo.toml file
31    let folder_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join(folder);
32
33    if !folder_path.exists() {
34        panic!(
35            "Unable to embed folder that doesn't exist: {}",
36            folder_path.display()
37        )
38    }
39
40    let mut includes = Vec::new();
41
42    // Walk the folder path collecting all files
43    let files = WalkDir::new(&folder_path)
44        .into_iter()
45        .filter_map(|entry| entry.ok())
46        .filter(|entry| entry.file_type().is_file());
47
48    for file in files {
49        let rel_path = file
50            .path()
51            // Remove the path prefix from the relative path
52            .strip_prefix(&folder_path)
53            .unwrap()
54            .to_str()
55            .expect("Path missing str repr")
56            // Normalize the path seporator
57            .replace("\\", "/");
58
59        let full_path = canonicalize(file.path()).expect("Failed to get file canonical path");
60        let full_path = full_path.to_str().expect("Path missing str repr");
61
62        includes.push(quote! {
63            #rel_path => include_bytes!(#full_path),
64        });
65    }
66
67    quote! {
68        impl embeddy::Embedded for #ident {
69
70            fn get(path: &str) -> Option<&'static [u8]> {
71                Some(match path {
72                    #(#includes)*
73                    _ => return None
74                })
75            }
76        }
77    }
78    .into()
79}