use ahash::{AHashMap, AHashSet};
use crate::codegen::naming::{to_node_name, wire_variant_value};
use crate::core::ir::{ApiSurface, EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use super::enums::is_untagged_data_enum;
enum TsAuxDecl {
Interface { name: String, fields: Vec<TsField> },
Alias { name: String, members: Vec<String> },
}
struct TsField {
name: String,
ts_type: String,
}
pub(super) struct UntaggedEnumTsPlan {
pub(super) value_type_name: String,
pub(super) extern_type_declaration: String,
}
pub(super) struct AllUntaggedEnumsTsPlan {
pub(super) plans: AHashMap<String, UntaggedEnumTsPlan>,
pub(super) ts_body: String,
pub(super) custom_section: String,
}
pub(super) fn build_untagged_enum_ts_plan_for_api(
api: &ApiSurface,
exclude_types: &[String],
opaque_type_names: &AHashSet<String>,
text_field_enum_names: &AHashSet<String>,
prefix: &str,
) -> AllUntaggedEnumsTsPlan {
let exclude_types_set: AHashSet<String> = exclude_types.iter().cloned().collect();
let untagged_enum_defs: Vec<&EnumDef> = api
.enums
.iter()
.filter(|e| gets_a_ts_union(e, &exclude_types_set, text_field_enum_names))
.collect();
build_untagged_enum_ts_plans(&untagged_enum_defs, api, &exclude_types_set, opaque_type_names, prefix)
}
fn gets_a_ts_union(
enum_def: &EnumDef,
exclude_types: &AHashSet<String>,
text_field_enum_names: &AHashSet<String>,
) -> bool {
!exclude_types.contains(&enum_def.name)
&& !text_field_enum_names.contains(&enum_def.name)
&& is_untagged_data_enum(enum_def)
}
pub(super) fn value_type_names(plan: &AllUntaggedEnumsTsPlan) -> AHashMap<String, String> {
plan.plans
.iter()
.map(|(name, enum_plan)| (name.clone(), enum_plan.value_type_name.clone()))
.collect()
}
pub(super) fn build_untagged_enum_ts_plans(
untagged_enums: &[&EnumDef],
api: &ApiSurface,
exclude_types: &AHashSet<String>,
opaque_type_names: &AHashSet<String>,
prefix: &str,
) -> AllUntaggedEnumsTsPlan {
let mut ctx = TsMapContext {
api,
exclude_types,
opaque_type_names,
prefix,
in_progress: AHashSet::default(),
resolved_names: AHashMap::default(),
decls: Vec::new(),
};
let mut plans = AHashMap::default();
for enum_def in untagged_enums {
if !ctx.resolved_names.contains_key(&enum_def.name) {
ctx.in_progress.insert(enum_def.name.clone());
let members: Vec<String> = enum_def.variants.iter().map(|v| ctx.map_variant(v)).collect();
ctx.in_progress.remove(&enum_def.name);
let ts_type_name = format!("{prefix}{}", enum_def.name);
ctx.resolved_names.insert(enum_def.name.clone(), ts_type_name.clone());
ctx.decls.push(TsAuxDecl::Alias {
name: ts_type_name,
members,
});
}
let ts_type_name = format!("{prefix}{}", enum_def.name);
let value_type_name = format!("{ts_type_name}Value");
let extern_type_declaration = crate::backends::wasm::template_env::render(
"ts_extern_value_type",
minijinja::context! {
ts_type_name => ts_type_name,
value_type_name => value_type_name.clone(),
},
);
plans.insert(
enum_def.name.clone(),
UntaggedEnumTsPlan {
value_type_name,
extern_type_declaration,
},
);
}
let ts_body = if ctx.decls.is_empty() {
String::new()
} else {
ctx.decls.iter().map(render_aux_decl).collect::<Vec<_>>().join("\n\n")
};
let custom_section = if ts_body.is_empty() {
String::new()
} else {
crate::backends::wasm::template_env::render(
"ts_custom_section",
minijinja::context! {
const_name => "ALEF_UNTAGGED_UNIONS_TS",
ts_body => ts_body.clone(),
},
)
};
AllUntaggedEnumsTsPlan {
plans,
ts_body,
custom_section,
}
}
fn render_alias(name: &str, members: &[String]) -> String {
crate::backends::wasm::template_env::render(
"ts_type_alias",
minijinja::context! { name => name, members => members },
)
.trim_end()
.to_string()
}
fn render_aux_decl(decl: &TsAuxDecl) -> String {
match decl {
TsAuxDecl::Interface { name, fields } => render_interface(name, fields),
TsAuxDecl::Alias { name, members } => render_alias(name, members),
}
}
fn render_interface(name: &str, fields: &[TsField]) -> String {
crate::backends::wasm::template_env::render(
"ts_interface",
minijinja::context! {
name => name,
fields => fields.iter().map(|f| minijinja::context! {
name => f.name,
ts_type => f.ts_type,
}).collect::<Vec<_>>(),
},
)
.trim_end()
.to_string()
}
fn render_inline_object(fields: &[TsField]) -> String {
crate::backends::wasm::template_env::render(
"ts_inline_object",
minijinja::context! {
fields => fields.iter().map(|f| minijinja::context! {
name => f.name,
ts_type => f.ts_type,
}).collect::<Vec<_>>(),
},
)
.trim_end()
.to_string()
}
struct TsMapContext<'a> {
api: &'a ApiSurface,
exclude_types: &'a AHashSet<String>,
opaque_type_names: &'a AHashSet<String>,
prefix: &'a str,
in_progress: AHashSet<String>,
resolved_names: AHashMap<String, String>,
decls: Vec<TsAuxDecl>,
}
impl TsMapContext<'_> {
fn map_variant(&mut self, variant: &EnumVariant) -> String {
if variant.fields.is_empty() {
return "null".to_string();
}
if variant.is_tuple {
if variant.fields.len() == 1 {
return self.map_type(&variant.fields[0].ty);
}
let members: Vec<String> = variant.fields.iter().map(|f| self.map_type(&f.ty)).collect();
return format!("[{}]", members.join(", "));
}
let fields = self.map_fields(&variant.fields);
render_inline_object(&fields)
}
fn map_fields(&mut self, fields: &[FieldDef]) -> Vec<TsField> {
fields
.iter()
.map(|f| TsField {
name: to_node_name(&f.name),
ts_type: self.map_field_type(f),
})
.collect()
}
fn map_field_type(&mut self, field: &FieldDef) -> String {
let base = self.map_type(&field.ty);
if field.optional && !matches!(field.ty, TypeRef::Optional(_)) {
format!("{base} | undefined")
} else {
base
}
}
fn map_type(&mut self, ty: &TypeRef) -> String {
match ty {
TypeRef::Primitive(p) => primitive_ts_type(p).to_string(),
TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
TypeRef::Bytes => "Uint8Array".to_string(),
TypeRef::Unit => "null".to_string(),
TypeRef::Json => "any".to_string(),
TypeRef::Duration => "number".to_string(),
TypeRef::Optional(inner) => format!("{} | undefined", self.map_type(inner)),
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Optional(_) => format!("({})[]", self.map_type(inner)),
_ => format!("{}[]", self.map_type(inner)),
},
TypeRef::Map(key, value) => {
let key_ts = self.map_type(key);
if key_ts == "string" {
format!("Record<string, {}>", self.map_type(value))
} else {
"any".to_string()
}
}
TypeRef::Named(name) => self.map_named(name),
}
}
fn map_named(&mut self, name: &str) -> String {
if self.exclude_types.contains(name) || self.opaque_type_names.contains(name) {
return "any".to_string();
}
if let Some(resolved) = self.resolved_names.get(name) {
return resolved.clone();
}
let ts_name = format!("{}{name}", self.prefix);
if self.in_progress.contains(name) {
return ts_name;
}
if let Some(enum_def) = self.api.enums.iter().find(|e| e.name == name) {
return self.map_named_enum(enum_def, ts_name);
}
if let Some(type_def) = self.api.types.iter().find(|t| t.name == name) {
return self.map_named_struct(type_def, ts_name);
}
"any".to_string()
}
fn map_named_struct(&mut self, type_def: &TypeDef, ts_name: String) -> String {
if type_def.is_opaque {
return "any".to_string();
}
self.in_progress.insert(type_def.name.clone());
let fields = self.map_fields(&type_def.fields);
self.in_progress.remove(&type_def.name);
self.resolved_names.insert(type_def.name.clone(), ts_name.clone());
self.decls.push(TsAuxDecl::Interface {
name: ts_name.clone(),
fields,
});
ts_name
}
fn map_named_enum(&mut self, enum_def: &EnumDef, ts_name: String) -> String {
if enum_def.variants.iter().all(|v| v.fields.is_empty()) {
let literal_name = format!("{ts_name}Wire");
let values: Vec<String> = enum_def
.variants
.iter()
.map(|v| {
let wire =
wire_variant_value(&v.name, v.serde_rename.as_deref(), enum_def.serde_rename_all.as_deref());
format!("\"{wire}\"")
})
.collect();
self.resolved_names.insert(enum_def.name.clone(), literal_name.clone());
self.decls.push(TsAuxDecl::Alias {
name: literal_name.clone(),
members: values,
});
return literal_name;
}
if is_untagged_data_enum(enum_def) {
return ts_name;
}
"any".to_string()
}
}
pub(crate) fn docs_ts_type_for_untagged_enum(
enum_def: &EnumDef,
api: &ApiSurface,
config: &crate::core::config::ResolvedCrateConfig,
) -> Option<String> {
let exclude_types_vec = wasm_exclude_types(config);
let text_field_enum_names: AHashSet<String> = config.untagged_union_text_types.iter().cloned().collect();
let exclude_types: AHashSet<String> = exclude_types_vec.iter().cloned().collect();
if !gets_a_ts_union(enum_def, &exclude_types, &text_field_enum_names) {
return None;
}
let opaque_type_names = wasm_opaque_type_names(api, &exclude_types_vec);
let prefix = config.wasm_type_prefix();
let plan = build_untagged_enum_ts_plans(&[enum_def], api, &exclude_types, &opaque_type_names, &prefix);
Some(plan.ts_body)
}
pub(super) fn wasm_exclude_types(config: &crate::core::config::ResolvedCrateConfig) -> Vec<String> {
let mut exclude_types = config
.wasm
.as_ref()
.map(|c| c.exclude_types.clone())
.unwrap_or_default();
exclude_types.extend(
config
.opaque_types
.iter()
.filter(|(_, path)| path.contains('<'))
.map(|(name, _)| name.clone()),
);
exclude_types
}
pub(super) fn wasm_opaque_type_names(api: &ApiSurface, exclude_types: &[String]) -> AHashSet<String> {
api.types
.iter()
.filter(|t| t.is_opaque && !exclude_types.contains(&t.name))
.map(|t| t.name.clone())
.collect()
}
fn primitive_ts_type(prim: &crate::core::ir::PrimitiveType) -> &'static str {
use crate::core::ir::PrimitiveType;
match prim {
PrimitiveType::Bool => "boolean",
PrimitiveType::U64 | PrimitiveType::I64 => "bigint",
PrimitiveType::U8
| PrimitiveType::U16
| PrimitiveType::U32
| PrimitiveType::I8
| PrimitiveType::I16
| PrimitiveType::I32
| PrimitiveType::F32
| PrimitiveType::F64
| PrimitiveType::Usize
| PrimitiveType::Isize => "number",
}
}
#[cfg(test)]
mod tests;