struct_to_array_derive/
lib.rs1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::{ToTokens, quote};
4use syn::{Data, DeriveInput, Error, Fields, Ident, LitStr, parse_macro_input, spanned::Spanned};
5
6#[cfg(test)]
7mod tests;
8
9#[proc_macro_derive(StructToArray, attributes(struct_to_array))]
26pub fn derive_struct_to_array(input: TokenStream) -> TokenStream {
27 let input = parse_macro_input!(input as DeriveInput);
28 match expand_struct_to_array(&input) {
29 Ok(ts) => ts.into(),
30 Err(e) => e.to_compile_error().into(),
31 }
32}
33
34fn expand_struct_to_array(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
35 let name = &input.ident;
36
37 let data_struct = match &input.data {
38 Data::Struct(s) => s,
39 _ => {
40 return Err(Error::new_spanned(
41 name,
42 "StructToArray can only be derived for structs",
43 ));
44 }
45 };
46
47 let trait_crate = resolve_trait_crate_path(&input.attrs)?;
48 let trait_path = quote!(#trait_crate::StructToArray);
49
50 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
51
52 match &data_struct.fields {
53 Fields::Named(fields_named) => {
54 let fields: Vec<_> = fields_named.named.iter().collect();
55 if fields.is_empty() {
56 return Err(Error::new(
57 name.span(),
58 "StructToArray requires at least one field",
59 ));
60 }
61
62 let item_ty = &fields[0].ty;
63 let item_ty_str = item_ty.to_token_stream().to_string();
64
65 for f in fields.iter().skip(1) {
66 let f_ty_str = f.ty.to_token_stream().to_string();
67 if f_ty_str != item_ty_str {
68 return Err(Error::new(
69 f.ty.span(),
70 format!(
71 "StructToArray requires all fields to have identical type tokens; expected `{}`, found `{}`",
72 item_ty_str, f_ty_str
73 ),
74 ));
75 }
76 }
77
78 let field_idents: Vec<Ident> = fields
79 .iter()
80 .map(|f| f.ident.clone().expect("named field must have ident"))
81 .collect();
82
83 let n = field_idents.len();
84 let n_lit = syn::LitInt::new(&n.to_string(), Span::call_site());
85
86 let to_elems = field_idents.iter().map(|id| quote!(self.#id));
87
88 let expanded = quote! {
89 impl #impl_generics #trait_path<#item_ty> for #name #ty_generics #where_clause {
90 type Arr = [#item_ty; #n_lit];
91
92 #[inline]
93 fn to_arr(self) -> Self::Arr {
94 [#(#to_elems),*]
95 }
96
97 #[inline]
98 fn from_arr(arr: Self::Arr) -> Self {
99 let [#(#field_idents),*] = arr;
100 Self { #(#field_idents),* }
101 }
102 }
103 };
104
105 Ok(expanded)
106 }
107
108 Fields::Unnamed(fields_unnamed) => {
109 let fields: Vec<_> = fields_unnamed.unnamed.iter().collect();
110 if fields.is_empty() {
111 return Err(Error::new(
112 name.span(),
113 "StructToArray requires at least one field",
114 ));
115 }
116
117 let item_ty = &fields[0].ty;
118 let item_ty_str = item_ty.to_token_stream().to_string();
119
120 for f in fields.iter().skip(1) {
121 let f_ty_str = f.ty.to_token_stream().to_string();
122 if f_ty_str != item_ty_str {
123 return Err(Error::new(
124 f.ty.span(),
125 format!(
126 "StructToArray requires all fields to have identical type tokens; expected `{}`, found `{}`",
127 item_ty_str, f_ty_str
128 ),
129 ));
130 }
131 }
132
133 let n = fields.len();
134 let n_lit = syn::LitInt::new(&n.to_string(), Span::call_site());
135
136 let idxs: Vec<syn::Index> = (0..n).map(syn::Index::from).collect();
137 let to_elems = idxs.iter().map(|i| quote!(self.#i));
138
139 let binds: Vec<Ident> = (0..n)
141 .map(|i| Ident::new(&format!("__v{}", i), Span::call_site()))
142 .collect();
143
144 let expanded = quote! {
145 impl #impl_generics #trait_path<#item_ty> for #name #ty_generics #where_clause {
146 type Arr = [#item_ty; #n_lit];
147
148 #[inline]
149 fn to_arr(self) -> Self::Arr {
150 [#(#to_elems),*]
151 }
152
153 #[inline]
154 fn from_arr(arr: Self::Arr) -> Self {
155 let [#(#binds),*] = arr;
156 Self(#(#binds),*)
157 }
158 }
159 };
160
161 Ok(expanded)
162 }
163
164 Fields::Unit => Err(Error::new_spanned(
165 name,
166 "StructToArray cannot be derived for unit structs",
167 )),
168 }
169}
170
171fn resolve_trait_crate_path(attrs: &[syn::Attribute]) -> syn::Result<proc_macro2::TokenStream> {
172 for attr in attrs {
174 if !attr.path().is_ident("struct_to_array") {
175 continue;
176 }
177
178 let mut override_name: Option<LitStr> = None;
179 attr.parse_nested_meta(|meta| {
180 if meta.path.is_ident("crate") {
181 let lit: LitStr = meta.value()?.parse()?;
182 override_name = Some(lit);
183 Ok(())
184 } else {
185 Err(meta.error("supported: #[struct_to_array(crate = \"...\")]"))
186 }
187 })?;
188
189 if let Some(lit) = override_name {
190 let s = lit.value();
191 if s == "crate" {
192 return Ok(quote!(crate));
193 }
194 let ident = Ident::new(&s, lit.span());
195 return Ok(quote!(::#ident));
196 }
197 }
198
199 match proc_macro_crate::crate_name("struct_to_array") {
201 Ok(proc_macro_crate::FoundCrate::Itself) => Ok(quote!(crate)),
202 Ok(proc_macro_crate::FoundCrate::Name(name)) => {
203 let ident = Ident::new(&name, Span::call_site());
204 Ok(quote!(::#ident))
205 }
206 Err(_) => Ok(quote!(::struct_to_array)), }
208}