rust_embed_for_web_impl/
lib.rs1#![recursion_limit = "1024"]
7#![forbid(unsafe_code)]
8#[macro_use]
9extern crate quote;
10extern crate proc_macro;
11
12mod attributes;
13mod compress;
14mod dynamic;
15mod embed;
16
17use attributes::read_attribute_config;
18use dynamic::generate_dynamic_impl;
19use embed::generate_embed_impl;
20use proc_macro::TokenStream;
21use proc_macro2::TokenStream as TokenStream2;
22use std::{env, path::Path};
23use syn::{Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
24
25fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
27 ast.attrs
28 .iter()
29 .filter(|value| value.path().is_ident(attr_name))
30 .map(|attr| &attr.meta)
31 .filter_map(|meta| match meta {
32 Meta::NameValue(MetaNameValue {
33 value:
34 Expr::Lit(ExprLit {
35 lit: Lit::Str(val), ..
36 }),
37 ..
38 }) => Some(val.value()),
39 _ => None,
40 })
41 .collect()
42}
43
44fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
45 match ast.data {
46 Data::Struct(ref data) => match data.fields {
47 Fields::Unit => {}
48 _ => {
49 return Err(syn::Error::new_spanned(
50 ast,
51 "RustEmbed can only be derived for unit structs",
52 ))
53 }
54 },
55 _ => {
56 return Err(syn::Error::new_spanned(
57 ast,
58 "RustEmbed can only be derived for unit structs",
59 ))
60 }
61 };
62
63 let mut folder_paths = find_attribute_values(ast, "folder");
64 if folder_paths.len() != 1 {
65 return Err(syn::Error::new_spanned(
66 ast,
67 "#[derive(RustEmbed)] must contain one and only one folder attribute",
68 ));
69 }
70 let folder_path = folder_paths.remove(0);
71 #[cfg(feature = "interpolate-folder-path")]
72 let folder_path = shellexpand::full(&folder_path)
73 .map_err(|e| {
74 syn::Error::new_spanned(ast, format!("Could not interpolate folder path: {e}"))
75 })?
76 .to_string();
77
78 let folder_path = if Path::new(&folder_path).is_relative() {
80 let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|e| {
81 syn::Error::new_spanned(
82 ast,
83 format!("Could not read the CARGO_MANIFEST_DIR environment variable: {e}"),
84 )
85 })?;
86 Path::new(&manifest_dir)
87 .join(folder_path)
88 .to_str()
89 .ok_or_else(|| {
90 syn::Error::new_spanned(
91 ast,
92 "The folder path does not have a valid string representation",
93 )
94 })?
95 .to_owned()
96 } else {
97 folder_path
98 };
99
100 let config = read_attribute_config(ast);
101
102 if !Path::new(&folder_path).exists() && !config.allow_missing() {
105 return Err(syn::Error::new_spanned(
106 ast,
107 format!(
108 "#[derive(RustEmbed)] folder '{folder_path}' does not exist. \
109 Set `#[allow_missing = true]` to allow a missing folder and \
110 generate an empty asset set instead."
111 ),
112 ));
113 }
114
115 let prefixes = find_attribute_values(ast, "prefix");
116 let prefix = if prefixes.is_empty() {
117 "".to_string()
118 } else if prefixes.len() == 1 {
119 prefixes[0].clone()
120 } else {
121 return Err(syn::Error::new_spanned(
122 ast,
123 "#[derive(RustEmbed)] must have at most one prefix, you supplied several",
124 ));
125 };
126
127 if cfg!(debug_assertions) && !cfg!(feature = "always-embed") {
128 Ok(generate_dynamic_impl(
129 &ast.ident,
130 &config,
131 &folder_path,
132 &prefix,
133 ))
134 } else {
135 Ok(generate_embed_impl(
136 &ast.ident,
137 &config,
138 &folder_path,
139 &prefix,
140 ))
141 }
142}
143
144#[proc_macro_derive(
145 RustEmbed,
146 attributes(folder, prefix, include, exclude, gzip, br, zstd, allow_missing)
147)]
148pub fn derive_input_object(input: TokenStream) -> TokenStream {
164 let ast: DeriveInput = match syn::parse(input) {
165 Ok(ast) => ast,
166 Err(e) => return e.to_compile_error().into(),
167 };
168 match impl_rust_embed_for_web(&ast) {
169 Ok(gen) => gen.into(),
170 Err(e) => e.to_compile_error().into(),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::impl_rust_embed_for_web;
177
178 fn err_message(input: &str) -> String {
179 let ast: syn::DeriveInput = syn::parse_str(input).unwrap();
180 impl_rust_embed_for_web(&ast)
181 .expect_err("expected the derive input to fail")
182 .to_string()
183 }
184
185 #[test]
186 fn rejects_enums() {
187 assert!(err_message("#[folder = \"src\"] enum Bad { A }")
188 .contains("can only be derived for unit structs"));
189 }
190
191 #[test]
192 fn rejects_structs_with_fields() {
193 assert!(err_message("#[folder = \"src\"] struct Bad { field: u32 }")
194 .contains("can only be derived for unit structs"));
195 }
196
197 #[test]
198 fn requires_a_folder_attribute() {
199 assert!(err_message("struct Bad;").contains("one and only one folder attribute"));
200 }
201
202 #[test]
203 fn rejects_multiple_folder_attributes() {
204 assert!(
205 err_message("#[folder = \"src\"] #[folder = \"src\"] struct Bad;")
206 .contains("one and only one folder attribute")
207 );
208 }
209
210 #[test]
211 fn rejects_multiple_prefix_attributes() {
212 assert!(err_message(
213 "#[folder = \"src\"] #[prefix = \"a/\"] #[prefix = \"b/\"] struct Bad;"
214 )
215 .contains("at most one prefix"));
216 }
217
218 #[test]
219 fn rejects_a_missing_folder() {
220 assert!(
221 err_message("#[folder = \"does-not-exist\"] struct Bad;").contains("does not exist")
222 );
223 }
224
225 #[test]
226 fn allows_a_missing_folder_when_opted_in() {
227 let ast: syn::DeriveInput =
228 syn::parse_str("#[folder = \"does-not-exist\"] #[allow_missing = true] struct Good;")
229 .unwrap();
230 assert!(impl_rust_embed_for_web(&ast).is_ok());
231 }
232
233 #[test]
234 fn accepts_a_valid_unit_struct() {
235 let ast: syn::DeriveInput = syn::parse_str("#[folder = \"src\"] struct Good;").unwrap();
238 assert!(impl_rust_embed_for_web(&ast).is_ok());
239 }
240}