werewolves/werewolves-macros/src/ref_and_mut.rs

251 lines
7.7 KiB
Rust

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 <https://www.gnu.org/licenses/>.
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<Self> {
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::<Box<[_]>>();
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::<Box<[_]>>();
let fields_idents = fields
.named
.iter()
.map(|c| c.ident.as_ref().expect("this is a named field??"))
.collect::<Box<[_]>>();
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::<Box<[_]>>();
let fields_mut = fields
.unnamed
.iter()
.map(|field| {
let ty = &field.ty;
quote! {
pub &'a mut #ty
}
})
.collect::<Box<[_]>>();
let fields_names = fields
.unnamed
.iter()
.enumerate()
.map(|(idx, field)| Ident::new(format!("field{idx}").as_str(), field.span()))
.collect::<Box<[_]>>();
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,
}
}
}
});
}