use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{ToTokens, quote};
use syn::{Data, DeriveInput, Error, Fields, Ident, LitStr, parse_macro_input, spanned::Spanned};
#[cfg(test)]
mod tests;
#[proc_macro_derive(StructToArray, attributes(struct_to_array))]
pub fn derive_struct_to_array(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_struct_to_array(&input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn expand_struct_to_array(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let data_struct = match &input.data {
Data::Struct(s) => s,
_ => {
return Err(Error::new_spanned(
name,
"StructToArray can only be derived for structs",
));
}
};
let trait_crate = resolve_trait_crate_path(&input.attrs)?;
let trait_path = quote!(#trait_crate::StructToArray);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
match &data_struct.fields {
Fields::Named(fields_named) => {
let fields: Vec<_> = fields_named.named.iter().collect();
if fields.is_empty() {
return Err(Error::new(
name.span(),
"StructToArray requires at least one field",
));
}
let item_ty = &fields[0].ty;
let item_ty_str = item_ty.to_token_stream().to_string();
for f in fields.iter().skip(1) {
let f_ty_str = f.ty.to_token_stream().to_string();
if f_ty_str != item_ty_str {
return Err(Error::new(
f.ty.span(),
format!(
"StructToArray requires all fields to have identical type tokens; expected `{}`, found `{}`",
item_ty_str, f_ty_str
),
));
}
}
let field_idents: Vec<Ident> = fields
.iter()
.map(|f| f.ident.clone().expect("named field must have ident"))
.collect();
let n = field_idents.len();
let n_lit = syn::LitInt::new(&n.to_string(), Span::call_site());
let to_elems = field_idents.iter().map(|id| quote!(self.#id));
let expanded = quote! {
impl #impl_generics #trait_path<#item_ty> for #name #ty_generics #where_clause {
type Arr = [#item_ty; #n_lit];
#[inline]
fn to_arr(self) -> Self::Arr {
[#(#to_elems),*]
}
#[inline]
fn from_arr(arr: Self::Arr) -> Self {
let [#(#field_idents),*] = arr;
Self { #(#field_idents),* }
}
}
};
Ok(expanded)
}
Fields::Unnamed(fields_unnamed) => {
let fields: Vec<_> = fields_unnamed.unnamed.iter().collect();
if fields.is_empty() {
return Err(Error::new(
name.span(),
"StructToArray requires at least one field",
));
}
let item_ty = &fields[0].ty;
let item_ty_str = item_ty.to_token_stream().to_string();
for f in fields.iter().skip(1) {
let f_ty_str = f.ty.to_token_stream().to_string();
if f_ty_str != item_ty_str {
return Err(Error::new(
f.ty.span(),
format!(
"StructToArray requires all fields to have identical type tokens; expected `{}`, found `{}`",
item_ty_str, f_ty_str
),
));
}
}
let n = fields.len();
let n_lit = syn::LitInt::new(&n.to_string(), Span::call_site());
let idxs: Vec<syn::Index> = (0..n).map(syn::Index::from).collect();
let to_elems = idxs.iter().map(|i| quote!(self.#i));
let binds: Vec<Ident> = (0..n)
.map(|i| Ident::new(&format!("__v{}", i), Span::call_site()))
.collect();
let expanded = quote! {
impl #impl_generics #trait_path<#item_ty> for #name #ty_generics #where_clause {
type Arr = [#item_ty; #n_lit];
#[inline]
fn to_arr(self) -> Self::Arr {
[#(#to_elems),*]
}
#[inline]
fn from_arr(arr: Self::Arr) -> Self {
let [#(#binds),*] = arr;
Self(#(#binds),*)
}
}
};
Ok(expanded)
}
Fields::Unit => Err(Error::new_spanned(
name,
"StructToArray cannot be derived for unit structs",
)),
}
}
fn resolve_trait_crate_path(attrs: &[syn::Attribute]) -> syn::Result<proc_macro2::TokenStream> {
for attr in attrs {
if !attr.path().is_ident("struct_to_array") {
continue;
}
let mut override_name: Option<LitStr> = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("crate") {
let lit: LitStr = meta.value()?.parse()?;
override_name = Some(lit);
Ok(())
} else {
Err(meta.error("supported: #[struct_to_array(crate = \"...\")]"))
}
})?;
if let Some(lit) = override_name {
let s = lit.value();
if s == "crate" {
return Ok(quote!(crate));
}
let ident = Ident::new(&s, lit.span());
return Ok(quote!(::#ident));
}
}
match proc_macro_crate::crate_name("struct_to_array") {
Ok(proc_macro_crate::FoundCrate::Itself) => Ok(quote!(crate)),
Ok(proc_macro_crate::FoundCrate::Name(name)) => {
let ident = Ident::new(&name, Span::call_site());
Ok(quote!(::#ident))
}
Err(_) => Ok(quote!(::struct_to_array)), }
}