use crate::core::ir::{ApiSurface, EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use ahash::AHashSet;
use crate::backends::wasm::type_map::WasmMapper;
use crate::codegen::cfg::is_host_owned_rust_path;
use crate::codegen::conversions::helpers::{apply_explicit_field_newtype_from_core, is_explicit_newtype};
use crate::codegen::field_init::struct_field_init;
use crate::codegen::naming::{to_node_name, wire_variant_value};
use crate::codegen::type_mapper::TypeMapper;
use super::functions::{emit_rustdoc, typeref_to_core_type_str};
use super::types::types_helpers::apply_newtype_to_core;
use super::types::types_helpers::class_backed_field_type;
use super::types::types_helpers::complex_newtype_field_uses_jsvalue;
#[path = "enums_accessors.rs"]
mod enums_accessors;
use enums_accessors::PayloadField;
mod tagged_conversions;
#[cfg(test)]
pub(super) use tagged_conversions::{gen_tagged_enum_binding_to_core, gen_tagged_enum_core_to_binding};
pub(super) use tagged_conversions::{
gen_tagged_enum_binding_to_core_with_paths, gen_tagged_enum_core_to_binding_with_remaps,
};
pub(crate) fn is_tagged_data_enum(enum_def: &EnumDef, types: &[TypeDef]) -> bool {
let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
has_data_variants
&& (enum_def.serde_tag.is_some() || !enum_def.serde_untagged)
&& !is_variant_untagged_string_enum(enum_def)
&& !is_fully_flattened_internal_enum(enum_def, types)
}
pub(crate) fn is_untagged_data_enum(enum_def: &EnumDef) -> bool {
enum_def.serde_untagged && enum_def.variants.iter().any(|v| !v.fields.is_empty())
}
pub(crate) fn is_variant_untagged_string_enum(enum_def: &EnumDef) -> bool {
let mut data_variants = enum_def.variants.iter().filter(|v| !v.fields.is_empty()).peekable();
data_variants.peek().is_some()
&& enum_def.serde_tag.is_none()
&& !enum_def.serde_untagged
&& data_variants.clone().all(|v| v.serde_untagged)
}
pub(crate) fn is_fully_flattened_internal_enum(enum_def: &EnumDef, types: &[TypeDef]) -> bool {
let data_variants: Vec<&EnumVariant> = enum_def.variants.iter().filter(|v| !v.fields.is_empty()).collect();
!data_variants.is_empty()
&& data_variants
.iter()
.all(|v| crate::codegen::serde_enum_repr::serde_flattens_newtype_payload(enum_def, v, types))
}
pub(crate) fn is_json_passthrough_data_enum(enum_def: &EnumDef, types: &[TypeDef]) -> bool {
is_untagged_data_enum(enum_def)
|| is_variant_untagged_string_enum(enum_def)
|| is_fully_flattened_internal_enum(enum_def, types)
}
pub(super) fn register_untagged_data_enum_overrides(
api: &ApiSurface,
type_overrides: &mut std::collections::HashMap<String, String>,
) -> AHashSet<String> {
let names: AHashSet<String> = api
.enums
.iter()
.filter(|e| is_json_passthrough_data_enum(e, &api.types))
.map(|e| e.name.clone())
.collect();
for name in &names {
type_overrides
.entry(name.clone())
.or_insert_with(|| "JsValue".to_string());
}
names
}
fn escape_rust_keyword(name: &str) -> String {
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl",
"in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
"super", "trait", "true", "type", "unsafe", "use", "where", "while", "async", "await", "dyn", "abstract",
"become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
];
if RUST_KEYWORDS.contains(&name) {
format!("r#{name}")
} else {
name.to_string()
}
}
fn mixed_type_fields(enum_def: &EnumDef) -> std::collections::BTreeSet<String> {
let mut field_types: std::collections::HashMap<String, std::collections::BTreeSet<String>> =
std::collections::HashMap::new();
for variant in &enum_def.variants {
for field in &variant.fields {
if let TypeRef::Named(n) = &field.ty {
field_types.entry(field.name.clone()).or_default().insert(n.clone());
}
}
}
field_types
.into_iter()
.filter(|(_, types)| types.len() > 1)
.map(|(name, _)| name)
.collect()
}
fn is_sanitized_tuple_vec(field: &FieldDef) -> bool {
field.sanitized && field.original_type.as_deref().is_some_and(|s| s.starts_with("Vec<("))
}
fn is_sanitized_fixed_tuple_array(field: &FieldDef) -> bool {
field.sanitized
&& field
.original_type
.as_deref()
.is_some_and(|s| s.starts_with("[(") && s.contains(");"))
}
fn box_wrap_map_into(base: String, is_boxed: bool) -> String {
if is_boxed {
format!("{base}.map(Box::new)")
} else {
base
}
}
fn box_explicit_newtype_expr(expr: String, field: &FieldDef) -> String {
if !field.is_boxed || !field.newtype_wrapper.as_deref().is_some_and(is_explicit_newtype) {
return expr;
}
if field.optional || matches!(field.ty, TypeRef::Optional(_)) {
format!("({expr}).map(Box::new)")
} else {
format!("Box::new({expr})")
}
}
fn apply_wasm_explicit_field_newtype_to_core(expr: &str, field: &FieldDef) -> Option<String> {
field
.newtype_wrapper
.as_deref()
.filter(|wrapper| is_explicit_newtype(wrapper))
.map(|wrapper| apply_newtype_to_core(expr, &field.ty, field.optional, wrapper))
}
fn tagged_enum_binding_to_core_expr(
field: &FieldDef,
field_ident: &str,
core_import: &str,
source_crate_remaps: &[(&str, &str)],
type_paths: &ahash::AHashMap<String, String>,
) -> String {
if complex_newtype_field_uses_jsvalue(field) {
let core_ty = typeref_to_core_type_str(&field.ty, core_import, source_crate_remaps, type_paths);
let decoded = if field.optional {
format!(
"val.{field_ident}.clone().map(|value| \
serde_wasm_bindgen::from_value::<{core_ty}>(value).unwrap_or_else(|error| \
wasm_bindgen::throw_val(wasm_bindgen::JsValue::from_str(&error.to_string()))))"
)
} else {
format!(
"serde_wasm_bindgen::from_value::<{core_ty}>(val.{field_ident}.clone().unwrap_or_else(|| \
wasm_bindgen::throw_str(\"missing enum field {field_ident}\"))).unwrap_or_else(|error| \
wasm_bindgen::throw_val(wasm_bindgen::JsValue::from_str(&error.to_string())))"
)
};
let converted = apply_wasm_explicit_field_newtype_to_core(&decoded, field).unwrap_or(decoded);
return box_explicit_newtype_expr(converted, field);
}
let field_ty = &field.ty;
let field_optional = field.optional;
let is_boxed = field.is_boxed;
if !field_optional
&& !matches!(field_ty, TypeRef::Optional(_))
&& field.newtype_wrapper.as_deref().is_some_and(is_explicit_newtype)
{
let required = format!(
"val.{field_ident}.clone().unwrap_or_else(|| \
wasm_bindgen::throw_str(\"missing enum field {field_ident}\"))"
);
let converted = apply_wasm_explicit_field_newtype_to_core(&required, field).unwrap_or(required);
return box_explicit_newtype_expr(converted, field);
}
if field_optional {
let base = match field_ty {
TypeRef::Named(_) => box_wrap_map_into(format!("val.{field_ident}.clone().map(Into::into)"), is_boxed),
TypeRef::Path => format!("val.{field_ident}.clone().map(Into::into)"),
TypeRef::Map(_, _) => {
format!("val.{field_ident}.clone().and_then(|v| serde_wasm_bindgen::from_value(v).ok())")
}
_ => format!("val.{field_ident}.clone()"),
};
let converted = apply_wasm_explicit_field_newtype_to_core(&base, field).unwrap_or(base);
return box_explicit_newtype_expr(converted, field);
}
let base = match field_ty {
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(_) => box_wrap_map_into(format!("val.{field_ident}.clone().map(Into::into)"), is_boxed),
TypeRef::Path => format!("val.{field_ident}.clone().map(Into::into)"),
TypeRef::Map(_, _) => {
format!("val.{field_ident}.clone().and_then(|v| serde_wasm_bindgen::from_value(v).ok())")
}
_ => format!("val.{field_ident}.clone()"),
},
TypeRef::Named(_) => {
let converted = box_wrap_map_into(format!("val.{field_ident}.clone().map(Into::into)"), is_boxed);
format!("{converted}.unwrap_or_else(|| wasm_bindgen::throw_str(\"missing enum field {field_ident}\"))")
}
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
format!("val.{field_ident}.clone().unwrap_or_default().into_iter().map(Into::into).collect()")
}
TypeRef::Path => format!("val.{field_ident}.clone().map(Into::into).unwrap_or_default()"),
TypeRef::Map(_, _) => format!(
"val.{field_ident}.clone().and_then(|v| serde_wasm_bindgen::from_value(v).ok()).unwrap_or_default()"
),
_ => format!("val.{field_ident}.clone().unwrap_or_default()"),
};
let converted = apply_wasm_explicit_field_newtype_to_core(&base, field).unwrap_or(base);
box_explicit_newtype_expr(converted, field)
}
fn box_unwrap_into(local: &str, is_boxed: bool) -> String {
if is_boxed {
format!("(*{local}).into()")
} else {
format!("{local}.into()")
}
}
fn box_unwrap_map_into(local: &str, is_boxed: bool) -> String {
if is_boxed {
format!("{local}.map(|v| (*v).into())")
} else {
format!("{local}.map(Into::into)")
}
}
fn tagged_enum_core_to_binding_expr(field: &FieldDef, field_ident: &str, local: &str) -> String {
let explicit_wrapper = field.newtype_wrapper.as_deref().is_some_and(is_explicit_newtype);
let source = if field.is_boxed && explicit_wrapper {
if field.optional || matches!(field.ty, TypeRef::Optional(_)) {
format!("{local}.map(|value| *value)")
} else {
format!("*{local}")
}
} else {
local.to_string()
};
let converted = apply_explicit_field_newtype_from_core(&source, field).unwrap_or(source);
if complex_newtype_field_uses_jsvalue(field) {
if field.optional || matches!(field.ty, TypeRef::Optional(_)) {
return format!(
" {field_ident}: \
{converted}.and_then(|value| serde_wasm_bindgen::to_value(&value).ok())"
);
}
return format!(" {field_ident}: serde_wasm_bindgen::to_value(&{converted}).ok()");
}
let field_ty = &field.ty;
let field_optional = field.optional;
let is_boxed = field.is_boxed && !explicit_wrapper;
if field_optional {
return match field_ty {
TypeRef::Named(_) => format!(
" {field_ident}: {}",
box_unwrap_map_into(&converted, is_boxed)
),
TypeRef::Path => {
format!(" {field_ident}: {converted}.map(|p| p.to_string_lossy().to_string())")
}
TypeRef::Map(_, _) => {
format!(
" {field_ident}: \
{converted}.as_ref().and_then(|m| serde_wasm_bindgen::to_value(m).ok())"
)
}
_ => format!(" {}", struct_field_init(field_ident, &converted)),
};
}
match field_ty {
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(_) => format!(
" {field_ident}: {}",
box_unwrap_map_into(&converted, is_boxed)
),
TypeRef::Path => {
format!(" {field_ident}: {converted}.map(|p| p.to_string_lossy().to_string())")
}
TypeRef::Map(_, _) => {
format!(
" {field_ident}: \
{converted}.as_ref().and_then(|m| serde_wasm_bindgen::to_value(m).ok())"
)
}
_ => format!(" {}", struct_field_init(field_ident, &converted)),
},
TypeRef::Named(_) => format!(
" {field_ident}: Some({})",
box_unwrap_into(&converted, is_boxed)
),
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
format!(" {field_ident}: Some({converted}.into_iter().map(Into::into).collect())")
}
TypeRef::Path => format!(" {field_ident}: Some({converted}.to_string_lossy().to_string())"),
TypeRef::Map(_, _) => format!(" {field_ident}: serde_wasm_bindgen::to_value(&{converted}).ok()"),
_ => format!(" {field_ident}: Some({converted})"),
}
}
pub(super) fn variant_tag_value(
variant_name: &str,
serde_rename: Option<&str>,
serde_rename_all: Option<&str>,
) -> String {
wire_variant_value(variant_name, serde_rename, serde_rename_all)
}
fn flattened_only_field_names<'a>(enum_def: &'a EnumDef, types: &[TypeDef]) -> std::collections::BTreeSet<&'a str> {
let mut flattened: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut plain: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
for variant in &enum_def.variants {
let target = if crate::codegen::serde_enum_repr::serde_flattens_newtype_payload(enum_def, variant, types) {
&mut flattened
} else {
&mut plain
};
target.extend(variant.fields.iter().map(|field| field.name.as_str()));
}
&flattened - &plain
}
pub(super) fn gen_tagged_enum_as_struct(
enum_def: &EnumDef,
mapper: &WasmMapper,
types: &[TypeDef],
exclude_types: &[String],
) -> String {
let prefix = mapper.prefix.as_str();
let js_name = format!("{prefix}{}", enum_def.name);
let tag_field = crate::codegen::serde_enum_repr::tagged_object_tag_key(enum_def);
let tag_field_ident = escape_rust_keyword(tag_field);
let tag_js_name = to_node_name(tag_field);
let class_type_names: AHashSet<String> = types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
.map(|t| t.name.clone())
.collect();
let mut lines = vec![];
let doc = emit_rustdoc(&enum_def.doc);
if !doc.is_empty() {
lines.push(doc);
}
lines.push("#[wasm_bindgen]".to_string());
lines.push("#[derive(Clone, Default)]".to_string());
lines.push(format!("pub struct {js_name} {{"));
lines.push(format!(" pub(crate) {tag_field_ident}: String,"));
let mixed = mixed_type_fields(enum_def);
let tuple_vec_fields: std::collections::BTreeSet<String> = enum_def
.variants
.iter()
.flat_map(|v| v.fields.iter())
.filter(|f| is_sanitized_tuple_vec(f) || is_sanitized_fixed_tuple_array(f))
.map(|f| f.name.clone())
.collect();
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut field_entries: Vec<PayloadField> = Vec::new();
for variant in &enum_def.variants {
for field in &variant.fields {
if !seen.insert(field.name.clone()) {
continue;
}
let degraded = mixed.contains(&field.name)
|| tuple_vec_fields.contains(&field.name)
|| complex_newtype_field_uses_jsvalue(field);
let field_ty = if degraded {
"Option<JsValue>".to_string()
} else {
let mapped = mapper.map_type(&field.ty);
if matches!(&field.ty, TypeRef::Optional(_)) {
mapped
} else {
format!("Option<{mapped}>")
}
};
let class_type = if degraded {
None
} else {
class_backed_field_type(field, mapper, &class_type_names)
};
let escaped = escape_rust_keyword(&field.name);
lines.push(format!(" pub(crate) {escaped}: {field_ty},"));
field_entries.push(PayloadField {
field: field.clone(),
binding_type: field_ty,
class_type,
});
}
}
lines.push("}".to_string());
lines.push(String::new());
lines.push("#[wasm_bindgen]".to_string());
lines.push(format!("impl {js_name} {{"));
lines.push(" #[wasm_bindgen(constructor)]".to_string());
lines.push(format!(" pub fn new() -> {js_name} {{ Self::default() }}"));
lines.push(String::new());
lines.push(" #[wasm_bindgen]".to_string());
lines.push(" #[allow(clippy::should_implement_trait)]".to_string());
lines.push(format!(
" pub fn default() -> {js_name} {{ <Self as ::core::default::Default>::default() }}"
));
lines.push(String::new());
lines.push(format!(" #[wasm_bindgen(getter, js_name = \"{tag_js_name}\")]"));
lines.push(format!(
" pub fn {tag_field_ident}(&self) -> String {{ self.{tag_field_ident}.clone() }}"
));
let setter_ident = format!("set_{tag_field}");
let setter_ident_escaped = escape_rust_keyword(&setter_ident);
lines.push(format!(" #[wasm_bindgen(setter, js_name = \"{tag_js_name}\")]"));
lines.push(format!(
" pub fn {setter_ident_escaped}(&mut self, value: String) {{ self.{tag_field_ident} = value; }}"
));
let flattened_only = flattened_only_field_names(enum_def, types);
let reserved_idents =
enums_accessors::reserved_payload_idents(&tag_field_ident, &setter_ident_escaped, &field_entries);
lines.extend(enums_accessors::gen_payload_accessors(
&field_entries,
&flattened_only,
&reserved_idents,
));
lines.push("}".to_string());
lines.join("\n")
}
fn path_crate_segment(path: &str) -> &str {
path.split("::").next().unwrap_or("")
}
fn is_external_crate_type(rust_path: &str, core_import: &str) -> bool {
let crate_seg = path_crate_segment(rust_path);
let core_seg = core_import.replace('-', "_");
crate_seg.replace('-', "_") != core_seg
}
fn mixed_field_binding_to_core_expr(field: &FieldDef, field_ident: &str, core_import: &str) -> String {
let is_external = field
.type_rust_path
.as_deref()
.is_some_and(|path| is_external_crate_type(path, core_import));
let expr = if is_external {
"Default::default()".to_string()
} else {
let core_inner = match field.type_rust_path.as_deref() {
Some(path) => path.replace('-', "_"),
None => match &field.ty {
TypeRef::Named(n) => format!("{core_import}::{n}"),
_ => "serde_json::Value".to_string(),
},
};
format!(
"val.{field_ident}.as_ref().and_then(|v| serde_wasm_bindgen::from_value::<{core_inner}>(v.clone()).ok()).unwrap_or_default()"
)
};
if field.is_boxed {
format!("Box::new({expr})")
} else {
expr
}
}
pub(crate) fn gen_enum(
enum_def: &EnumDef,
mapper: &WasmMapper,
core_import: &str,
configured_features: &std::collections::HashSet<&str>,
types: &[TypeDef],
exclude_types: &[String],
) -> String {
let prefix = mapper.prefix.as_str();
if is_tagged_data_enum(enum_def, types) {
return gen_tagged_enum_as_struct(enum_def, mapper, types, exclude_types);
}
let js_name = format!("{prefix}{}", enum_def.name);
let mut lines = vec![];
let doc = emit_rustdoc(&enum_def.doc);
if !doc.is_empty() {
lines.push(doc);
}
lines.extend([
"#[wasm_bindgen]".to_string(),
"#[derive(Clone, Copy, PartialEq, Eq)]".to_string(),
format!("pub enum {} {{", js_name),
]);
let is_host_enum = is_host_owned_rust_path(core_import, &enum_def.rust_path);
let declared_variants: Vec<&EnumVariant> = enum_def
.variants
.iter()
.filter(|variant| {
matches!(
crate::codegen::conversions::enum_variant_declaration_without_cfg_attribute(
variant,
is_host_enum,
configured_features,
),
crate::codegen::conversions::VariantDeclaration::Keep { .. }
)
})
.collect();
for (idx, variant) in declared_variants.iter().enumerate() {
lines.push(format!(" {} = {},", variant.name, idx));
}
lines.push("}".to_string());
let default_variant = declared_variants
.iter()
.find(|v| v.is_default)
.or_else(|| declared_variants.first());
if let Some(dv) = default_variant {
lines.push(String::new());
lines.push("#[allow(clippy::derivable_impls)]".to_string());
lines.push(format!("impl Default for {} {{", js_name));
lines.push(format!(" fn default() -> Self {{ Self::{} }}", dv.name));
lines.push("}".to_string());
}
if !declared_variants.is_empty() {
lines.push(String::new());
lines.push(format!("impl {} {{", js_name));
lines.push(
" /// Returns the serde wire string for this variant (e.g. `\"stop\"`, `\"tool_calls\"`).".to_string(),
);
lines.push(" pub fn to_api_str(self) -> &'static str {".to_string());
lines.push(" match self {".to_string());
for variant in &declared_variants {
let wire = wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
);
lines.push(format!(" Self::{} => \"{}\",", variant.name, wire));
}
lines.push(" }".to_string());
lines.push(" }".to_string());
lines.push(String::new());
lines.push(
" /// Parses a serde wire string and returns the corresponding variant, or None if unrecognized."
.to_string(),
);
lines.push(" pub fn from_api_str(s: &str) -> Option<Self> {".to_string());
lines.push(" match s {".to_string());
for variant in &declared_variants {
let wire = wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
);
lines.push(format!(" \"{}\" => Some(Self::{}),", wire, variant.name));
}
lines.push(" _ => None,".to_string());
lines.push(" }".to_string());
lines.push(" }".to_string());
lines.push("}".to_string());
}
lines.join("\n")
}
#[cfg(test)]
mod cfg_gate_tests;
#[cfg(test)]
mod tagged_enum_tests;
#[cfg(test)]
mod tests;