dir_structure_macros/
lib.rs

1use proc_macro2::{Ident, TokenStream};
2use quote::{format_ident, quote};
3use syn::{Field, ItemStruct, Token, Type};
4
5#[proc_macro_derive(DirStructure, attributes(dir_structure))]
6pub fn derive_dir_structure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
7    let item = syn::parse_macro_input!(item as ItemStruct);
8
9    expand_dir_structure(item)
10        .unwrap_or_else(|err| err.to_compile_error())
11        .into()
12}
13
14struct DirStructureForField {
15    read_code: TokenStream,
16    write_code: TokenStream,
17}
18
19fn expand_dir_structure_for_field(
20    path_param_name: &Ident,
21    field: &Field,
22) -> syn::Result<DirStructureForField> {
23    let field_name = field.ident.as_ref().ok_or_else(|| {
24        syn::Error::new_spanned(
25            field,
26            "DirStructure can only be derived for structs with named fields",
27        )
28    })?;
29
30    let field_ty = &field.ty;
31
32    enum PathData {
33        SelfPath,
34        Path(String),
35        None,
36    }
37
38    let mut path = PathData::None;
39    let mut self_path = field_name == "self_path";
40    let mut with_newtype = None::<Type>;
41
42    for attr in field
43        .attrs
44        .iter()
45        .filter(|attr| attr.meta.path().is_ident("dir_structure"))
46    {
47        attr.parse_nested_meta(|meta| {
48            if meta.path.is_ident("path") {
49                let _eq = meta.input.parse::<Token![=]>()?;
50                if meta.input.peek(syn::LitStr) {
51                    let s = meta.input.parse::<syn::LitStr>()?;
52                    path = PathData::Path(s.value());
53                } else if meta.input.peek(Token![self]) {
54                    let _self = meta.input.parse::<Token![self]>()?;
55                    path = PathData::SelfPath;
56                } else {
57                    return Err(syn::Error::new_spanned(
58                        meta.path,
59                        "Expected a string literal or `self`",
60                    ));
61                }
62            } else if meta.path.is_ident("self_path") {
63                self_path = true;
64            } else if meta.path.is_ident("with_newtype") {
65                let _eq = meta.input.parse::<Token![=]>()?;
66                let ty = meta.input.parse::<Type>()?;
67                with_newtype = Some(ty);
68            } else {
69                return Err(syn::Error::new_spanned(
70                    meta.path,
71                    "Unknown attribute for dir_structure",
72                ));
73            }
74
75            Ok(())
76        })?;
77    }
78
79    let actual_path_expr = match path {
80        PathData::Path(p) => quote! {#path_param_name.join(#p)},
81        PathData::SelfPath => quote! { #path_param_name },
82        PathData::None => {
83            let name = field_name.to_string();
84            quote! {#path_param_name.join(#name)}
85        }
86    };
87    let actual_field_ty_perform = with_newtype.as_ref().unwrap_or(field_ty);
88    let read_code = if self_path {
89        quote! {
90            #field_ty::from(#path_param_name)
91        }
92    } else {
93        let value_name = format_ident!("__value");
94        let end_expr = match &with_newtype {
95            Some(nt) => quote! {
96                <#nt as ::dir_structure::NewtypeToInner>::into_inner(#value_name)
97            },
98            None => quote! {
99                #value_name
100            },
101        };
102
103        quote! {{
104            let __translated__path = #actual_path_expr;
105            let #value_name = <#actual_field_ty_perform as ::dir_structure::ReadFrom>::read_from(&__translated__path)?;
106            #end_expr
107        }}
108    };
109
110    let write_code = if self_path {
111        quote! {}
112    } else {
113        let writer = match &with_newtype {
114            Some(nt) => {
115                quote! { &<#nt as ::dir_structure::FromRefForWriter<'_>>::from_ref_for_writer(&self.#field_name) }
116            }
117            None => quote! { &self.#field_name },
118        };
119        quote! {
120            let __translated_path = #actual_path_expr;
121            ::dir_structure::WriteTo::write_to(#writer, &__translated_path)?;
122        }
123    };
124
125    Ok(DirStructureForField {
126        read_code: quote! {
127            #field_name: #read_code
128        },
129        write_code,
130    })
131}
132
133fn expand_dir_structure(st: ItemStruct) -> syn::Result<TokenStream> {
134    let name = &st.ident;
135    let path_param_name = format_ident!("__DIR_STRUCTURE_PATH");
136    let (impl_generics, ty_generics, where_clause) = st.generics.split_for_impl();
137
138    let mut field_read_impls = Vec::new();
139    let mut field_write_impls = Vec::new();
140
141    for field in &st.fields {
142        let DirStructureForField {
143            read_code,
144            write_code,
145        } = expand_dir_structure_for_field(&path_param_name, field)?;
146        field_read_impls.push(read_code);
147        field_write_impls.push(write_code);
148    }
149
150    let expanded = quote! {
151        impl #impl_generics ::dir_structure::ReadFrom for #name #ty_generics #where_clause {
152            fn read_from(#path_param_name: &std::path::Path) -> ::dir_structure::Result<Self>
153            where
154                Self: Sized,
155            {
156                Ok(Self {
157                    #(#field_read_impls,)*
158                })
159            }
160        }
161        impl #impl_generics ::dir_structure::WriteTo for #name #ty_generics #where_clause {
162            fn write_to(&self, #path_param_name: &std::path::Path) -> ::dir_structure::Result<()> {
163                #(#field_write_impls)*
164                Ok(())
165            }
166        }
167        impl #impl_generics ::dir_structure::DirStructure for #name #ty_generics #where_clause {}
168    };
169
170    Ok(expanded)
171}