dir_structure_macros/
lib.rs

1use proc_macro2::Ident;
2use proc_macro2::TokenStream;
3use quote::format_ident;
4use quote::quote;
5use syn::Field;
6use syn::ImplGenerics;
7use syn::ItemStruct;
8use syn::Token;
9use syn::Type;
10use syn::parse_quote;
11
12#[proc_macro_derive(DirStructure, attributes(dir_structure))]
13pub fn derive_dir_structure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
14    let item = syn::parse_macro_input!(item as ItemStruct);
15
16    expand_dir_structure(item)
17        .unwrap_or_else(|err| err.to_compile_error())
18        .into()
19}
20
21struct DirStructureForField {
22    read_code: TokenStream,
23    async_read_code: TokenStream,
24    async_read_bound: Option<syn::WherePredicate>,
25    write_code: TokenStream,
26    async_write_code: TokenStream,
27    async_write_bound: Option<syn::WherePredicate>,
28    async_write_owned_code: TokenStream,
29    async_write_owned_bound: Option<syn::WherePredicate>,
30    #[cfg(feature = "resolve-path")]
31    has_field_impl: TokenStream,
32}
33
34fn expand_dir_structure_for_field(
35    (impl_generics, ty_name, ty_generics, where_clause): (
36        &ImplGenerics,
37        &Ident,
38        &syn::TypeGenerics,
39        Option<&syn::WhereClause>,
40    ),
41    path_param_name: &Ident,
42    field: &Field,
43) -> syn::Result<DirStructureForField> {
44    let field_name = field.ident.as_ref().ok_or_else(|| {
45        syn::Error::new_spanned(
46            field,
47            "DirStructure can only be derived for structs with named fields",
48        )
49    })?;
50
51    let field_ty = &field.ty;
52
53    enum PathData {
54        SelfPath,
55        Path(String),
56        None,
57    }
58
59    let mut path = PathData::None;
60    let mut self_path = field_name == "self_path";
61    let mut with_newtype = None::<Type>;
62
63    for attr in field
64        .attrs
65        .iter()
66        .filter(|attr| attr.meta.path().is_ident("dir_structure"))
67    {
68        attr.parse_nested_meta(|meta| {
69            if meta.path.is_ident("path") {
70                let _eq = meta.input.parse::<Token![=]>()?;
71                if meta.input.peek(syn::LitStr) {
72                    let s = meta.input.parse::<syn::LitStr>()?;
73                    path = PathData::Path(s.value());
74                } else if meta.input.peek(Token![self]) {
75                    let _self = meta.input.parse::<Token![self]>()?;
76                    path = PathData::SelfPath;
77                } else {
78                    return Err(syn::Error::new_spanned(
79                        meta.path,
80                        "Expected a string literal or `self`",
81                    ));
82                }
83            } else if meta.path.is_ident("self_path") {
84                self_path = true;
85            } else if meta.path.is_ident("with_newtype") {
86                let _eq = meta.input.parse::<Token![=]>()?;
87                let ty = meta.input.parse::<Type>()?;
88                with_newtype = Some(ty);
89            } else {
90                return Err(syn::Error::new_spanned(
91                    meta.path,
92                    "Unknown attribute for dir_structure",
93                ));
94            }
95
96            Ok(())
97        })?;
98    }
99
100    let (actual_path_expr, actual_path_expr_move, path_pusher_for_has_field) = match path {
101        PathData::Path(p) => (
102            quote! { #path_param_name.join(#p) },
103            quote! { #path_param_name.join(#p) },
104            quote! { #path_param_name.push(#p); },
105        ),
106        PathData::SelfPath => (
107            quote! { #path_param_name },
108            quote! { #path_param_name.clone() },
109            quote! {},
110        ),
111        PathData::None => {
112            let name = field_name.to_string();
113            (
114                quote! { #path_param_name.join(#name) },
115                quote! { #path_param_name.join(#name) },
116                quote! { #path_param_name.push(#name); },
117            )
118        }
119    };
120    let actual_field_ty_perform = with_newtype.as_ref().unwrap_or(field_ty);
121    let read_code = if self_path {
122        // self_path field, just use the path directly
123        quote! {
124            #field_ty::from(#path_param_name)
125        }
126    } else {
127        let value_name = format_ident!("__value");
128        let end_expr = match &with_newtype {
129            Some(nt) => quote! {
130                <#nt as ::dir_structure::NewtypeToInner>::into_inner(#value_name)
131            },
132            None => quote! {
133                #value_name
134            },
135        };
136
137        quote! {{
138            let __translated_path = #actual_path_expr;
139            let #value_name = <#actual_field_ty_perform as ::dir_structure::ReadFrom>::read_from(&__translated_path)?;
140            #end_expr
141        }}
142    };
143
144    let async_read_code = if self_path {
145        // self_path field, just use the path directly
146        quote! {
147            #field_ty::from(#path_param_name)
148        }
149    } else {
150        let value_name = format_ident!("__value");
151        let end_expr = match &with_newtype {
152            Some(nt) => quote! {
153                <#nt as ::dir_structure::NewtypeToInner>::into_inner(#value_name)
154            },
155            None => quote! {
156                #value_name
157            },
158        };
159
160        quote! {{
161            let __translated_path = #actual_path_expr_move;
162            let #value_name = <#actual_field_ty_perform as ::dir_structure::ReadFromAsync>::read_from_async(__translated_path).await?;
163            #end_expr
164        }}
165    };
166
167    let write_code = if self_path {
168        // self_path does not need to write anything
169        quote! {}
170    } else {
171        let writer = match &with_newtype {
172            Some(nt) => {
173                quote! { &<#nt as ::dir_structure::FromRefForWriter<'_>>::from_ref_for_writer(&self.#field_name) }
174            }
175            None => quote! { &self.#field_name },
176        };
177        quote! {
178            let __translated_path = #actual_path_expr;
179            ::dir_structure::WriteTo::write_to(#writer, &__translated_path)?;
180        }
181    };
182
183    let async_write_code = if self_path {
184        // self_path does not need to write anything
185        quote! {}
186    } else {
187        match &with_newtype {
188            Some(nt) => {
189                quote! {
190                    let __translated_path = #actual_path_expr_move;
191                    <<#nt as ::dir_structure::FromRefForWriterAsync<'_>>::Wr as ::dir_structure::WriteToAsyncOwned<'_>>::write_to_async_owned(<#nt as ::dir_structure::FromRefForWriterAsync<'_>>::from_ref_for_writer_async(&self.#field_name), __translated_path).await?;
192                }
193            }
194            None => quote! {
195                let __translated_path = #actual_path_expr_move;
196                <#actual_field_ty_perform as ::dir_structure::WriteToAsync>::write_to_async(&self.#field_name, __translated_path).await?;
197            },
198        }
199    };
200
201    let async_write_owned_code = if self_path {
202        // self_path does not need to write anything
203        quote! {}
204    } else {
205        quote! {
206            let __translated_path = #actual_path_expr_move;
207            ::dir_structure::WriteToAsyncOwned<'_>::write_to_async_owned(self.#field_name, __translated_path).await?;
208        }
209    };
210
211    #[cfg(feature = "resolve-path")]
212    let has_field_impl = if self_path {
213        quote! {}
214    } else {
215        use std::iter;
216
217        use crate::resolve_path::MAX_LEN;
218
219        let field_name_str = field_name.to_string();
220        if field_name_str.len() > MAX_LEN {
221            return Err(syn::Error::new_spanned(
222                field_name,
223                format!(
224                    "Field name for DirStructure must be at most {} characters long",
225                    MAX_LEN
226                ),
227            ));
228        }
229        let field_name_array: [char; MAX_LEN] = field_name_str
230            .chars()
231            .chain(iter::repeat('\0'))
232            .take(MAX_LEN)
233            .collect::<Vec<_>>()
234            .try_into()
235            .unwrap();
236
237        quote! {
238            impl #impl_generics ::dir_structure::HasField<{ [#(#field_name_array),*] }> for #ty_name #ty_generics #where_clause {
239                type Inner = #field_ty;
240
241                fn resolve_path(mut #path_param_name: ::std::path::PathBuf) -> ::std::path::PathBuf {
242                    #path_pusher_for_has_field
243                    #path_param_name
244                }
245            }
246        }
247    };
248    Ok(DirStructureForField {
249        read_code: quote! {
250            #field_name: #read_code
251        },
252        async_read_code: quote! {
253            #field_name: #async_read_code
254        },
255        async_read_bound: if self_path {
256            None
257        } else {
258            Some(parse_quote! {
259                for<'___trivial_bound> #actual_field_ty_perform: ::dir_structure::ReadFromAsync
260            })
261        },
262        write_code,
263        async_write_code,
264        async_write_bound: if self_path {
265            None
266        } else {
267            Some(parse_quote! {
268                for<'___trivial_bound> #actual_field_ty_perform: ::dir_structure::WriteToAsync
269            })
270        },
271        async_write_owned_code,
272        async_write_owned_bound: if self_path {
273            None
274        } else {
275            Some(parse_quote! {
276                for<'___trivial_bound> #actual_field_ty_perform: ::dir_structure::WriteToAsyncOwned<'_>
277            })
278        },
279        #[cfg(feature = "resolve-path")]
280        has_field_impl,
281    })
282}
283
284fn expand_dir_structure(st: ItemStruct) -> syn::Result<TokenStream> {
285    let name = &st.ident;
286    let path_param_name = format_ident!("__dir_structure_path");
287    let (impl_generics, ty_generics, where_clause) = st.generics.split_for_impl();
288
289    let mut field_read_impls = Vec::new();
290    let mut field_async_read_impls = Vec::new();
291    let mut field_async_read_bounds = Vec::new();
292    let mut field_write_impls = Vec::new();
293    let mut field_async_write_impls = Vec::new();
294    let mut field_async_write_bounds = Vec::new();
295    let mut field_async_write_owned_impls = Vec::new();
296    let mut field_async_write_owned_bounds = Vec::new();
297    #[cfg(feature = "resolve-path")]
298    let mut has_field_impls = Vec::new();
299
300    for field in &st.fields {
301        let DirStructureForField {
302            read_code,
303            async_read_code,
304            async_read_bound,
305            write_code,
306            async_write_code,
307            async_write_bound,
308            async_write_owned_code,
309            async_write_owned_bound,
310            #[cfg(feature = "resolve-path")]
311            has_field_impl,
312        } = expand_dir_structure_for_field(
313            (&impl_generics, name, &ty_generics, where_clause),
314            &path_param_name,
315            field,
316        )?;
317        field_read_impls.push(read_code);
318        field_async_read_impls.push(async_read_code);
319        field_async_read_bounds.extend(async_read_bound);
320        field_write_impls.push(write_code);
321        field_async_write_impls.push(async_write_code);
322        field_async_write_bounds.extend(async_write_bound);
323        field_async_write_owned_impls.push(async_write_owned_code);
324        field_async_write_owned_bounds.push(async_write_owned_bound);
325        #[cfg(feature = "resolve-path")]
326        has_field_impls.push(has_field_impl);
327    }
328
329    #[cfg_attr(
330        all(not(feature = "async"), not(feature = "resolve-path")),
331        expect(unused_mut)
332    )]
333    let mut expanded = quote! {
334        impl #impl_generics ::dir_structure::ReadFrom for #name #ty_generics #where_clause {
335            fn read_from(#path_param_name: &::std::path::Path) -> ::dir_structure::Result<Self>
336            where
337                Self: Sized,
338            {
339                Ok(Self {
340                    #(#field_read_impls,)*
341                })
342            }
343        }
344        impl #impl_generics ::dir_structure::WriteTo for #name #ty_generics #where_clause {
345            fn write_to(&self, #path_param_name: &::std::path::Path) -> ::dir_structure::Result<()> {
346                #(#field_write_impls)*
347                Ok(())
348            }
349        }
350        impl #impl_generics ::dir_structure::DirStructure for #name #ty_generics #where_clause {}
351    };
352
353    #[cfg(feature = "async")]
354    {
355        use syn::punctuated::Punctuated;
356
357        fn merge_where_clause(
358            where_clause: Option<syn::WhereClause>,
359            additional_bounds: Vec<syn::WherePredicate>,
360        ) -> Option<syn::WhereClause> {
361            if let Some(mut where_clause) = where_clause {
362                where_clause.predicates.extend(additional_bounds);
363                Some(where_clause)
364            } else {
365                let mut where_clause = syn::WhereClause {
366                    where_token: <Token![where]>::default(),
367                    predicates: Punctuated::new(),
368                };
369                where_clause.predicates.extend(additional_bounds);
370                if where_clause.predicates.is_empty() {
371                    None
372                } else {
373                    Some(where_clause)
374                }
375            }
376        }
377
378        let where_clause_read_from_async =
379            merge_where_clause(where_clause.cloned(), field_async_read_bounds);
380        let where_clause_write_to_async =
381            merge_where_clause(where_clause.cloned(), field_async_write_bounds);
382        expanded.extend(quote! {
383            impl #impl_generics ::dir_structure::ReadFromAsync for #name #ty_generics #where_clause_read_from_async {
384                type Future = ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::dir_structure::Result<Self>> + ::std::marker::Send + 'static>>;
385
386                fn read_from_async(#path_param_name: ::std::path::PathBuf) -> Self::Future
387                where
388                    Self: Sized,
389                {
390                    Box::pin(async move {
391                        Ok(Self {
392                            #(#field_async_read_impls,)*
393                        })
394                    })
395                }
396            }
397            impl #impl_generics ::dir_structure::WriteToAsync for #name #ty_generics #where_clause_write_to_async {
398                type Future<'a> = ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::dir_structure::Result<()>> + ::std::marker::Send + 'a>>
399                where
400                    Self: 'a;
401
402                fn write_to_async(&self, #path_param_name: ::std::path::PathBuf) -> Self::Future<'_> {
403                    Box::pin(async move {
404                        #(#field_async_write_impls)*
405                        Ok(())
406                    })
407                }
408            }
409        });
410    }
411
412    #[cfg(feature = "resolve-path")]
413    {
414        for has_field_impl in has_field_impls {
415            expanded.extend(has_field_impl);
416        }
417    }
418
419    Ok(expanded)
420}
421
422#[cfg(feature = "resolve-path")]
423mod resolve_path;
424
425#[cfg(feature = "resolve-path")]
426#[proc_macro]
427pub fn resolve_path(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
428    resolve_path::resolve_path(input)
429}
430
431#[cfg(feature = "resolve-path")]
432#[proc_macro]
433pub fn __resolve_max_len(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
434    // This macro is used to get the maximum length of a field name for the `HasField` trait.
435    // It is used in the `resolve_path` macro to ensure that field names do not exceed this length.
436    let max_len = resolve_path::MAX_LEN;
437    let output = quote! { #max_len };
438    output.into()
439}