use crate::Error;
use crate::module::ModuleImports;
use crate::types::elm::sanitise_key;
use crate::types::primitive::INDENT;
use crate::{
Exporter,
types::{
NDT, fields,
fields::{datatype_with_inline_attr, optional_element_needs_parens, trailing_optional_run},
primitive::STRING,
},
};
use std::{borrow::Cow, collections::BTreeSet};
const NEVER: &'static str = "HOLA";
use specta::{
Types,
datatype::{DataType, Enum, Fields, Variant},
};
const FIELD_ALIAS_UNION_MARKER: &str = "specta_serde:deferred_alias_union";
const FIELD_ALIAS_EXCLUSION_MARKER: &str = "specta_serde:alias_exclusion";
pub fn render<E: Exporter>(
s: &mut String,
imports: &mut ModuleImports,
exporter: &E,
types: &Types,
e: &Enum,
ndt: &NDT,
) -> Result<(), Error> {
if e.attributes.get_named_as(FIELD_ALIAS_UNION_MARKER) == Some(&true) {
return alias_field_union_dt(s, imports, exporter, types, ndt, e);
}
let filtered_variants = active_variants(e);
for (variant_name, _variant) in filtered_variants.iter() {
let mut variant_location = Vec::from([ndt.rust_type_path()]);
variant_location.push(variant_name.clone());
}
let variants = filtered_variants
.into_iter()
.map(|(str, _)| str.to_string())
.collect::<Vec<String>>();
push_union(s, variants);
Ok(())
}
fn alias_field_union_dt<E: Exporter>(
s: &mut String,
imports: &mut ModuleImports,
exporter: &E,
types: &Types,
ndt: &NDT,
e: &Enum,
) -> Result<(), Error> {
let mut fields = Vec::with_capacity(e.variants.len());
let mut all_optional = true;
let location = Vec::from([ndt.rust_type_path()]);
for (name, variant) in active_variants(e) {
let mut variant = (*variant).clone();
if let Fields::Unnamed(unnamed) = &mut variant.fields
&& let Some(DataType::Struct(strct)) = unnamed
.fields
.first_mut()
.and_then(|field| field.ty.as_mut())
&& let Fields::Named(named) = &mut strct.fields
{
named
.fields
.retain(|(_, field)| !field.attributes.contains_key(FIELD_ALIAS_EXCLUSION_MARKER));
all_optional &= named.fields.iter().all(|(_, field)| field.optional);
} else {
all_optional = false;
}
let mut variant_location = location.clone();
variant_location.push(name.clone());
let field =
enum_variant_datatype(imports, exporter, types, ndt, name.clone(), &variant, None)?
.unwrap_or_else(|| NEVER.to_string());
fields.push(field);
}
let source = fields.join(" & ");
if all_optional {
s.push_str(&source);
return Ok(());
}
s.push_str("((");
s.push_str(&source);
s.push_str(") extends infer T extends object ? { [K in keyof T]: { [P in keyof T as P extends K ? P : never]: T[P] } & { [P in keyof T as P extends K ? never : P]?: never } }[keyof T] & object : never)");
Ok(())
}
fn active_variants(e: &Enum) -> Vec<&(Cow<'static, str>, Variant)> {
e.variants
.iter()
.filter(|(_, variant)| !variant.skip)
.collect()
}
#[derive(Debug, Clone, Copy)]
struct VariantTypeOverride<'a> {
key: &'a str,
ty: &'a str,
}
fn enum_variant_datatype<E: Exporter>(
imports: &mut ModuleImports,
exporter: &E,
types: &Types,
ndt: &NDT,
name: Cow<'static, str>,
variant: &Variant,
ty_override: Option<VariantTypeOverride<'_>>,
) -> Result<Option<String>, Error> {
let location = Vec::from([ndt.rust_type_path()]);
let path_string = location.join(".");
match &variant.fields {
Fields::Unit if name.is_empty() => Err(Error::unsupported_anonymous_enum_variant(
path_string,
"unit",
)),
Fields::Unit => Ok(Some(
sanitise_key(name, Some(&ndt.rust_type_path()))?.to_string(),
)),
Fields::Named(_) if name.is_empty() => Err(Error::unsupported_anonymous_enum_variant(
path_string,
"named-field",
)),
Fields::Named(obj) => {
let regular_fields: Vec<String> = Vec::new();
for (field_name, field) in &obj.fields {
let Some(ty) = field.ty.as_ref() else {
continue;
};
let mut other = String::new();
let mut field_location = Vec::from(location.clone());
if field_location
.last()
.is_some_and(|location| location == field_name)
{
if !matches!(ty, DataType::Struct(_)) {
field_location.push("0".into());
}
} else {
field_location.push(field_name.clone());
}
fields::render(
&mut other,
imports,
exporter,
types,
ndt,
field_name.clone(),
(field, ty),
None,
false,
ty_override
.as_ref()
.filter(|override_ty| override_ty.key == field_name.as_ref())
.map(|override_ty| override_ty.ty),
)?;
}
Ok(Some(if regular_fields.is_empty() {
format!("Record<{STRING}, {NEVER}>")
} else {
format!("{{ {} }}", regular_fields.join("; "))
}))
}
Fields::Unnamed(obj) => {
let live_fields = obj
.fields
.iter()
.filter_map(|field| field.ty.as_ref().map(|ty| (field, ty)))
.collect::<Vec<_>>();
let is_newtype = obj.fields.len() == 1;
let optional_from = if is_newtype {
live_fields.len()
} else {
trailing_optional_run(live_fields.iter().map(|(field, _)| *field))
};
let fields = live_fields
.iter()
.enumerate()
.map(|(idx, (_, ty))| {
let mut out = String::new();
let mut field_location = location.clone();
field_location.push(idx.to_string().into());
datatype_with_inline_attr(
&mut out, imports, exporter, types, ty, ndt, None, false,
)?;
if idx >= optional_from {
if optional_element_needs_parens(ty) {
out = format!("({out})");
}
out.push('?');
}
Ok(out)
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(match &fields[..] {
[] if obj.fields.is_empty() => Some("[]".to_string()),
[] => None,
[field] if is_newtype => Some(field.to_string()),
fields => Some(format!("[{}]", fields.join(", "))),
})
}
}
}
fn push_union(s: &mut String, variants: Vec<String>) {
let mut seen = BTreeSet::new();
let variants = variants
.into_iter()
.filter(|variant| seen.insert(variant.clone()))
.collect::<Vec<_>>();
s.push_str(&variants.join(&format!(" \n{INDENT}| ")));
}