use convert_case::Casing; // Copyright (C) 2025 Emilis Bliūdžius // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use quote::{ToTokens, quote}; use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, spanned::Spanned}; #[allow(unused)] pub struct RefAndMut { name: syn::Ident, variants: Vec<(Ident, Fields)>, } impl RefAndMut { pub fn parse(input: syn::DeriveInput) -> syn::Result { let variants = match input.data { syn::Data::Enum(data) => data.variants, syn::Data::Struct(_) | syn::Data::Union(_) => { return Err(syn::Error::new( input.span(), "RefAndMut can only be used on enums", )); } }; Ok(Self { name: input.ident, variants: variants.into_iter().map(|v| (v.ident, v.fields)).collect(), }) } } impl ToTokens for RefAndMut { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { for (field, value) in self.variants.iter().cloned() { match value { Fields::Named(named) => named_fields(self.name.clone(), field, named, tokens), Fields::Unnamed(unnamed) => { unnamed_fields(self.name.clone(), field, unnamed, tokens) } Fields::Unit => continue, } } tokens.extend(quote! {}); } } fn named_fields( struct_name: Ident, name: Ident, fields: FieldsNamed, tokens: &mut proc_macro2::TokenStream, ) { let base_name = name.to_string().to_case(convert_case::Case::Pascal); let name_ref = Ident::new(format!("{base_name}Ref").as_str(), name.span()); let fn_name_ref = Ident::new( format!( "{}_ref", name.to_string().to_case(convert_case::Case::Snake) ) .as_str(), name.span(), ); let fn_name_mut = Ident::new( format!( "{}_mut", name.to_string().to_case(convert_case::Case::Snake) ) .as_str(), name.span(), ); let name_mut = Ident::new(format!("{base_name}Mut").as_str(), name.span()); let struct_fields_ref = fields .named .iter() .map(|c| { let name = c.ident.as_ref().expect("this is a named field??"); let ty = &c.ty; quote! { pub #name: &'a #ty, } }) .collect::>(); let struct_fields_mut = fields .named .iter() .map(|c| { let name = c.ident.as_ref().expect("this is a named field??"); let ty = &c.ty; quote! { pub #name: &'a mut #ty, } }) .collect::>(); let fields_idents = fields .named .iter() .map(|c| c.ident.as_ref().expect("this is a named field??")) .collect::>(); let for_character = (struct_name == "Role").then_some(quote!{ impl crate::character::Character { pub fn #fn_name_ref<'a>(&'a self) -> core::result::Result<#name_ref<'a>, crate::error::GameError> { let got = self.role_title(); self.role().#fn_name_ref().ok_or(crate::error::GameError::InvalidRole { got, expected: RoleTitle::#name, }) } pub fn #fn_name_mut<'a>(&'a mut self) -> core::result::Result<#name_mut<'a>, crate::error::GameError> { let got = self.role_title(); self.role_mut().#fn_name_mut().ok_or(crate::error::GameError::InvalidRole { got, expected: RoleTitle::#name, }) } } }); tokens.extend(quote! { pub struct #name_ref<'a> { #(#struct_fields_ref)* } pub struct #name_mut<'a> { #(#struct_fields_mut)* } #for_character impl #struct_name { pub fn #fn_name_ref<'a>(&'a self) -> Option<#name_ref<'a>> { match self { Self::#name{ #(#fields_idents),* } => Some(#name_ref {#(#fields_idents),* }), _ => None, } } pub fn #fn_name_mut<'a>(&'a mut self) -> Option<#name_mut<'a>> { match self { Self::#name{ #(#fields_idents),* } => Some(#name_mut {#(#fields_idents),* }), _ => None, } } } }); } fn unnamed_fields( struct_name: Ident, name: Ident, fields: FieldsUnnamed, tokens: &mut proc_macro2::TokenStream, ) { let base_name = name.to_string().to_case(convert_case::Case::Pascal); let name_ref = Ident::new(format!("{base_name}Ref").as_str(), name.span()); let fn_name_ref = Ident::new( format!( "{}_ref", name.to_string().to_case(convert_case::Case::Snake) ) .as_str(), name.span(), ); let fn_name_mut = Ident::new( format!( "{}_mut", name.to_string().to_case(convert_case::Case::Snake) ) .as_str(), name.span(), ); let name_mut = Ident::new(format!("{base_name}Mut").as_str(), name.span()); let fields_ref = fields .unnamed .iter() .map(|field| { let ty = &field.ty; quote! { pub &'a #ty } }) .collect::>(); let fields_mut = fields .unnamed .iter() .map(|field| { let ty = &field.ty; quote! { pub &'a mut #ty } }) .collect::>(); let fields_names = fields .unnamed .iter() .enumerate() .map(|(idx, field)| Ident::new(format!("field{idx}").as_str(), field.span())) .collect::>(); let for_character = (struct_name == "Role").then_some(quote! { impl crate::character::Character { pub fn #fn_name_ref<'a>(&'a self) -> core::result::Result<#name_ref<'a>, crate::error::GameError> { let got = self.role_title(); self.role().#fn_name_ref().ok_or(crate::error::GameError::InvalidRole { got, expected: RoleTitle::#name, }) } pub fn #fn_name_mut<'a>(&'a mut self) -> core::result::Result<#name_mut<'a>, crate::error::GameError> { let got = self.role_title(); self.role_mut().#fn_name_mut().ok_or(crate::error::GameError::InvalidRole { got, expected: RoleTitle::#name, }) } } }); tokens.extend(quote! { pub struct #name_ref<'a>(#(#fields_ref),*); pub struct #name_mut<'a>(#(#fields_mut),*); #for_character impl #struct_name { pub fn #fn_name_ref<'a>(&'a self) -> Option<#name_ref<'a>> { match self { Self::#name(#(#fields_names),*) => Some(#name_ref(#(#fields_names),*)), _ => None, } } pub fn #fn_name_mut<'a>(&'a mut self) -> Option<#name_mut<'a>> { match self { Self::#name(#(#fields_names),*) => Some(#name_mut(#(#fields_names),*)), _ => None, } } } }); }