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