use std::collections::{BTreeMap, BTreeSet, HashMap};
use proc_macro2::TokenStream;
use quote::quote;
use crate::generate::ident::{make_ident, sanitize_doc, sanitize_ident, to_pascal_case};
use crate::schema::{ApiType, RuntimeApi};
#[derive(Debug, Clone)]
pub struct LiteralUnionEnum {
pub name: String,
pub doc: Option<String>,
pub variants: Vec<(String, String)>,
}
#[derive(Debug, Default)]
pub struct UnionRegistry {
enums: Vec<LiteralUnionEnum>,
by_values: HashMap<Vec<String>, String>,
names: BTreeSet<String>,
}
impl UnionRegistry {
#[must_use]
pub fn names(&self) -> &BTreeSet<String> {
&self.names
}
#[must_use]
pub fn enums(&self) -> &[LiteralUnionEnum] {
&self.enums
}
#[must_use]
pub fn resolve(&self, api_type: &ApiType) -> Option<&str> {
if !api_type.is_homog_string_literal_union() {
return None;
}
let values = api_type.string_literal_values();
self.by_values.get(&values).map(String::as_str)
}
fn insert(&mut self, name: String, doc: Option<String>, literals: Vec<String>) {
if self.by_values.contains_key(&literals) {
return;
}
let mut unique_name = name;
if self.names.contains(&unique_name) {
let mut suffix = 2u32;
while self.names.contains(&format!("{unique_name}{suffix}")) {
suffix = suffix.saturating_add(1);
}
unique_name = format!("{unique_name}{suffix}");
}
let variants = unique_variants(&literals);
self.names.insert(unique_name.clone());
self.by_values.insert(literals, unique_name.clone());
self.enums.push(LiteralUnionEnum {
name: unique_name,
doc,
variants,
});
}
fn register_named(&mut self, name: &str, doc: &str, api_type: &ApiType) {
let literals: Vec<String> = api_type.string_literal_values();
if literals.is_empty() {
return;
}
let doc = if doc.is_empty() {
None
} else {
Some(sanitize_doc(doc))
};
if let Some(existing) = self.by_values.get(&literals).cloned() {
if existing == name {
return;
}
self.by_values.insert(literals, name.to_string());
self.names.remove(&existing);
self.names.insert(name.to_string());
if let Some(entry) = self.enums.iter_mut().find(|e| e.name == existing) {
entry.name = name.to_string();
entry.doc = doc.or_else(|| entry.doc.clone());
}
return;
}
self.insert(name.to_string(), doc, literals);
}
fn register_anonymous(&mut self, owner: &str, member: &str, api_type: &ApiType) {
let literals = api_type.string_literal_values();
if literals.is_empty() {
return;
}
if self.by_values.contains_key(&literals) {
return;
}
let name = format!("{}{}", to_pascal_case(owner), to_pascal_case(member));
self.insert(name, None, literals);
}
}
#[must_use]
pub fn collect_literal_unions(api: &RuntimeApi) -> UnionRegistry {
let mut registry = UnionRegistry::default();
for concept in &api.concepts {
if concept.type_name.is_homog_string_literal_union() {
registry.register_named(&concept.name, &concept.description, &concept.type_name);
}
}
for class in &api.classes {
for attribute in &class.attributes {
if let Some(ty) = attribute
.read_type
.as_ref()
.or(attribute.write_type.as_ref())
{
walk_type_for_anonymous(&mut registry, &class.name, &attribute.name, ty);
}
}
for method in &class.methods {
for param in &method.parameters {
let member = if param.name.is_empty() {
method.name.as_str()
} else {
param.name.as_str()
};
walk_type_for_anonymous(&mut registry, &class.name, member, ¶m.type_name);
}
for ret in &method.return_values {
walk_type_for_anonymous(&mut registry, &class.name, &method.name, &ret.type_name);
}
}
}
for event in &api.events {
for param in &event.data {
walk_type_for_anonymous(&mut registry, &event.name, ¶m.name, ¶m.type_name);
}
}
for concept in &api.concepts {
walk_concept_tables(&mut registry, concept.name.as_str(), &concept.type_name);
}
registry
}
fn walk_concept_tables(registry: &mut UnionRegistry, owner: &str, ty: &ApiType) {
match ty.complex_type() {
Some("table") => {
for (field, field_ty, _) in ty.parameters() {
walk_type_for_anonymous(registry, owner, &field, &field_ty);
}
}
Some("union") => {
for option in ty.options() {
walk_concept_tables(registry, owner, &option);
}
}
_ => {}
}
}
fn walk_type_for_anonymous(registry: &mut UnionRegistry, owner: &str, member: &str, ty: &ApiType) {
if ty.is_homog_string_literal_union() {
registry.register_anonymous(owner, member, ty);
return;
}
match ty.complex_type() {
Some("array" | "dictionary" | "LuaCustomTable" | "LuaLazyLoadedValue" | "type") => {
if let Some(inner) = ty.child_type("value").or_else(|| ty.child_type("key")) {
walk_type_for_anonymous(registry, owner, member, &inner);
}
}
Some("tuple") => {
for (index, value) in ty.tuple_values().iter().enumerate() {
walk_type_for_anonymous(registry, owner, &format!("{member}_{index}"), value);
}
}
Some("table") => {
for (field, field_ty, _) in ty.parameters() {
walk_type_for_anonymous(registry, owner, &field, &field_ty);
}
}
Some("union") => {
for option in ty.non_nil_options() {
walk_type_for_anonymous(registry, owner, member, &option);
}
}
_ => {}
}
}
fn unique_variants(literals: &[String]) -> Vec<(String, String)> {
let mut used: BTreeMap<String, u32> = BTreeMap::new();
literals
.iter()
.map(|literal| {
let mut variant = literal_to_variant(literal);
let count = used.entry(variant.clone()).or_insert(0);
*count = count.saturating_add(1);
if *count > 1 {
variant = format!("{variant}_{count}");
}
(variant, literal.clone())
})
.collect()
}
fn literal_to_variant(literal: &str) -> String {
match literal {
"=" | "==" => return "Eq".to_string(),
"≠" | "!=" => return "Ne".to_string(),
">" => return "Gt".to_string(),
"<" => return "Lt".to_string(),
"≥" | ">=" => return "Ge".to_string(),
"≤" | "<=" => return "Le".to_string(),
"*" => return "Mul".to_string(),
"/" => return "Div".to_string(),
"+" => return "Add".to_string(),
"-" => return "Sub".to_string(),
"%" => return "Mod".to_string(),
"^" => return "Pow".to_string(),
"<<" => return "Shl".to_string(),
">>" => return "Shr".to_string(),
_ => {}
}
let normalized = literal.replace(['-', ' ', '/'], "_");
let pascal = to_pascal_case(&normalized);
let sanitized = sanitize_ident(&pascal);
if sanitized.is_empty() || sanitized.chars().next().is_some_and(|c| c.is_ascii_digit()) {
format!("Value_{sanitized}")
} else {
sanitized
}
}
#[must_use]
pub fn generate_unions(registry: &UnionRegistry) -> String {
let header = "#![allow(nonstandard_style)]\n";
let enums = registry.enums().iter().map(generate_enum);
let lookup_arms = registry.enums().iter().flat_map(|enumeration| {
enumeration.variants.iter().map(move |(variant, literal)| {
let type_name = enumeration.name.as_str();
let variant_name = variant.as_str();
quote! { (#type_name, #variant_name) => Some(#literal), }
})
});
let tokens = quote! {
#( #enums )*
#[must_use]
pub fn literal_enum_variant_str(type_name: &str, variant: &str) -> Option<&'static str> {
match (type_name, variant) {
#( #lookup_arms )*
_ => None,
}
}
};
format!("{header}{tokens}")
}
fn generate_enum(enumeration: &LiteralUnionEnum) -> TokenStream {
let name = make_ident(&enumeration.name);
let variants: Vec<_> = enumeration
.variants
.iter()
.map(|(variant, _)| make_ident(variant))
.collect();
let first = variants
.first()
.cloned()
.unwrap_or_else(|| make_ident("Unknown"));
let as_str_arms = enumeration.variants.iter().map(|(variant, literal)| {
let variant_ident = make_ident(variant);
quote! { Self::#variant_ident => #literal, }
});
let doc_attr = enumeration.doc.as_ref().map(|d| quote!(#[doc = #d]));
quote! {
#doc_attr
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum #name {
#( #variants, )*
}
impl #name {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
#( #as_str_arms )*
}
}
}
impl Default for #name {
fn default() -> Self {
Self::#first
}
}
impl PartialEq<&str> for #name {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<#name> for &str {
fn eq(&self, other: &#name) -> bool {
*self == other.as_str()
}
}
impl From<#name> for crate::LuaAny {
fn from(_: #name) -> Self {
crate::LuaAny
}
}
impl From<#name> for &'static str {
fn from(value: #name) -> Self {
value.as_str()
}
}
}
}