1#![recursion_limit = "128"]
2#![allow(unused_imports)]
3
4extern crate proc_macro;
5
6#[macro_use]
7extern crate quote;
8
9use darling::{FromDeriveInput, FromField, FromMeta};
10use proc_macro2::TokenStream;
11
12#[proc_macro_derive(Load, attributes(load))]
13pub fn derive_assets(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
14 let input: syn::DeriveInput = syn::parse_macro_input!(input);
15 match DeriveInput::from_derive_input(&input) {
16 Ok(input) => input.derive().into(),
17 Err(e) => e.write_errors().into(),
18 }
19}
20
21#[derive(FromDeriveInput)]
22#[darling(attributes(load))]
23struct DeriveInput {
24 ident: syn::Ident,
25 generics: syn::Generics,
26 data: darling::ast::Data<(), Field>,
27 #[darling(default)]
28 serde: Option<String>,
29 #[darling(default)]
30 sequential: bool,
31}
32
33struct Options {
34 setters: Vec<(syn::Ident, syn::Expr)>,
35}
36
37impl darling::FromMeta for Options {
38 fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
39 Ok(Self {
40 setters: items
41 .iter()
42 .map(|item| {
43 if let darling::ast::NestedMeta::Meta(syn::Meta::NameValue(meta)) = item {
44 let ident: syn::Ident = meta
45 .path
46 .get_ident()
47 .ok_or(darling::Error::unsupported_shape("key must be an ident"))?
48 .clone();
49 let syn::Expr::Lit(syn::ExprLit {
50 lit: syn::Lit::Str(lit),
51 ..
52 }) = &meta.value
53 else {
54 return Err(darling::Error::unsupported_shape("lit must be str"));
55 };
56 let expr: syn::Expr = syn::parse_str(&lit.value())?;
57 Ok((ident, expr))
58 } else {
59 Err(darling::Error::unsupported_shape("expected namevalue"))
60 }
61 })
62 .collect::<Result<Vec<_>, _>>()?,
63 })
64 }
65}
66
67#[derive(FromField)]
68#[darling(attributes(load))]
69struct Field {
70 ident: Option<syn::Ident>,
71 ty: syn::Type,
72 #[darling(default)]
73 path: Option<String>,
74 #[darling(default)]
75 ext: Option<String>,
76 #[darling(default)]
77 postprocess: Option<syn::Path>,
78 #[darling(default, map = "parse_syn")]
79 load_with: Option<syn::Expr>,
80 #[darling(default, map = "parse_syn")]
81 list: Option<syn::Expr>,
82 #[darling(default)]
83 listed_in: Option<String>,
84 #[darling(default)]
85 condition: Option<syn::Expr>,
86 #[darling(default)]
87 serde: bool,
88 options: Option<Options>,
89}
90
91fn parse_syn<T: syn::parse::Parse>(value: Option<String>) -> Option<T> {
92 value.map(|s| syn::parse_str(&s).unwrap())
93}
94
95impl DeriveInput {
96 pub fn derive(self) -> TokenStream {
97 let Self {
98 ident,
99 generics,
100 data,
101 serde,
102 sequential,
103 } = self;
104 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
105
106 if let Some(ext) = serde {
107 return quote! {
108 #[allow(clippy::needless_question_mark)]
109 impl #impl_generics geng::asset::Load #ty_generics for #ident #where_clause {
110 type Options = ();
111 fn load(manager: &geng::asset::Manager, path: &std::path::Path, options: &Self::Options) -> geng::asset::Future<Self> {
112 let path = path.to_owned();
113 async move {
114 Ok(batbox::file::load_detect(path).await?)
115 }.boxed_local()
116 }
117 const DEFAULT_EXT: Option<&'static str> = Some(#ext);
118 }
119 };
120 }
121
122 let data = data.take_struct().unwrap();
123 let field_names = data
124 .fields
125 .iter()
126 .enumerate()
127 .map(|(index, field)| {
128 field
129 .ident
130 .as_ref()
131 .map(|ident| quote! { #ident })
132 .unwrap_or_else(|| {
133 let index = syn::Index::from(index);
134 quote! { #index }
135 })
136 })
137 .collect::<Vec<_>>();
138 let field_loaders = data.fields.iter().map(|field| {
139 if let Some(expr) = &field.load_with {
140 return quote!(#expr);
141 }
142 let ident = field.ident.as_ref().unwrap();
143 let ext = match &field.ext {
144 Some(ext) => quote!(Some(#ext)),
145 None => quote!(None::<&str>),
146 };
147 if field.serde {
148 return match &field.path {
149 Some(path) => quote! {
150 batbox::file::load_detect(base_path.join(#path))
151 },
152 None => quote! {
153 batbox::file::load_detect(base_path.join(stringify!(#ident)), #ext)
154 },
155 };
156 }
157 let list = match (&field.listed_in, &field.list) {
158 (None, None) => None,
159 (None, Some(range)) => Some(quote! {
160 (#range).map(|item| item.to_string())
161 }),
162 (Some(listed_in), None) => Some({
163 let base_path = match &field.path {
164 Some(_) => quote! { base_path },
165 None => quote! {
166 base_path.join(stringify!(#ident))
167 },
168 };
169 quote! {
170 file::load_detect::<Vec<String>>(
171 #base_path.join(#listed_in)
172 ).await?.into_iter()
173 }
174 }),
175 (Some(_), Some(_)) => panic!("Can't specify both list and listed_in"),
176 };
177 let field_ty = &field.ty;
178 let field_ty = match field.condition.is_some() {
179 false => {
180 quote!(#field_ty)
181 }
182 true => {
183 quote!(<#field_ty as geng::asset::Optional>::Type)
184 }
185 };
186 let options_setters = field.options.iter().flat_map(|options| options.setters.iter()).map(|(ident, expr)| {
187 quote! {
188 options.#ident = #expr;
189 }
190 });
191 let options_ty = match list {
192 Some(_) => quote!(<<#field_ty as geng::asset::Collection>::Item as geng::asset::Load>::Options),
193 None => quote!(<#field_ty as geng::asset::Load>::Options)
194 };
195 let options = quote! {
196 let mut options: #options_ty = Default::default();
197 #(#options_setters)*
198 };
199 let mut loader = if let Some(list) = list {
200 let loader = match &field.path {
201 Some(path) => quote! {
202 manager.load_with(base_path.join(#path.replace("*", &item)), &options)
203 },
204 None => quote! {
205 manager.load_ext(base_path.join(stringify!(#ident)).join(item), &options, #ext)
206 },
207 };
208 quote! {
209 futures::future::try_join_all((#list).map(|item| { #loader }))
210 }
211 } else {
212 match &field.path {
213 Some(path) => quote! {
214 manager.load_with(base_path.join(#path), &options)
215 },
216 None => quote! {
217 manager.load_ext(base_path.join(stringify!(#ident)), &options, #ext)
218 },
219 }
220 };
221 loader = quote! {{
222 #options
223 #loader
224 }};
225 if let Some(postprocess) = &field.postprocess {
226 loader = quote! {
227 #loader.map(|result| {
228 result.map(|mut asset| {
229 #postprocess(&mut asset);
230 asset
231 })
232 })
233 };
234 }
235 loader
236 });
237 let field_loaders = data
238 .fields
239 .iter()
240 .zip(field_loaders)
241 .map(|(field, loader)| {
242 let loader = if let Some(expr) = &field.condition {
243 quote! {
244 async {
245 let result = Ok::<_, anyhow::Error>(if #expr {
246 Some(#loader.await?)
247 } else {
248 None
249 });
250 manager.yield_now().await;
251 result
252 }
253 }
254 } else {
255 loader
256 };
257 let ty = &field.ty;
258 quote! {
259 async {
260 let value = #loader.await?;
261 manager.yield_now().await;
262 Ok::<#ty, anyhow::Error>(value)
263 }
264 }
265 });
266 let load_fields = if sequential {
267 quote! {
268 #(
269 let #field_names = anyhow::Context::context(
270 #field_loaders.await,
271 concat!("Failed to load ", stringify!(#field_names)),
272 )?;
273 manager.yield_now().await;
274 )*
275 }
276 } else {
277 quote! {
278 #(let #field_names = #field_loaders;)*
279 let (#(#field_names,)*) = futures::join!(#(#field_names,)*);
280 #(
281 let #field_names = anyhow::Context::context(
282 #field_names,
283 concat!("Failed to load ", stringify!(#field_names)),
284 )?;
285 )*
286 }
287 };
288 quote! {
289 #[allow(clippy::needless_question_mark)]
290 impl geng::asset::Load for #ident
291 {
292 type Options = ();
293 fn load(manager: &geng::asset::Manager, base_path: &std::path::Path, options: &Self::Options) -> geng::asset::Future<Self> {
294 let manager = manager.clone();
295 let base_path = base_path.to_owned();
296 Box::pin(async move {
297 #load_fields
298 Ok(Self {
299 #(#field_names,)*
300 })
301 })
302 }
303 const DEFAULT_EXT: Option<&'static str> = None;
304 }
305 }
306 }
307}