use crate::backends::napi::type_map::NapiMapper;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeRef};
pub(super) fn tagged_enum_field_is_tuple(field: &FieldDef) -> bool {
field
.name
.strip_prefix('_')
.is_some_and(|s| s.chars().all(|c| c.is_ascii_digit()))
}
pub(super) fn tagged_enum_field_name(variant: &EnumVariant, field: &FieldDef) -> String {
if let Some(index) = field
.name
.strip_prefix('_')
.filter(|s| s.chars().all(|c| c.is_ascii_digit()))
{
if variant.fields.len() == 1 {
let source_name = field
.serde_rename
.as_deref()
.or(variant.serde_rename.as_deref())
.unwrap_or(&variant.name);
return crate::codegen::naming::to_python_name(source_name);
}
return format!("field_{index}");
}
field.name.clone()
}
pub(super) fn tagged_enum_field_js_name(variant: &EnumVariant, field: &FieldDef) -> String {
if let Some(index) = field
.name
.strip_prefix('_')
.filter(|s| s.chars().all(|c| c.is_ascii_digit()))
{
if variant.fields.len() == 1 {
return field
.serde_rename
.clone()
.or_else(|| variant.serde_rename.clone())
.unwrap_or_else(|| crate::codegen::naming::to_node_name(&variant.name));
}
return format!("field{index}");
}
crate::codegen::naming::to_node_name(&field.name)
}
pub(super) fn tagged_enum_binding_field_name(enum_def: &EnumDef, variant: &EnumVariant, field: &FieldDef) -> String {
if enum_def.serde_content.is_some() && variant.fields.len() == 1 && tagged_enum_field_is_tuple(field) {
return crate::codegen::naming::to_python_name(
enum_def.serde_content.as_deref().expect("adjacent content is present"),
);
}
tagged_enum_field_name(variant, field)
}
pub(super) fn tagged_enum_binding_field_js_name(enum_def: &EnumDef, variant: &EnumVariant, field: &FieldDef) -> String {
if enum_def.serde_content.is_some() && variant.fields.len() == 1 && tagged_enum_field_is_tuple(field) {
return enum_def.serde_content.clone().expect("adjacent content is present");
}
tagged_enum_field_js_name(variant, field)
}
pub(super) fn variant_data_field_names(enum_def: &EnumDef) -> Vec<String> {
let mut names = Vec::new();
for v in &enum_def.variants {
if v.fields.len() != 1 {
continue;
}
let field = &v.fields[0];
if !tagged_enum_field_is_tuple(field) {
continue;
}
if matches!(&field.ty, TypeRef::Named(_)) {
names.push(tagged_enum_binding_field_name(enum_def, v, field));
}
}
names
}
fn napi_string_enum_case(enum_def: &EnumDef) -> Option<&'static str> {
enum_def.serde_rename_all.as_deref().and_then(|s| match s {
"snake_case" => Some("snake_case"),
"camelCase" => Some("camelCase"),
"kebab-case" => Some("kebab-case"),
"SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
"lowercase" => Some("lowercase"),
"UPPERCASE" => Some("UPPERCASE"),
"PascalCase" => Some("PascalCase"),
_ => None,
})
}
pub(super) fn declared_string_enum_variants<'a>(
enum_def: &'a EnumDef,
is_host_enum: bool,
configured_features: Option<&std::collections::HashSet<&str>>,
) -> Option<Vec<(&'a EnumVariant, String)>> {
let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
if enum_def.serde_tag.is_some() {
return None;
}
if enum_def.serde_untagged && has_data_variants {
return None;
}
if has_data_variants || enum_def.variants.is_empty() {
return None;
}
let case = napi_string_enum_case(enum_def);
Some(
enum_def
.variants
.iter()
.filter(|variant| {
!matches!(
crate::codegen::conversions::enum_variant_declaration(variant, is_host_enum, configured_features),
crate::codegen::conversions::VariantDeclaration::Drop
)
})
.map(|variant| {
let value = match variant.serde_rename.as_deref() {
Some(rename) => rename.to_string(),
None => apply_napi_case(&variant.name, case),
};
(variant, value)
})
.collect(),
)
}
pub(super) fn string_enum_js_values(
enum_def: &EnumDef,
is_host_enum: bool,
configured_features: Option<&std::collections::HashSet<&str>>,
) -> Option<Vec<String>> {
declared_string_enum_variants(enum_def, is_host_enum, configured_features)
.map(|declared| declared.into_iter().map(|(_, value)| value).collect())
}
fn apply_napi_case(name: &str, case: Option<&str>) -> String {
use convert_case::Casing;
let Some(case) = case.and_then(napi_convert_case) else {
return name.to_string();
};
name.trim_start_matches('_').to_case(case)
}
fn napi_convert_case(case: &str) -> Option<convert_case::Case<'static>> {
use convert_case::Case;
match case {
"snake_case" => Some(Case::Snake),
"camelCase" => Some(Case::Camel),
"kebab-case" => Some(Case::Kebab),
"UPPER_SNAKE" => Some(Case::UpperSnake),
"lowercase" => Some(Case::Flat),
"UPPERCASE" => Some(Case::UpperFlat),
"PascalCase" => Some(Case::Pascal),
_ => None,
}
}
pub(super) fn gen_enum(
enum_def: &EnumDef,
prefix: &str,
has_serde: bool,
core_import: &str,
configured_features: Option<&std::collections::HashSet<&str>>,
) -> String {
let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
let is_tagged_data_enum = enum_def.serde_tag.is_some() || (has_data_variants && !enum_def.serde_untagged);
let is_untagged_data_enum = enum_def.serde_untagged && has_data_variants;
if is_tagged_data_enum {
return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
}
if is_untagged_data_enum {
return gen_untagged_data_enum_as_value_wrapper(enum_def, prefix);
}
let napi_case = napi_string_enum_case(enum_def);
let js_name = &enum_def.name;
let string_enum_attr = match napi_case {
Some(case) => format!("#[napi(string_enum = \"{case}\", js_name = \"{js_name}\")]"),
None => format!("#[napi(string_enum, js_name = \"{js_name}\")]"),
};
let derives = if has_serde {
"#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
} else {
"#[derive(Clone)]".to_string()
};
let mut enum_doc = String::new();
let sanitized_enum_doc = crate::codegen::doc_emission::sanitize_rust_idioms(
&enum_def.doc,
crate::codegen::doc_emission::DocTarget::TsDoc,
);
crate::codegen::doc_emission::emit_rustdoc(&mut enum_doc, &sanitized_enum_doc, "");
let mut lines: Vec<String> = Vec::new();
if !enum_doc.is_empty() {
lines.push(enum_doc.trim_end_matches('\n').to_string());
}
lines.push(string_enum_attr);
lines.push(derives);
lines.push(format!("pub enum {prefix}{} {{", enum_def.name));
let is_host_enum = crate::codegen::cfg::is_host_owned_rust_path(core_import, &enum_def.rust_path);
let declared_variants: Vec<(&EnumVariant, Option<String>)> = enum_def
.variants
.iter()
.filter_map(|variant| {
match crate::codegen::conversions::enum_variant_declaration(variant, is_host_enum, configured_features) {
crate::codegen::conversions::VariantDeclaration::Keep { cfg } => Some((variant, cfg)),
crate::codegen::conversions::VariantDeclaration::Drop => None,
}
})
.collect();
for (variant, cfg) in &declared_variants {
let mut variant_doc = String::new();
let sanitized_variant_doc = crate::codegen::doc_emission::sanitize_rust_idioms(
&variant.doc,
crate::codegen::doc_emission::DocTarget::TsDoc,
);
let escaped_variant_doc = sanitized_variant_doc.replace("*/", "* /");
crate::codegen::doc_emission::emit_rustdoc(&mut variant_doc, &escaped_variant_doc, " ");
if !variant_doc.is_empty() {
lines.push(variant_doc.trim_end_matches('\n').to_string());
}
if let Some(cfg) = cfg {
lines.push(format!(" #[cfg({cfg})]"));
}
if let Some(rename) = variant.serde_rename.as_deref() {
lines.push(format!(" #[napi(value = \"{rename}\")]"));
}
lines.push(format!(" {},", variant.name));
}
lines.push("}".to_string());
if !declared_variants.is_empty() {
let candidates = default_impl_cfg_cascade(&declared_variants);
lines.push(String::new());
lines.push(
crate::backends::napi::template_env::render(
"enum_default_impl_cascade.jinja",
minijinja::context! {
binding_name => format!("{prefix}{}", enum_def.name),
candidates,
},
)
.trim_end()
.to_string(),
);
}
lines.join("\n")
}
fn default_impl_cfg_cascade(declared_variants: &[(&EnumVariant, Option<String>)]) -> Vec<minijinja::Value> {
let mut candidates = Vec::new();
let mut prior_cfgs: Vec<String> = Vec::new();
for (variant, cfg) in declared_variants {
let candidate_cfg = match cfg {
None if prior_cfgs.is_empty() => None,
None => Some(format!("not(any({}))", prior_cfgs.join(", "))),
Some(c) if prior_cfgs.is_empty() => Some(c.clone()),
Some(c) => Some(format!("all({c}, not(any({})))", prior_cfgs.join(", "))),
};
candidates.push(minijinja::context! {
variant_name => variant.name.clone(),
cfg => candidate_cfg,
});
let is_unconditional = cfg.is_none();
if let Some(c) = cfg {
prior_cfgs.push(c.clone());
}
if is_unconditional {
break;
}
}
candidates
}
pub(super) fn gen_untagged_data_enum_as_value_wrapper(enum_def: &EnumDef, prefix: &str) -> String {
let name = format!("{prefix}{}", enum_def.name);
format!(
"#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n\
#[serde(transparent)]\n\
pub struct {name}(pub serde_json::Value);\n\
\n\
impl napi::bindgen_prelude::TypeName for {name} {{\n \
fn type_name() -> &'static str {{ \"{name}\" }}\n \
fn value_type() -> napi::ValueType {{ napi::ValueType::Unknown }}\n\
}}\n\
\n\
impl napi::bindgen_prelude::FromNapiValue for {name} {{\n \
unsafe fn from_napi_value(env: napi::sys::napi_env, val: napi::sys::napi_value) -> napi::Result<Self> {{\n \
let v: serde_json::Value = unsafe {{ napi::bindgen_prelude::FromNapiValue::from_napi_value(env, val)? }};\n \
Ok(Self(v))\n \
}}\n\
}}\n\
\n\
impl napi::bindgen_prelude::ToNapiValue for {name} {{\n \
unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result<napi::sys::napi_value> {{\n \
unsafe {{ napi::bindgen_prelude::ToNapiValue::to_napi_value(env, val.0) }}\n \
}}\n\
}}\n\
\n\
impl napi::bindgen_prelude::ValidateNapiValue for {name} {{}}\n"
)
}
pub(super) fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
use crate::codegen::type_mapper::TypeMapper;
let mapper = NapiMapper::new(prefix.to_string());
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let ts_discriminant = tag_field;
let derive = if has_serde {
"#[derive(Clone, serde::Serialize, serde::Deserialize)]"
} else {
"#[derive(Clone)]"
};
let js_name = &enum_def.name;
let mut lines: Vec<String> = Vec::new();
let mut enum_doc = String::new();
let sanitized_enum_doc = crate::codegen::doc_emission::sanitize_rust_idioms(
&enum_def.doc,
crate::codegen::doc_emission::DocTarget::TsDoc,
);
crate::codegen::doc_emission::emit_rustdoc(&mut enum_doc, &sanitized_enum_doc, "");
if !enum_doc.is_empty() {
lines.push(enum_doc.trim_end_matches('\n').to_string());
}
lines.push(derive.to_string());
lines.push(format!("#[napi(object, js_name = \"{js_name}\")]"));
lines.push(format!("pub struct {prefix}{} {{", enum_def.name));
lines.push(format!(" #[napi(js_name = \"{ts_discriminant}\")]"));
if has_serde {
lines.push(format!(" #[serde(rename = \"{ts_discriminant}\")]"));
}
lines.push(format!(" pub {tag_field}_tag: String,"));
let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for variant in &enum_def.variants {
for field in &variant.fields {
if tagged_enum_field_is_tuple(field) && matches!(&field.ty, TypeRef::Named(_)) {
continue;
}
let field_name = tagged_enum_binding_field_name(enum_def, variant, field);
if seen_fields.insert(field_name.clone()) {
let field_type = if (field.sanitized || mixed_named_fields.contains(&field_name))
&& matches!(&field.ty, TypeRef::Named(_))
{
"String".to_string()
} else {
mapper.map_type(&field.ty).to_string()
};
let js_name = tagged_enum_binding_field_js_name(enum_def, variant, field);
if js_name != field_name {
lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
if has_serde {
lines.push(format!(" #[serde(rename = \"{js_name}\")]"));
}
}
lines.push(format!(" pub {field_name}: Option<{field_type}>,"));
}
}
}
enum_def.variants.iter().for_each(|v| {
if v.fields.len() != 1 {
return;
}
let field = &v.fields[0];
if !tagged_enum_field_is_tuple(field) {
return;
}
if let TypeRef::Named(inner_type_name) = &field.ty {
let field_name = tagged_enum_binding_field_name(enum_def, v, field);
let binding_type = format!("{prefix}{inner_type_name}");
let js_name = tagged_enum_binding_field_js_name(enum_def, v, field);
if js_name != field_name {
lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
if has_serde {
lines.push(format!(" #[serde(rename = \"{js_name}\")]"));
}
}
lines.push(format!(" pub {field_name}: Option<{binding_type}>,"));
}
});
lines.push("}".to_string());
let synth_fields = variant_data_field_names(enum_def);
let default_inits: Vec<String> = seen_fields
.iter()
.cloned()
.chain(synth_fields.iter().cloned())
.map(|f| format!("{f}: None"))
.collect();
lines.push(String::new());
lines.push("#[allow(clippy::derivable_impls)]".to_string());
lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
lines.push(format!(
" fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
default_inits.join(", ")
));
lines.push("}".to_string());
let _tuple_named_variants: Vec<(&crate::core::ir::EnumVariant, &str)> = enum_def
.variants
.iter()
.filter_map(|v| {
if v.fields.len() != 1 {
return None;
}
let field = &v.fields[0];
let is_tuple = field
.name
.strip_prefix('_')
.is_some_and(|s| s.chars().all(|c| c.is_ascii_digit()));
if !is_tuple {
return None;
}
if let TypeRef::Named(inner_type_name) = &field.ty {
Some((v, inner_type_name.as_str()))
} else {
None
}
})
.collect();
if enum_def.serde_content.is_some() {
let total_field_count = 1 + seen_fields.len() + synth_fields.iter().collect::<ahash::AHashSet<_>>().len();
let variants: Vec<minijinja::Value> = enum_def
.variants
.iter()
.map(|variant| {
let wire_value = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
);
let payload_type = variant
.fields
.first()
.map(|field| mapper.map_type(&field.ty).to_string());
let has_payload = payload_type.is_some();
let rust_name = crate::codegen::naming::internal_rust_identifier(&format!(
"{}_{}",
crate::codegen::naming::pascal_to_snake(&enum_def.name),
crate::codegen::naming::to_python_name(&wire_value),
));
let fields_set = if has_payload { 2 } else { 1 };
minijinja::context! {
variant_name => variant.name.clone(),
rust_name,
wire_value,
payload_type,
has_payload,
needs_default_spread => fields_set < total_field_count,
}
})
.collect();
lines.push(String::new());
lines.push(
crate::backends::napi::template_env::render(
"adjacent_enum_namespace.rs.jinja",
minijinja::context! {
enum_name => enum_def.name.clone(),
binding_name => format!("{prefix}{}", enum_def.name),
tag_field => format!("{tag_field}_tag"),
content_field => crate::codegen::naming::to_python_name(
enum_def.serde_content.as_deref().expect("adjacent content is present"),
),
variants,
},
)
.trim_end()
.to_string(),
);
}
lines.join("\n")
}
pub(super) fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
use crate::core::ir::TypeRef;
let mut field_types: std::collections::HashMap<&str, ahash::AHashSet<&str>> = std::collections::HashMap::new();
for variant in &enum_def.variants {
for field in &variant.fields {
if field.sanitized {
continue;
}
if let TypeRef::Named(n) = &field.ty {
field_types.entry(&field.name).or_default().insert(n.as_str());
}
}
}
field_types
.into_iter()
.filter(|(_, types)| types.len() > 1)
.map(|(name, _)| name.to_string())
.collect()
}
pub(super) fn tagged_enum_binding_struct_fields<'a>(
enum_def: &'a EnumDef,
struct_names: &ahash::AHashSet<String>,
) -> ahash::AHashSet<&'a str> {
use crate::core::ir::TypeRef;
let mut field_types: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
let mut sanitized_fields: ahash::AHashSet<&str> = ahash::AHashSet::new();
for variant in &enum_def.variants {
for field in &variant.fields {
if field.sanitized {
sanitized_fields.insert(&field.name);
}
if let TypeRef::Named(n) = &field.ty {
field_types.entry(&field.name).or_default().push(n);
}
}
}
let mut result = ahash::AHashSet::new();
for (field_name, types) in &field_types {
if sanitized_fields.contains(field_name) {
continue;
}
if types.iter().all(|t| *t == types[0]) && struct_names.contains(types[0]) {
result.insert(*field_name);
}
}
result
}
#[cfg(test)]
#[allow(clippy::print_stderr)] mod tests;
#[cfg(test)]
mod default_impl_cfg_tests;