geng_asset_derive/
lib.rs

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                /* where #(#field_constraints),* */ {
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}