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#[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 match &meta_list.strategy {
32 Strategy::Remove(fs) => {
33 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 !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 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 !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 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
154struct SubStructAttributes {
157 name: String,
158 strategy: Strategy,
159}
160
161enum Strategy {
162 Remove(Vec<String>),
163 Retain(Vec<String>),
164}
165
166fn 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 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 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}