struct_to_array_derive/
lib.rs

1use 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/// Derive `struct_to_array::StructToArray<Item>` for a struct whose fields are all the same
10/// type (token-identical).
11///
12/// Supported:
13/// - named-field structs: `struct Foo { a: T, b: T }`
14/// - tuple structs: `struct Foo(T, T)`
15///
16/// Not supported:
17/// - unit structs: `struct Foo;`
18/// - structs with zero fields (no way to infer `Item`)
19///
20/// Optional attribute:
21/// - `#[struct_to_array(crate = "path_ident")]`
22///   Use if you renamed the `struct_to_array` dependency in Cargo.toml.
23///   Example: `struct_to_array = { package = "struct_to_array", version = "...", package = "...", default-features = false }`
24///   (you usually won’t need this; `proc-macro-crate` handles common renames)
25#[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            // Bindings for destructuring the array: let [v0, v1, ...] = arr;
140            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    // 1) Optional explicit override: #[struct_to_array(crate = "foo")]
173    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    // 2) Otherwise resolve via proc-macro-crate (handles dependency renames)
200    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)), // fallback
207    }
208}