sub_struct/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use proc_macro2::Span;
4use quote::quote;
5use syn::{
6    parse_macro_input, punctuated::Punctuated, spanned::Spanned, Data, DataStruct, DeriveInput,
7    Field, Fields, FieldsNamed, Ident, MetaNameValue, Token,
8};
9
10/// Enables the generaation of structs that are a subset of the given struct.
11/// #[sub_struct(name = "CreateCustomerParams", columns = ["id", "live_mode", "created", "updated", "delinquent"])]
12#[proc_macro_attribute]
13pub fn sub_struct(args: TokenStream, input: TokenStream) -> TokenStream {
14    let ast = parse_macro_input!(input as DeriveInput);
15
16    let parsed_args =
17        parse_macro_input!(args with Punctuated::<MetaNameValue, syn::Token![,]>::parse_terminated);
18
19    let meta_list = match parse_sub_struct_args(&parsed_args) {
20        Ok(attrs) => attrs,
21        Err(e) => return e.to_compile_error().into(),
22    };
23
24    let new_struct = match ast.data {
25        syn::Data::Struct(ref ds) => {
26            match &ds.fields {
27                Fields::Named(named_fields) => {
28                    let mut unrecognized_fields = vec![];
29
30                    // Validates that the provided fields actually exist.
31                    match &meta_list.strategy {
32                        Strategy::Remove(fs) => {
33                            // Validate that the fields provided by the user actually exist on the ident
34                            for field in fs {
35                                if !named_fields
36                                    .named
37                                    .iter()
38                                    .any(|f| f.ident.as_ref().unwrap().to_string() == *field)
39                                {
40                                    unrecognized_fields.push(field.clone());
41                                }
42                            }
43                            // If one of the given fields to remove is not recognized, return an error
44                            if !unrecognized_fields.is_empty() {
45                                let invalid_fields_str = unrecognized_fields.join(", ");
46
47                                return ::syn::Error::new_spanned(
48                                    &ast.ident,
49                                    format!(
50                                        "Invalid field(s) specified in the remove attribute: {}",
51                                        invalid_fields_str
52                                    ),
53                                )
54                                .to_compile_error()
55                                .into();
56                            }
57                        }
58                        Strategy::Retain(fs) => {
59                            // Validate that the fields provided by the user actually exist on the ident
60                            for field in fs {
61                                if !named_fields
62                                    .named
63                                    .iter()
64                                    .any(|f| f.ident.as_ref().unwrap().to_string() == *field)
65                                {
66                                    unrecognized_fields.push(field.clone());
67                                }
68                            }
69                            // If one of the given fields to remove is not recognized, return an error
70                            if !unrecognized_fields.is_empty() {
71                                let invalid_fields_str = unrecognized_fields.join(", ");
72
73                                return ::syn::Error::new_spanned(
74                                    &ast.ident,
75                                    format!(
76                                        "Invalid field(s) specified in the retain attribute: {}",
77                                        invalid_fields_str
78                                    ),
79                                )
80                                .to_compile_error()
81                                .into();
82                            }
83                        }
84                    };
85
86                    // Generate a new struct
87
88                    let new_fields: FieldsNamed = {
89                        let fields: Punctuated<Field, Token![,]> = match &meta_list.strategy {
90                            Strategy::Remove(fs) => named_fields
91                                .named
92                                .iter()
93                                .filter(|f| !fs.contains(&f.ident.as_ref().unwrap().to_string()))
94                                .cloned()
95                                .collect(),
96                            Strategy::Retain(fs) => named_fields
97                                .named
98                                .iter()
99                                .filter(|f| fs.contains(&f.ident.as_ref().unwrap().to_string()))
100                                .cloned()
101                                .collect(),
102                        };
103
104                        FieldsNamed {
105                            brace_token: syn::token::Brace::default(),
106                            named: fields,
107                        }
108                    };
109
110                    let new = Data::Struct(DataStruct {
111                        struct_token: ds.struct_token.clone(),
112                        fields: Fields::Named(new_fields),
113                        semi_token: ds.semi_token.clone(),
114                    });
115
116                    let new_struct = DeriveInput {
117                        data: new,
118                        attrs: ast.attrs.clone(),
119                        vis: ast.vis.clone(),
120                        ident: Ident::new(&meta_list.name, Span::call_site()),
121                        generics: ast.generics.clone(),
122                    };
123
124                    new_struct
125                }
126                _ => {
127                    return ::syn::Error::new_spanned(
128                        &ast.ident,
129                        "sub_struct only supports structs with named fields",
130                    )
131                    .to_compile_error()
132                    .into()
133                }
134            }
135        }
136        _ => {
137            return ::syn::Error::new_spanned(
138                &ast.ident,
139                "sub_struct only supports structs with named fields",
140            )
141            .to_compile_error()
142            .into()
143        }
144    };
145
146    let final_output = quote! {
147        #ast
148        #new_struct
149    };
150
151    final_output.into()
152}
153
154// When generating a new struct from an existing struct, one can choose the fields to remove or fields to retain.
155// These options are mutually exclusive.
156struct SubStructAttributes {
157    name: String,
158    strategy: Strategy,
159}
160
161enum Strategy {
162    Remove(Vec<String>),
163    Retain(Vec<String>),
164}
165
166// Parse the Punctuated<MetaNameValue, Comma> into something usable.
167fn parse_sub_struct_args(
168    args: &syn::punctuated::Punctuated<MetaNameValue, syn::token::Comma>,
169) -> Result<SubStructAttributes, syn::Error> {
170    let mut name = String::new();
171    let mut remove = vec![];
172    let mut retain = vec![];
173
174    for arg in args {
175        if arg.path.is_ident("name") {
176            match &arg.value {
177                syn::Expr::Lit(v) => match &v.lit {
178                    syn::Lit::Str(n) => {
179                        name = n.value();
180                    }
181                    _ => {
182                        return Err(syn::Error::new(
183                            arg.span(),
184                            "The name attribute only accepts strings as valid inputs",
185                        ))
186                    }
187                },
188                _ => {
189                    return Err(syn::Error::new(
190                        arg.span(),
191                        "Could not parse as a valid value for the name attribute",
192                    ))
193                }
194            }
195        } else if arg.path.is_ident("remove") {
196            match &arg.value {
197                syn::Expr::Array(v) => {
198                    for e in v.elems.iter() {
199                        match e {
200                            syn::Expr::Lit(v) => match &v.lit {
201                                syn::Lit::Str(n) => {
202                                    remove.push(n.value())
203                                },
204                                _ => return Err(syn::Error::new(
205                                    arg.span(),
206                                    "Could not parse as a valid string. The name attribute only accepts strings as valid inputs",
207                                )),
208                            },
209                            _ => return Err( syn::Error::new(
210                                arg.span(),
211                                "The remove attribute only accepts an array of strings as valid inputs",
212                            )),
213                        }
214                    }
215                }
216                _ => {
217                    return Err(syn::Error::new(
218                        arg.span(),
219                        "The remove attribute only accepts an array strings as valid inputs",
220                    ))
221                }
222            };
223        } else if arg.path.is_ident("retain") {
224            match &arg.value {
225                syn::Expr::Array(v) => {
226                    for e in v.elems.iter() {
227                        match e {
228                            syn::Expr::Lit(v) => match &v.lit {
229                                syn::Lit::Str(n) => {
230                                    retain.push(n.value())
231                                },
232                                _ => return Err(syn::Error::new(
233                                    arg.span(),
234                                    "Could not parse as a valid string. The name attribute only accepts strings as valid inputs",
235                                )),
236                            },
237                            _ => return Err( syn::Error::new(
238                                arg.span(),
239                                "The remove attribute only accepts an array of strings as valid inputs",
240                            )),
241                        }
242                    }
243                }
244                _ => {
245                    return Err(syn::Error::new(
246                        arg.span(),
247                        "The remove attribute only accepts an array strings as valid inputs",
248                    ))
249                }
250            };
251        } else {
252            return Err(syn::Error::new(
253                proc_macro::Span::call_site().into(),
254                format!(
255                "{} is not a valid attribute for sub_struct. Valid attributes are name and remove",
256                arg.path.get_ident().unwrap().to_string()
257            ),
258            ));
259        };
260    }
261
262    match name.is_empty() {
263        false => match remove.is_empty() {
264            // Under this branch, remove has been specified, so inclusion of retain is invalid
265            false => match retain.is_empty() {
266                false => Err(syn::Error::new(
267                    proc_macro::Span::call_site().into(),
268                    "Only one of remove or retain attributes can be used at a time",
269                )),
270                true => Ok(SubStructAttributes {
271                    name,
272                    strategy: Strategy::Remove(remove),
273                }),
274            },
275            // If remove is_empty, then retain should be provided.
276            true => match retain.is_empty() {
277                true => Err(syn::Error::new(
278                    proc_macro::Span::call_site().into(),
279                    "Only one of remove or retain attributes can be used at a time",
280                )),
281                false => Ok(SubStructAttributes {
282                    name,
283                    strategy: Strategy::Retain(retain),
284                }),
285            },
286        },
287        true => Err(syn::Error::new(
288            proc_macro::Span::call_site().into(),
289            "The `name` attribute is required",
290        )),
291    }
292}