Files
ic10emu/xtask/src/generate/enums.rs
2024-09-16 18:02:56 -07:00

460 lines
14 KiB
Rust

use convert_case::{Case, Casing};
use std::collections::BTreeMap;
use std::{
fmt::Display,
io::{BufWriter, Write},
path::PathBuf,
str::FromStr,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
pub fn generate(
stationpedia: &crate::stationpedia::Stationpedia,
enums: &crate::enums::Enums,
workspace: &std::path::Path,
) -> color_eyre::Result<Vec<PathBuf>> {
eprintln!("Writing Enum Listings ...");
let enums_path = workspace.join("stationeers_data").join("src").join("enums");
if !enums_path.exists() {
std::fs::create_dir(&enums_path)?;
}
let basic_enum_names = enums
.basic_enums
.values()
.map(|enm| enm.enum_name.clone())
.collect::<Vec<_>>();
let mut writer = std::io::BufWriter::new(std::fs::File::create(enums_path.join("script.rs"))?);
write_repr_enum_use_header(&mut writer)?;
for enm in enums.script_enums.values() {
write_enum_listing(&mut writer, enm)?;
}
let mut writer = std::io::BufWriter::new(std::fs::File::create(enums_path.join("basic.rs"))?);
write_repr_enum_use_header(&mut writer)?;
let script_enums_in_basic = enums
.script_enums
.values()
.filter(|enm| basic_enum_names.contains(&enm.enum_name))
.collect::<Vec<_>>();
let script_enums_in_basic_names = script_enums_in_basic
.iter()
.map(|enm| enm.enum_name.as_str())
.collect::<Vec<_>>();
write_repr_basic_use_header(&mut writer, script_enums_in_basic.as_slice())?;
for enm in enums.basic_enums.values() {
if script_enums_in_basic_names.contains(&enm.enum_name.as_str()) {
continue;
}
write_enum_listing(&mut writer, enm)?;
}
write_enum_aggregate_mod(&mut writer, &enums.basic_enums)?;
let mut writer = std::io::BufWriter::new(std::fs::File::create(enums_path.join("prefabs.rs"))?);
write_repr_enum_use_header(&mut writer)?;
let prefabs = stationpedia
.pages
.iter()
.map(|page| {
let variant = ReprEnumVariant {
value: page.prefab_hash,
deprecated: false,
props: vec![
("name".to_owned(), page.title.clone()),
("desc".to_owned(), page.description.clone()),
],
};
(page.prefab_name.clone(), variant)
})
.collect::<Vec<_>>();
write_repr_enum(&mut writer, "StationpediaPrefab", &prefabs, true)?;
Ok(vec![
enums_path.join("script.rs"),
enums_path.join("basic.rs"),
enums_path.join("prefabs.rs"),
])
}
#[allow(clippy::type_complexity)]
fn write_enum_aggregate_mod<T: std::io::Write>(
writer: &mut BufWriter<T>,
enums: &BTreeMap<String, crate::enums::EnumListing>,
) -> color_eyre::Result<()> {
let (
(variant_lines, value_arms),
(
(get_str_arms, iter_chain),
(from_str_arms_iter, display_arms)
)
): (
(Vec<_>, Vec<_>),
((Vec<_>, Vec<_>), (Vec<_>, Vec<_>)),
) = enums
.iter()
.enumerate()
.map(|(index, (name, listing))| {
let variant_name: TokenStream = if name.is_empty() || name == "_unnamed" {
"Unnamed"
} else {
name
}
.to_case(Case::Pascal)
.parse()
.unwrap();
let fromstr_variant_name = variant_name.clone();
let enum_name: TokenStream = listing.enum_name.to_case(Case::Pascal).parse().unwrap();
let display_sep = if name.is_empty() || name == "_unnamed" {
""
} else {
"."
};
let display_pat = format!("{name}{display_sep}{{}}");
let name: TokenStream = if name == "_unnamed" {
String::new()
} else {
name.clone()
}
.parse()
.unwrap();
(
(
quote! {
#variant_name(#enum_name),
},
quote! {
Self::#variant_name(enm) => *enm as u32,
},
),
(
(
quote! {
Self::#variant_name(enm) => enm.get_str(prop),
},
if index == 0 {
quote! {
#enum_name::iter().map(Self::#variant_name)
}
} else {
quote! {
.chain(#enum_name::iter().map(Self::#variant_name))
}
},
),
(
listing.values.keys().map(move |variant| {
let sep = if name.is_empty() { "" } else { "." };
let fromstr_pat = format!("{name}{sep}{variant}").to_lowercase();
let variant: TokenStream = variant.to_case(Case::Pascal).parse().unwrap();
quote! {
#fromstr_pat => Ok(Self::#fromstr_variant_name(#enum_name::#variant)),
}
}),
quote! {
Self::#variant_name(enm) => write!(f, #display_pat, enm),
},
),
),
)
})
.unzip();
let from_str_arms = from_str_arms_iter.into_iter().flatten().collect::<Vec<_>>();
let tokens = quote! {
pub enum BasicEnum {
#(#variant_lines)*
}
impl BasicEnum {
pub fn get_value(&self) -> u32 {
match self {
#(#value_arms)*
}
}
pub fn get_str(&self, prop: &str) -> Option<&'static str> {
match self {
#(#get_str_arms)*
}
}
pub fn iter() -> impl std::iter::Iterator<Item = Self> {
use strum::IntoEnumIterator;
#(#iter_chain)*
}
}
impl std::str::FromStr for BasicEnum {
type Err = super::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
#(#from_str_arms)*
_ => Err(super::ParseError { enm: s.to_string() })
}
}
}
impl std::fmt::Display for BasicEnum {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#(#display_arms)*
}
}
}
};
write!(writer, "{tokens}",)?;
Ok(())
}
pub fn write_enum_listing<T: std::io::Write>(
writer: &mut BufWriter<T>,
enm: &crate::enums::EnumListing,
) -> color_eyre::Result<()> {
let max = enm
.values
.values()
.map(|var| var.value)
.max()
.expect("enum should have max value");
let min = enm
.values
.values()
.map(|var| var.value)
.min()
.expect("enum should have min value");
if max < u8::MAX as i64 && min >= u8::MIN as i64 {
let variants: Vec<_> = enm
.values
.iter()
.map(|(n, var)| {
let variant = ReprEnumVariant {
value: var.value as u8,
deprecated: var.deprecated,
props: vec![("docs".to_owned(), var.description.clone())],
};
(n.clone(), variant)
})
.collect();
write_repr_enum(
writer,
&enm.enum_name.to_case(Case::Pascal),
&variants,
true,
)?;
} else if max < u16::MAX as i64 && min >= u16::MIN as i64 {
let variants: Vec<_> = enm
.values
.iter()
.map(|(n, var)| {
let variant = ReprEnumVariant {
value: var.value as u16,
deprecated: var.deprecated,
props: vec![("docs".to_owned(), var.description.clone())],
};
(n.clone(), variant)
})
.collect();
write_repr_enum(writer, &enm.enum_name, &variants, true)?;
} else if max < u32::MAX as i64 && min >= u32::MIN as i64 {
let variants: Vec<_> = enm
.values
.iter()
.map(|(n, var)| {
let variant = ReprEnumVariant {
value: var.value as u32,
deprecated: var.deprecated,
props: vec![("docs".to_owned(), var.description.clone())],
};
(n.clone(), variant)
})
.collect();
write_repr_enum(writer, &enm.enum_name, &variants, true)?;
} else if max < i32::MAX as i64 && min >= i32::MIN as i64 {
let variants: Vec<_> = enm
.values
.iter()
.map(|(n, var)| {
let variant = ReprEnumVariant {
value: var.value as i32,
deprecated: var.deprecated,
props: vec![("docs".to_owned(), var.description.clone())],
};
(n.clone(), variant)
})
.collect();
write_repr_enum(writer, &enm.enum_name, &variants, true)?;
} else {
let variants: Vec<_> = enm
.values
.iter()
.map(|(n, var)| {
let variant = ReprEnumVariant {
value: var.value as i32,
deprecated: var.deprecated,
props: vec![("docs".to_owned(), var.description.clone())],
};
(n.clone(), variant)
})
.collect();
write_repr_enum(writer, &enm.enum_name, &variants, true)?;
}
Ok(())
}
struct ReprEnumVariant<P>
where
P: Display + FromStr + Ord,
{
pub value: P,
pub deprecated: bool,
pub props: Vec<(String, String)>,
}
fn write_repr_enum_use_header<T: std::io::Write>(
writer: &mut BufWriter<T>,
) -> color_eyre::Result<()> {
write!(
writer,
"{}",
quote! {
use serde_derive::{{Deserialize, Serialize}};
use strum::{
AsRefStr, Display, EnumIter, EnumProperty, EnumString, FromRepr,
};
#[cfg(feature = "tsify")]
use tsify::Tsify;
#[cfg(feature = "tsify")]
use wasm_bindgen::prelude::*;
}
)?;
Ok(())
}
fn write_repr_basic_use_header<T: std::io::Write>(
writer: &mut BufWriter<T>,
script_enums: &[&crate::enums::EnumListing],
) -> color_eyre::Result<()> {
let enums = script_enums
.iter()
.map(|enm| Ident::new(&enm.enum_name.to_case(Case::Pascal), Span::call_site()))
.collect::<Vec<_>>();
write!(writer, "{}", quote! {use super::script::{ #(#enums),*};},)?;
Ok(())
}
fn write_repr_enum<'a, T: std::io::Write, I, P>(
writer: &mut BufWriter<T>,
name: &str,
variants: I,
use_phf: bool,
) -> color_eyre::Result<()>
where
P: Display + FromStr + num::integer::Integer + num::cast::AsPrimitive<i64> + 'a,
I: IntoIterator<Item = &'a (String, ReprEnumVariant<P>)>,
{
let additional_strum = if use_phf {
quote! {#[strum(use_phf)]}
} else {
TokenStream::new()
};
let repr = Ident::new(std::any::type_name::<P>(), Span::call_site());
let mut sorted: Vec<_> = variants.into_iter().collect::<Vec<_>>();
sorted.sort_by_key(|(_, variant)| &variant.value);
let mut derives = [
"Debug",
"Display",
"Clone",
"Copy",
"PartialEq",
"Eq",
"PartialOrd",
"Ord",
"Hash",
"EnumString",
"AsRefStr",
"EnumProperty",
"EnumIter",
"FromRepr",
"Serialize",
"Deserialize",
]
.into_iter()
.map(|d| Ident::new(d, Span::call_site()))
.collect::<Vec<_>>();
if sorted
.iter()
.any(|(name, _)| name == "None" || name == "Default")
{
derives.insert(0, Ident::new("Default", Span::call_site()));
}
let variants = sorted
.iter()
.map(|(name, variant)| {
let variant_name = Ident::new(
&name.replace('.', "").to_case(Case::Pascal),
Span::call_site(),
);
let mut props = Vec::new();
if variant.deprecated {
props.push(quote! {deprecated = "true"});
}
for (prop_name, prop_val) in &variant.props {
let prop_name = Ident::new(prop_name, Span::call_site());
let val_string = prop_val.to_string();
props.push(quote! { #prop_name = #val_string });
}
let val: TokenStream = format!("{}{repr}", variant.value).parse().unwrap();
let val_string = variant.value.as_().to_string();
props.push(quote! {value = #val_string });
let default = if variant_name == "None" || variant_name == "Default" {
quote! {#[default]}
} else {
TokenStream::new()
};
quote! {
#[strum(serialize = #name)]
#[strum(props(#(#props),*))]
#default
#variant_name = #val,
}
})
.collect::<Vec<_>>();
let name = Ident::new(name, Span::call_site());
write!(
writer,
"{}",
quote! {
#[derive(#(#derives),*)]
#[cfg_attr(feature = "tsify", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#additional_strum
#[repr(#repr)]
pub enum #name {
#(#variants)*
}
impl TryFrom<f64> for #name {
type Error = super::ParseError;
fn try_from(value: f64) -> Result<Self, <#name as TryFrom<f64>>::Error> {
use strum::IntoEnumIterator;
if let Some(enm) = #name::iter().find(|enm| (f64::from(*enm as #repr) - value).abs() < f64::EPSILON ) {
Ok(enm)
} else {
Err(super::ParseError {
enm: value.to_string()
})
}
}
}
}
)?;
Ok(())
}