use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, GenericParam, Type};
struct TucoType {
original: Type,
tuple: Type,
}
enum FieldTypeInfo {
Tuco(TucoType),
Standard(Type),
}
struct StructFieldEntry {
single: bool,
index: usize,
ident: Option<syn::Ident>,
t: FieldTypeInfo,
}
impl StructFieldEntry {
fn get_original_type(&self) -> proc_macro2::TokenStream {
match &self.t {
FieldTypeInfo::Tuco(tuco) => tuco.original.to_token_stream(),
FieldTypeInfo::Standard(x) => x.to_token_stream(),
}
}
fn get_tuple_type(&self) -> proc_macro2::TokenStream {
match &self.t {
FieldTypeInfo::Tuco(tuco) => tuco.tuple.to_token_stream(),
FieldTypeInfo::Standard(x) => x.to_token_stream(),
}
}
fn get_convert_to_tuple_row(&self) -> proc_macro2::TokenStream {
match &self.ident {
Some(ident) => match &self.t {
FieldTypeInfo::Tuco(_) => {
quote! { self.#ident.into_tuple() }
}
FieldTypeInfo::Standard(_) => {
quote! { self.#ident }
}
},
None => {
let index = syn::Index::from(self.index);
match &self.t {
FieldTypeInfo::Tuco(_) => {
quote! { self.#index.into_tuple() }
}
FieldTypeInfo::Standard(_) => {
quote! { self.#index }
}
}
}
}
}
fn get_convert_from_tuple_row(&self) -> proc_macro2::TokenStream {
let index = syn::Index::from(self.index);
let type_token = self.get_original_type();
let input_select = if self.single {
quote! { tuple }
} else {
quote! { tuple.#index }
};
match &self.ident {
Some(ident) => match &self.t {
FieldTypeInfo::Tuco(_) => {
quote! { #ident : <#type_token as Tuco>::from_tuple(#input_select) }
}
FieldTypeInfo::Standard(_) => {
quote! { #ident : #input_select }
}
},
None => match &self.t {
FieldTypeInfo::Tuco(_) => {
quote! { <#type_token as Tuco>::from_tuple(#input_select) }
}
FieldTypeInfo::Standard(_) => {
quote! { #input_select }
}
},
}
}
}
fn has_tuco_meta_tag(field: &Field) -> bool {
field.attrs.iter().any(|x| x.path().is_ident("tuco"))
}
fn parse_struct_field(
index: usize,
field: &Field,
single: bool,
) -> Result<StructFieldEntry, String> {
if has_tuco_meta_tag(field) {
let type_as_string = field.ty.to_token_stream().to_string().replace(" ", "");
let tuple_type = format!("<{} as Tuco>::Tuple", type_as_string);
let parsed_type: syn::Type = match syn::parse_str(&tuple_type) {
Ok(t) => t,
_ => return Err("Failed to parse type".to_string()),
};
Ok(StructFieldEntry {
single,
index,
ident: field.ident.clone(),
t: FieldTypeInfo::Tuco(TucoType {
original: field.ty.clone(),
tuple: parsed_type,
}),
})
} else {
Ok(StructFieldEntry {
single,
index,
ident: field.ident.clone(),
t: FieldTypeInfo::Standard(field.ty.clone()),
})
}
}
#[proc_macro_attribute]
pub fn tuco(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
#[proc_macro_derive(Tuco, attributes(tuco))]
pub fn derive_tuco(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = input.ident;
let generics = input.generics;
let where_clause: &Option<syn::WhereClause> = &generics.where_clause;
let generic_params: Vec<_> = generics
.params
.iter()
.map(|param| match param {
GenericParam::Type(type_param) => quote! { #type_param },
GenericParam::Lifetime(lifetime) => quote! { #lifetime },
GenericParam::Const(const_param) => quote! { #const_param },
})
.collect();
let impl_statement = quote! { impl<#(#generic_params),*> Tuco for #struct_name<#(#generic_params),*> #where_clause };
let Data::Struct(data_struct) = &input.data else {
panic!("Tuco can only be used with structs.");
};
let single = data_struct.fields.len() == 1;
let fields: Vec<StructFieldEntry> = match &data_struct.fields {
Fields::Named(fields_named) => fields_named
.named
.iter()
.enumerate()
.map(|(i, f)| parse_struct_field(i, f, single).unwrap())
.collect(),
Fields::Unnamed(fields) => fields
.unnamed
.iter()
.enumerate()
.map(|(i, f)| parse_struct_field(i, f, single).unwrap())
.collect(),
Fields::Unit => Vec::new(),
};
let tuple_types: Vec<_> = fields
.iter()
.map(StructFieldEntry::get_tuple_type)
.collect();
let from_tuple_rows: Vec<_> = fields
.iter()
.map(StructFieldEntry::get_convert_from_tuple_row)
.collect();
let from_tuple_impl = match &data_struct.fields {
Fields::Named(_) => match fields.len() {
0 => quote! {fn from_tuple(tuple: Self::Tuple) -> Self { #struct_name{} }},
1 => {
let first = from_tuple_rows.first().unwrap();
quote! {fn from_tuple(tuple: Self::Tuple) -> Self { #struct_name{#first} }}
}
_ => {
quote! {fn from_tuple(tuple: Self::Tuple) -> Self { #struct_name{#(#from_tuple_rows),*} }}
}
},
Fields::Unnamed(_) => match fields.len() {
0 => quote! {fn from_tuple(tuple: Self::Tuple) -> Self { #struct_name() }},
1 => {
let first = from_tuple_rows.first().unwrap();
quote! {fn from_tuple(tuple: Self::Tuple) -> Self { #struct_name(#first) }}
}
_ => {
quote! {fn from_tuple(tuple: Self::Tuple) -> Self { #struct_name(#(#from_tuple_rows),*) }}
}
},
Fields::Unit => {
quote! {fn from_tuple(tuple: Self::Tuple) -> Self {#struct_name} }
}
};
let into_tuple_rows: Vec<_> = fields
.iter()
.map(StructFieldEntry::get_convert_to_tuple_row)
.collect();
let into_tuple_impl = match &data_struct.fields {
Fields::Unnamed(_) | Fields::Named(_) => match fields.len() {
0 => quote! {fn into_tuple(self) -> Self::Tuple { () }},
1 => {
let first = into_tuple_rows.first().unwrap();
quote! {fn into_tuple(self) -> Self::Tuple { #first }}
}
_ => quote! {fn into_tuple(self) -> Self::Tuple { (#(#into_tuple_rows),*,) }},
},
Fields::Unit => {
quote! {fn into_tuple(self) -> Self::Tuple { () }}
}
};
let associated_type_assign = match &data_struct.fields {
Fields::Unnamed(_) | Fields::Named(_) => match fields.len() {
0 => quote! {type Tuple = ();},
1 => {
let first = tuple_types.first().unwrap();
quote! {type Tuple = #first;}
}
_ => quote! {type Tuple = (#(#tuple_types),*,);},
},
Fields::Unit => {
quote! {type Tuple = ();}
}
};
let decompose_impl = quote! {
#[automatically_derived]
#impl_statement {
#associated_type_assign
#into_tuple_impl
#from_tuple_impl
fn from_tuco<TFromTuco: Tuco<Tuple = Self::Tuple>>(value: TFromTuco) -> Self {
Self::from_tuple(value.into_tuple())
}
}
};
decompose_impl.into()
}