use crate::type_map::NapiMapper;
use ahash::AHashSet;
use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
use alef_codegen::naming::to_node_name;
use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
use alef_codegen::type_mapper::TypeMapper;
use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
use alef_core::config::{AlefConfig, Language, resolve_output_dir};
use alef_core::hash::{self, CommentStyle};
use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
use std::path::PathBuf;
pub struct NapiBackend;
impl NapiBackend {
fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
RustBindingConfig {
struct_attrs: &["napi"],
field_attrs: &[],
struct_derives: &["Clone"],
method_block_attr: Some("napi"),
constructor_attr: "#[napi(constructor)]",
static_attr: None,
function_attr: "#[napi]",
enum_attrs: &["napi(string_enum)"],
enum_derives: &["Clone"],
needs_signature: false,
signature_prefix: "",
signature_suffix: "",
core_import,
async_pattern: AsyncPattern::NapiNativeAsync,
has_serde,
type_name_prefix: prefix,
option_duration_on_defaults: true,
opaque_type_names: &[],
}
}
}
impl Backend for NapiBackend {
fn name(&self) -> &str {
"napi"
}
fn language(&self) -> Language {
Language::Node
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
..Capabilities::default()
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let prefix = config.node_type_prefix();
let mapper = NapiMapper::new(prefix.clone());
let core_import = config.core_import();
let output_dir = resolve_output_dir(
config.output.node.as_ref(),
&config.crate_config.name,
"crates/{name}-node/src/",
);
let has_serde = alef_core::config::detect_serde_available(&output_dir);
let cfg = Self::binding_config(&core_import, &prefix, has_serde);
let mut builder = RustFileBuilder::new().with_generated_header();
builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
builder.add_import("napi::*");
builder.add_import("napi_derive::napi");
builder.add_import("serde_json");
for trait_path in generators::collect_trait_imports(api) {
builder.add_import(&trait_path);
}
let has_maps = api
.types
.iter()
.any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
|| api
.functions
.iter()
.any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
if has_maps {
builder.add_import("std::collections::HashMap");
}
let has_async =
api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
if has_async {
builder.add_item(&gen_tokio_runtime());
}
let opaque_types: AHashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque)
.map(|t| t.name.clone())
.collect();
if !opaque_types.is_empty() {
builder.add_import("std::sync::Arc");
}
let exclude_types: ahash::AHashSet<String> = config
.node
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
for adapter in &config.adapters {
match adapter.pattern {
alef_core::config::AdapterPattern::Streaming => {
let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
if let Some(struct_code) = adapter_bodies.get(&key) {
builder.add_item(struct_code);
}
}
alef_core::config::AdapterPattern::CallbackBridge => {
let struct_key = format!("{}.__bridge_struct__", adapter.name);
let impl_key = format!("{}.__bridge_impl__", adapter.name);
if let Some(struct_code) = adapter_bodies.get(&struct_key) {
builder.add_item(struct_code);
}
if let Some(impl_code) = adapter_bodies.get(&impl_key) {
builder.add_item(impl_code);
}
}
_ => {}
}
}
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
{
if typ.is_opaque {
builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
typ, &cfg, &prefix,
));
builder.add_item(&gen_opaque_struct_methods(
typ,
&mapper,
&cfg,
&opaque_types,
&prefix,
&adapter_bodies,
));
} else {
builder.add_item(&gen_struct(typ, &mapper, &prefix, has_serde));
}
}
let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
for enum_def in &api.enums {
builder.add_item(&gen_enum(enum_def, &prefix, has_serde));
}
let exclude_functions: ahash::AHashSet<String> = config
.node
.as_ref()
.map(|c| c.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
for func in &api.functions {
if exclude_functions.contains(&func.name) {
continue;
}
if func.sanitized {
continue;
}
let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
if let Some((param_idx, bridge_cfg)) = bridge_param {
builder.add_item(&crate::trait_bridge::gen_bridge_function(
func,
param_idx,
bridge_cfg,
&mapper,
&cfg,
&Default::default(),
&opaque_types,
&core_import,
));
} else {
builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types, &prefix));
}
}
for bridge_cfg in &config.trait_bridges {
if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
let bridge = crate::trait_bridge::gen_trait_bridge(
trait_type,
bridge_cfg,
&core_import,
&config.error_type(),
&config.error_constructor(),
api,
);
for imp in &bridge.imports {
builder.add_import(imp);
}
builder.add_item(&bridge.code);
}
}
let binding_to_core = alef_codegen::conversions::convertible_types(api);
let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
let input_types = alef_codegen::conversions::input_type_names(api);
let napi_conv_config = alef_codegen::conversions::ConversionConfig {
type_name_prefix: &prefix,
cast_large_ints_to_i64: true,
cast_f32_to_f64: true,
optionalize_defaults: true,
option_duration_on_defaults: true,
include_cfg_metadata: true,
..Default::default()
};
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if input_types.contains(&typ.name)
&& alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
{
builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
typ,
&core_import,
&napi_conv_config,
));
}
if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
typ,
&core_import,
&opaque_types,
&napi_conv_config,
));
}
}
for e in &api.enums {
let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
if is_tagged_data_enum {
builder.add_item(&gen_tagged_enum_binding_to_core(
e,
&core_import,
&prefix,
&struct_names,
));
builder.add_item(&gen_tagged_enum_core_to_binding(
e,
&core_import,
&prefix,
&struct_names,
));
} else {
if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
e,
&core_import,
&napi_conv_config,
));
}
if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
e,
&core_import,
&napi_conv_config,
));
}
}
}
for error in &api.errors {
builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
}
let content = builder.build();
let output_dir = resolve_output_dir(
config.output.node.as_ref(),
&config.crate_config.name,
"crates/{name}-node/src/",
);
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join("lib.rs"),
content,
generated_header: false,
}])
}
fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let prefix = config.node_type_prefix();
let mut type_exports = vec![];
let mut function_exports = vec![];
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
type_exports.push(format!("{prefix}{}", typ.name));
}
for enum_def in &api.enums {
type_exports.push(format!("{prefix}{}", enum_def.name));
}
for func in &api.functions {
let js_name = to_node_name(&func.name);
function_exports.push(js_name);
}
type_exports.sort();
function_exports.sort();
let mut lines = vec![
"// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
"".to_string(),
];
if !function_exports.is_empty() {
lines.push("export {".to_string());
for name in &function_exports {
lines.push(format!(" {name},"));
}
lines.push(format!("}} from '{}';", config.node_package_name()));
lines.push("".to_string());
}
if !type_exports.is_empty() {
lines.push("export type {".to_string());
for name in &type_exports {
lines.push(format!(" {name},"));
}
lines.push(format!("}} from '{}';", config.node_package_name()));
}
let custom_mods = config.custom_modules.for_language(Language::Node);
for module_name in custom_mods {
lines.push(format!("export * from './{module_name}';"));
}
let content = lines.join("\n");
let output_path = PathBuf::from("packages/typescript/src/index.ts");
Ok(vec![GeneratedFile {
path: output_path,
content,
generated_header: false,
}])
}
fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let prefix = config.node_type_prefix();
let content = gen_dts(api, &prefix);
let src_dir = resolve_output_dir(
config.output.node.as_ref(),
&config.crate_config.name,
"crates/{name}-node/src/",
);
let crate_root = {
let p = PathBuf::from(&src_dir);
match p.file_name().and_then(|n| n.to_str()) {
Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
_ => p,
}
};
Ok(vec![GeneratedFile {
path: crate_root.join("index.d.ts"),
content,
generated_header: false,
}])
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "napi",
crate_suffix: "-node",
build_dep: BuildDependency::None,
post_build: vec![PostBuildStep::PatchFile {
path: "index.d.ts",
find: "export declare const enum",
replace: "export declare enum",
}],
})
}
}
fn gen_struct(typ: &TypeDef, mapper: &NapiMapper, prefix: &str, has_serde: bool) -> String {
let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
struct_builder.add_attr("napi(object)");
struct_builder.add_derive("Clone");
struct_builder.add_derive("Default");
if has_serde {
struct_builder.add_derive("serde::Serialize");
struct_builder.add_derive("serde::Deserialize");
}
for field in &typ.fields {
let mapped_type = mapper.map_type(&field.ty);
let field_type = if (field.optional || typ.has_default) && !matches!(field.ty, TypeRef::Optional(_)) {
format!("Option<{}>", mapped_type)
} else {
mapped_type
};
let js_name = to_node_name(&field.name);
let attrs = if js_name != field.name {
vec![format!("napi(js_name = \"{}\")", js_name)]
} else {
vec![]
};
struct_builder.add_field(&field.name, &field_type, attrs);
}
struct_builder.build()
}
fn gen_opaque_struct_methods(
typ: &TypeDef,
mapper: &NapiMapper,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
prefix: &str,
adapter_bodies: &alef_adapters::AdapterBodies,
) -> String {
let mut impl_builder = ImplBuilder::new(&format!("{prefix}{}", typ.name));
impl_builder.add_attr("napi");
let (instance, statics) = partition_methods(&typ.methods);
for method in &instance {
let adapter_key = format!("{}.{}", typ.name, method.name);
if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
continue;
}
impl_builder.add_method(&gen_opaque_instance_method(
method,
mapper,
typ,
cfg,
opaque_types,
prefix,
adapter_bodies,
));
}
for method in &statics {
let adapter_key = format!("{}.{}", typ.name, method.name);
if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
continue;
}
impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types, prefix));
}
impl_builder.build()
}
fn gen_opaque_instance_method(
method: &MethodDef,
mapper: &NapiMapper,
typ: &TypeDef,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
prefix: &str,
adapter_bodies: &alef_adapters::AdapterBodies,
) -> String {
let params = function_params(&method.params, &|ty| mapper.map_type(ty));
let return_type = mapper.map_type(&method.return_type);
let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
let js_name = to_node_name(&method.name);
let js_name_attr = if js_name != method.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let async_kw = if method.is_async { "async " } else { "" };
let type_name = &typ.name;
let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
let call_args = napi_gen_call_args(&method.params, opaque_types);
let opaque_can_delegate = !method.sanitized
&& !is_ref_mut_receiver
&& (!is_owned_receiver || typ.is_clone)
&& method
.params
.iter()
.all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
&& alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
let async_result_wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
prefix,
);
let adapter_key = format!("{type_name}.{}", method.name);
let body = if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
adapter_body.clone()
} else if !opaque_can_delegate {
if cfg.has_serde
&& !method.sanitized
&& generators::has_named_params(&method.params, opaque_types)
&& method.error_type.is_some()
&& alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
{
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let serde_bindings =
generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, " ");
let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
let core_call = format!("self.inner.{}({serde_call_args})", method.name);
if matches!(method.return_type, TypeRef::Unit) {
format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
} else {
let wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
prefix,
);
format!("{serde_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
}
} else {
generators::gen_unimplemented_body(
&method.return_type,
&format!("{type_name}.{}", method.name),
method.error_type.is_some(),
cfg,
&method.params,
opaque_types,
)
}
} else if method.is_async {
let inner_clone_line = "let inner = self.inner.clone();\n ";
let core_call_str = make_async_core_call(&method.name);
generators::gen_async_body(
&core_call_str,
cfg,
method.error_type.is_some(),
&async_result_wrap,
true,
inner_clone_line,
matches!(method.return_type, TypeRef::Unit),
Some(&return_type),
)
} else {
let use_let_bindings = generators::has_named_params(&method.params, opaque_types);
let (let_bindings, call_args_for_call) = if use_let_bindings {
let bindings = generators::gen_named_let_bindings_pub(&method.params, opaque_types, cfg.core_import);
let args = napi_apply_primitive_casts_to_call_args(
&generators::gen_call_args_with_let_bindings(&method.params, opaque_types),
&method.params,
);
(bindings, args)
} else {
(String::new(), napi_gen_call_args(&method.params, opaque_types))
};
let core_call = if is_owned_receiver {
format!("(*self.inner).clone().{}({})", method.name, call_args_for_call)
} else {
format!("self.inner.{}({})", method.name, call_args_for_call)
};
if method.error_type.is_some() {
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
if matches!(method.return_type, TypeRef::Unit) {
format!("{let_bindings}{core_call}{err_conv}?;\n Ok(())")
} else {
let wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
prefix,
);
format!("{let_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
}
} else {
format!(
"{let_bindings}{}",
napi_wrap_return(
&core_call,
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
prefix,
)
)
}
};
let mut attrs = String::new();
if method.params.len() + 1 > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if method.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
if generators::is_trait_method_name(&method.name) {
attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
}
format!(
"{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n \
{body}\n}}",
method.name
)
}
fn gen_static_method(
method: &MethodDef,
mapper: &NapiMapper,
typ: &TypeDef,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
prefix: &str,
) -> String {
let params = function_params(&method.params, &|ty| mapper.map_type(ty));
let return_type = mapper.map_type(&method.return_type);
let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
let js_name = to_node_name(&method.name);
let js_name_attr = if js_name != method.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let type_name = &typ.name;
let core_type_path = typ.rust_path.replace('-', "_");
let call_args = napi_gen_call_args(&method.params, opaque_types);
let can_delegate_static = can_auto_delegate(method, opaque_types);
let async_kw = if method.is_async { "async " } else { "" };
let body = if !can_delegate_static {
generators::gen_unimplemented_body(
&method.return_type,
&format!("{type_name}::{}", method.name),
method.error_type.is_some(),
cfg,
&method.params,
opaque_types,
)
} else if method.is_async {
let core_call = format!("{core_type_path}::{}({call_args})", method.name);
let return_wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
typ.is_opaque,
method.returns_ref,
prefix,
);
generators::gen_async_body(
&core_call,
cfg,
method.error_type.is_some(),
&return_wrap,
false,
"",
matches!(method.return_type, TypeRef::Unit),
Some(&return_type),
)
} else {
let core_call = format!("{core_type_path}::{}({call_args})", method.name);
if method.error_type.is_some() {
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let wrapped = napi_wrap_return(
"val",
&method.return_type,
type_name,
opaque_types,
typ.is_opaque,
method.returns_ref,
prefix,
);
if wrapped == "val" {
format!("{core_call}{err_conv}")
} else {
format!("{core_call}.map(|val| {wrapped}){err_conv}")
}
} else {
napi_wrap_return(
&core_call,
&method.return_type,
type_name,
opaque_types,
typ.is_opaque,
method.returns_ref,
prefix,
)
}
};
let mut attrs = String::new();
if method.params.len() > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if method.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
if generators::is_trait_method_name(&method.name) {
attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
}
format!(
"{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
{body}\n}}",
method.name
)
}
fn gen_enum(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
if is_tagged_data_enum {
return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
}
let napi_case = 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,
});
let string_enum_attr = match napi_case {
Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
None => "#[napi(string_enum)]".to_string(),
};
let derives = if has_serde {
"#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
} else {
"#[derive(Clone)]".to_string()
};
let mut lines = vec![
string_enum_attr,
derives,
format!("pub enum {prefix}{} {{", enum_def.name),
];
for variant in &enum_def.variants {
lines.push(format!(" {},", variant.name));
}
lines.push("}".to_string());
if let Some(first) = enum_def.variants.first() {
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::{} }}", first.name));
lines.push("}".to_string());
}
lines.join("\n")
}
fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
use alef_codegen::type_mapper::TypeMapper;
let mapper = NapiMapper::new(prefix.to_string());
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let derive = if has_serde {
"#[derive(Clone, serde::Serialize, serde::Deserialize)]"
} else {
"#[derive(Clone)]"
};
let mut lines = vec![
derive.to_string(),
"#[napi(object)]".to_string(),
format!("pub struct {prefix}{} {{", enum_def.name),
format!(" #[napi(js_name = \"{tag_field}\")]"),
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 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 = alef_codegen::naming::to_node_name(&field.name);
if js_name != field.name {
lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
}
lines.push(format!(" pub {}: Option<{field_type}>,", field.name));
}
}
}
lines.push("}".to_string());
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(), {} }} }}",
seen_fields
.iter()
.map(|f| format!("{f}: None"))
.collect::<Vec<_>>()
.join(", ")
));
lines.push("}".to_string());
lines.join("\n")
}
fn gen_function(
func: &FunctionDef,
mapper: &NapiMapper,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
prefix: &str,
) -> String {
let params = function_params(&func.params, &|ty| {
if let TypeRef::Named(n) = ty {
if opaque_types.contains(n.as_str()) {
return format!("&{prefix}{n}");
}
}
mapper.map_type(ty)
});
let return_type = mapper.map_type(&func.return_type);
let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
let js_name = to_node_name(&func.name);
let js_name_attr = if js_name != func.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let core_import = cfg.core_import;
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
|| func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty));
let call_args = if use_let_bindings {
let base_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
} else {
napi_gen_call_args(&func.params, opaque_types)
};
let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let async_kw = if func.is_async { "async " } else { "" };
let body = if !can_delegate_fn {
if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
let serde_bindings =
generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
let vec_str_bindings: String = func.params.iter().filter(|p| {
p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
}).map(|p| {
format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n ", p.name, p.name)
}).collect();
let core_call = format!("{core_fn_path}({call_args})");
let await_kw = if func.is_async { ".await" } else { "" };
if matches!(func.return_type, TypeRef::Unit) {
format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n Ok(())")
} else {
let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
if wrapped == "val" {
format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
} else {
format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
}
}
} else {
generators::gen_unimplemented_body(
&func.return_type,
&func.name,
func.error_type.is_some(),
cfg,
&func.params,
opaque_types,
)
}
} else if func.is_async {
let mut let_bindings = if use_let_bindings {
generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
} else {
String::new()
};
let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
let core_call = format!("{core_fn_path}({call_args})");
let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref, prefix);
let return_type = mapper.map_type(&func.return_type);
generators::gen_async_body(
&core_call,
cfg,
func.error_type.is_some(),
&return_wrap,
false,
&let_bindings,
matches!(func.return_type, TypeRef::Unit),
Some(&return_type),
)
} else {
let core_call = format!("{core_fn_path}({call_args})");
let mut let_bindings = if use_let_bindings {
generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
} else {
String::new()
};
let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
if func.error_type.is_some() {
let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
if wrapped == "val" {
format!("{let_bindings}{core_call}{err_conv}")
} else {
format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
}
} else {
format!(
"{let_bindings}{}",
napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref, prefix)
)
}
};
let mut attrs = String::new();
if func.params.len() > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if func.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
format!(
"{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
{body}\n}}",
func.name
)
}
fn napi_apply_primitive_casts_to_call_args(generic_args: &str, params: &[ParamDef]) -> String {
let args_list: Vec<&str> = generic_args.split(',').map(|s| s.trim()).collect();
args_list
.iter()
.zip(params.iter())
.map(|(arg, p)| {
if needs_vec_f32_conversion(&p.ty) && p.is_ref {
return format!("&{}_f32", p.name);
}
match &p.ty {
TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
let core_ty = core_prim_str(prim);
if p.optional {
if arg.contains(".map(") || arg.contains(".as_") {
arg.to_string()
} else {
format!("{}.map(|v| v as {})", arg, core_ty)
}
} else {
format!("{} as {}", arg, core_ty)
}
}
_ => arg.to_string(),
}
})
.collect::<Vec<_>>()
.join(", ")
}
fn gen_vec_f32_conversion_bindings(params: &[ParamDef]) -> String {
let mut bindings = String::new();
for p in params {
if needs_vec_f32_conversion(&p.ty) && p.is_ref {
let conv_name = format!("{}_f32", p.name);
bindings.push_str(&format!(
" let {conv_name}: Vec<f32> = {}.iter().map(|&x| x as f32).collect();\n",
p.name
));
}
}
bindings
}
fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
params
.iter()
.map(|p| {
if needs_vec_f32_conversion(&p.ty) && p.is_ref {
return format!("&{}_f32", p.name);
}
match &p.ty {
TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
let core_ty = core_prim_str(prim);
if p.optional {
format!("{}.map(|v| v as {})", p.name, core_ty)
} else {
format!("{} as {}", p.name, core_ty)
}
}
TypeRef::Duration => {
if p.optional {
format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
} else {
format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
}
}
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if p.optional {
format!("{}.as_ref().map(|v| &v.inner)", p.name)
} else {
format!("&{}.inner", p.name)
}
}
TypeRef::Named(_) => {
if p.optional {
if p.is_ref {
format!("{}.as_ref()", p.name)
} else {
format!("{}.map(Into::into)", p.name)
}
} else {
format!("{}.into()", p.name)
}
}
TypeRef::String | TypeRef::Char => {
if p.optional {
if p.is_ref {
format!("{}.as_deref()", p.name)
} else {
p.name.clone()
}
} else if p.is_ref {
format!("&{}", p.name)
} else {
p.name.clone()
}
}
TypeRef::Path => {
if p.optional {
if p.is_ref {
format!("{}.as_deref().map(std::path::Path::new)", p.name)
} else {
format!("{}.map(std::path::PathBuf::from)", p.name)
}
} else if p.is_ref {
format!("std::path::Path::new(&{})", p.name)
} else {
format!("std::path::PathBuf::from({})", p.name)
}
}
TypeRef::Bytes => {
if p.optional {
if p.is_ref {
format!("{}.as_deref()", p.name)
} else {
p.name.clone()
}
} else if p.is_ref {
format!("&{}", p.name)
} else {
p.name.clone()
}
}
TypeRef::Vec(inner) => {
if p.optional {
if p.is_ref {
format!("{}.as_deref()", p.name)
} else {
p.name.clone()
}
} else if p.is_ref && matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) {
format!("&{}_refs", p.name)
} else if p.is_ref {
format!("&{}", p.name)
} else {
p.name.clone()
}
}
TypeRef::Map(_, _) => {
if p.optional {
if p.is_ref {
format!("{}.as_ref()", p.name)
} else {
p.name.clone()
}
} else if p.is_ref {
format!("&{}", p.name)
} else {
p.name.clone()
}
}
_ => p.name.clone(),
}
})
.collect::<Vec<_>>()
.join(", ")
}
fn napi_wrap_return(
expr: &str,
return_type: &TypeRef,
type_name: &str,
opaque_types: &AHashSet<String>,
self_is_opaque: bool,
returns_ref: bool,
prefix: &str,
) -> String {
match return_type {
TypeRef::Primitive(p) if needs_napi_cast(p) => {
format!("{expr} as i64")
}
TypeRef::Duration => format!("{expr}.as_millis() as i64"),
TypeRef::Named(n) if n == type_name && self_is_opaque => {
if returns_ref {
format!("Self {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("Self {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if returns_ref {
format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.clone().into()")
} else {
format!("{expr}.into()")
}
}
_ => generators::wrap_return(
expr,
return_type,
type_name,
opaque_types,
self_is_opaque,
returns_ref,
false,
),
}
}
fn napi_wrap_return_fn(
expr: &str,
return_type: &TypeRef,
opaque_types: &AHashSet<String>,
returns_ref: bool,
prefix: &str,
) -> String {
match return_type {
TypeRef::Primitive(p) if needs_napi_cast(p) => {
format!("{expr} as i64")
}
TypeRef::Duration => format!("{expr}.as_millis() as i64"),
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if returns_ref {
format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.clone().into()")
} else {
format!("{expr}.into()")
}
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.into()")
} else {
expr.to_string()
}
}
TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
TypeRef::Json => format!("{expr}.to_string()"),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if returns_ref {
format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
} else {
format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.map(|v| v.clone().into())")
} else {
format!("{expr}.map(Into::into)")
}
}
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
} else {
format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
}
}
_ => expr.to_string(),
},
TypeRef::Path => {
format!("{expr}.map(Into::into)")
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.map(Into::into)")
} else {
expr.to_string()
}
}
_ => expr.to_string(),
},
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Primitive(p) if needs_napi_cast(p) => {
let target_ty = match p {
alef_core::ir::PrimitiveType::F32 => "f64",
_ => "i64", };
format!("{expr}.into_iter().map(|v| v as {target_ty}).collect()")
}
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if returns_ref {
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
} else {
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
} else {
format!("{expr}.into_iter().map(Into::into).collect()")
}
}
TypeRef::Path => {
format!("{expr}.into_iter().map(Into::into).collect()")
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.into_iter().map(Into::into).collect()")
} else {
expr.to_string()
}
}
_ => expr.to_string(),
},
_ => expr.to_string(),
}
}
fn needs_vec_f32_conversion(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::F32)))
}
fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
matches!(
p,
alef_core::ir::PrimitiveType::U64
| alef_core::ir::PrimitiveType::Usize
| alef_core::ir::PrimitiveType::Isize
| alef_core::ir::PrimitiveType::F32
)
}
fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
match p {
alef_core::ir::PrimitiveType::U64 => "u64",
alef_core::ir::PrimitiveType::Usize => "usize",
alef_core::ir::PrimitiveType::Isize => "isize",
alef_core::ir::PrimitiveType::F32 => "f32",
_ => unreachable!(),
}
}
fn gen_tokio_runtime() -> String {
"static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect(\"Failed to create Tokio runtime\")
});"
.to_string()
}
fn gen_dts(api: &ApiSurface, prefix: &str) -> String {
let header = hash::header(CommentStyle::DoubleSlash);
let mut lines: Vec<String> = header.lines().map(|l| l.to_string()).collect();
lines.push("/* eslint-disable */".to_string());
let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
plain_types.sort_by(|a, b| a.name.cmp(&b.name));
let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
enum Decl<'a> {
Class(&'a TypeDef),
Interface(&'a TypeDef),
Enum(&'a EnumDef),
Function(&'a FunctionDef),
}
let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
for t in &opaque_types {
all_decls.push((format!("{prefix}{}", t.name), Decl::Class(t)));
}
for t in &plain_types {
all_decls.push((format!("{prefix}{}", t.name), Decl::Interface(t)));
}
for e in &sorted_enums {
all_decls.push((format!("{prefix}{}", e.name), Decl::Enum(e)));
}
for f in &sorted_fns {
all_decls.push((to_node_name(&f.name), Decl::Function(f)));
}
all_decls.sort_by_key(|a| a.0.to_lowercase());
for (_, decl) in &all_decls {
lines.push(String::new());
match decl {
Decl::Class(typ) => {
lines.extend(format_jsdoc(&typ.doc, ""));
lines.push(format!("export declare class {prefix}{} {{", typ.name));
for method in &typ.methods {
let js_name = to_node_name(&method.name);
let params = dts_params(&method.params, prefix);
let ret = dts_return_type(
&method.return_type,
method.error_type.is_some(),
method.is_async,
prefix,
);
lines.extend(format_jsdoc(&method.doc, " "));
if method.is_static {
lines.push(format!(" static {js_name}({params}): {ret}"));
} else {
lines.push(format!(" {js_name}({params}): {ret}"));
}
}
lines.push("}".to_string());
}
Decl::Interface(typ) => {
lines.extend(format_jsdoc(&typ.doc, ""));
lines.push(format!("export interface {prefix}{} {{", typ.name));
for field in &typ.fields {
let js_name = to_node_name(&field.name);
let ts_ty = dts_type(&field.ty, prefix);
lines.extend(format_jsdoc(&field.doc, " "));
if matches!(field.ty, TypeRef::Optional(_)) {
lines.push(format!(" {js_name}?: {ts_ty}"));
} else {
lines.push(format!(" {js_name}: {ts_ty}"));
}
}
lines.push("}".to_string());
}
Decl::Enum(e) => {
let is_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
lines.extend(format_jsdoc(&e.doc, ""));
if is_data_enum {
let tag_field = e.serde_tag.as_deref().unwrap_or("type");
let mut member_lines: Vec<String> = Vec::new();
for variant in &e.variants {
let tag_value = variant
.serde_rename
.as_deref()
.map(|s| s.to_string())
.unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
let mut obj_fields: Vec<String> = vec![format!("{tag_field}: '{tag_value}'")];
for field in &variant.fields {
let js_name = to_node_name(&field.name);
let ts_ty = dts_type(&field.ty, prefix);
if matches!(field.ty, TypeRef::Optional(_)) {
obj_fields.push(format!("{js_name}?: {ts_ty}"));
} else {
obj_fields.push(format!("{js_name}: {ts_ty}"));
}
}
member_lines.push(format!(" | {{ {} }}", obj_fields.join("; ")));
}
lines.push(format!("export type {prefix}{} =", e.name));
lines.extend(member_lines);
} else {
lines.push(format!("export declare enum {prefix}{} {{", e.name));
for variant in &e.variants {
let value = variant
.serde_rename
.as_deref()
.map(|s| s.to_string())
.unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
lines.extend(format_jsdoc(&variant.doc, " "));
lines.push(format!(" {} = \"{}\",", variant.name, value));
}
lines.push("}".to_string());
}
}
Decl::Function(func) => {
let js_name = to_node_name(&func.name);
let params = dts_params(&func.params, prefix);
let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async, prefix);
lines.extend(format_jsdoc(&func.doc, ""));
lines.push(format!("export declare function {js_name}({params}): {ret};"));
}
}
}
lines.push(String::new());
lines.join("\n")
}
fn format_jsdoc(doc: &str, indent: &str) -> Vec<String> {
let doc = doc.trim();
if doc.is_empty() {
return vec![];
}
let lines: Vec<&str> = doc.lines().collect();
if lines.len() == 1 {
vec![format!("{indent}/** {} */", lines[0].trim())]
} else {
let mut out = Vec::with_capacity(lines.len() + 2);
out.push(format!("{indent}/**"));
for line in &lines {
let trimmed = line.trim();
if trimmed.is_empty() {
out.push(format!("{indent} *"));
} else {
out.push(format!("{indent} * {trimmed}"));
}
}
out.push(format!("{indent} */"));
out
}
}
fn dts_type(ty: &TypeRef, prefix: &str) -> String {
match ty {
TypeRef::Primitive(p) => match p {
alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
alef_core::ir::PrimitiveType::U8
| alef_core::ir::PrimitiveType::U16
| alef_core::ir::PrimitiveType::U32
| alef_core::ir::PrimitiveType::I8
| alef_core::ir::PrimitiveType::I16
| alef_core::ir::PrimitiveType::I32
| alef_core::ir::PrimitiveType::F32
| alef_core::ir::PrimitiveType::F64 => "number".to_string(),
alef_core::ir::PrimitiveType::U64
| alef_core::ir::PrimitiveType::I64
| alef_core::ir::PrimitiveType::Usize
| alef_core::ir::PrimitiveType::Isize => "number".to_string(),
},
TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
TypeRef::Bytes => "Uint8Array".to_string(),
TypeRef::Json => "unknown".to_string(),
TypeRef::Duration => "number".to_string(),
TypeRef::Unit => "void".to_string(),
TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner, prefix)),
TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner, prefix)),
TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k, prefix), dts_type(v, prefix)),
TypeRef::Named(name) => format!("{prefix}{name}"),
}
}
fn dts_params(params: &[ParamDef], prefix: &str) -> String {
let mut required: Vec<&ParamDef> = Vec::new();
let mut optional: Vec<&ParamDef> = Vec::new();
for p in params {
if p.optional {
optional.push(p);
} else {
required.push(p);
}
}
let ordered: Vec<&ParamDef> = if params
.iter()
.zip(required.iter().chain(optional.iter()))
.all(|(a, b)| std::ptr::eq(a as *const ParamDef, *b as *const ParamDef))
{
params.iter().collect()
} else {
required.into_iter().chain(optional).collect()
};
ordered
.iter()
.map(|p| {
let js_name = to_node_name(&p.name);
let ts_ty = dts_type(&p.ty, prefix);
if p.optional {
format!("{js_name}?: {ts_ty} | undefined | null")
} else {
format!("{js_name}: {ts_ty}")
}
})
.collect::<Vec<_>>()
.join(", ")
}
fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool, prefix: &str) -> String {
let base = match ret {
TypeRef::Unit => "void".to_string(),
other => dts_type(other, prefix),
};
if is_async { format!("Promise<{base}>") } else { base }
}
fn apply_rename_all(variant_name: &str, rename_all: Option<&str>) -> String {
match rename_all {
Some("snake_case") => {
let mut out = String::with_capacity(variant_name.len() + 4);
for (i, c) in variant_name.chars().enumerate() {
if c.is_uppercase() && i > 0 {
out.push('_');
}
out.extend(c.to_lowercase());
}
out
}
Some("camelCase") => {
let mut chars = variant_name.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
}
}
Some("kebab-case") => {
let mut out = String::with_capacity(variant_name.len() + 4);
for (i, c) in variant_name.chars().enumerate() {
if c.is_uppercase() && i > 0 {
out.push('-');
}
out.extend(c.to_lowercase());
}
out
}
Some("SCREAMING_SNAKE_CASE") => {
let mut out = String::with_capacity(variant_name.len() + 4);
for (i, c) in variant_name.chars().enumerate() {
if c.is_uppercase() && i > 0 {
out.push('_');
}
out.extend(c.to_uppercase());
}
out
}
Some("lowercase") => variant_name.to_lowercase(),
Some("UPPERCASE") => variant_name.to_uppercase(),
_ => variant_name.to_string(),
}
}
fn gen_tagged_enum_binding_to_core(
enum_def: &EnumDef,
core_import: &str,
prefix: &str,
struct_names: &ahash::AHashSet<String>,
) -> String {
use alef_core::ir::TypeRef;
use std::fmt::Write;
let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
let binding_name = format!("{prefix}{}", enum_def.name);
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
let mut out = String::with_capacity(512);
writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
writeln!(out, " fn from(val: {binding_name}) -> Self {{").ok();
writeln!(out, " match val.{tag_field}_tag.as_str() {{").ok();
for variant in &enum_def.variants {
let default_tag = variant.name.to_lowercase();
let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
if variant.fields.is_empty() {
writeln!(out, " \"{tag_value}\" => Self::{},", variant.name).ok();
} else {
let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
let field_exprs: Vec<String> = variant
.fields
.iter()
.map(|f| {
let has_binding = fields_with_binding_struct.contains(f.name.as_str());
let is_mixed = mixed_named_fields.contains(&f.name);
if f.optional {
match &f.ty {
TypeRef::Path => {
format!("val.{}.map(std::path::PathBuf::from)", f.name)
}
TypeRef::Named(n) if is_mixed => {
let core_type = format!("{core_import}::{n}");
format!(
"val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok())",
f.name
)
}
TypeRef::Named(_) if has_binding => {
format!("val.{}.map(|v| v.into())", f.name)
}
TypeRef::Named(_) => {
format!("val.{}.map(|v| v.into())", f.name)
}
TypeRef::Primitive(p) if needs_napi_cast(p) => {
let core_ty = core_prim_str(p);
format!("val.{}.map(|v| v as {core_ty})", f.name)
}
_ => {
format!("val.{}", f.name)
}
}
} else if f.sanitized {
let expr = "Default::default()".to_string();
if f.is_boxed { format!("Box::new({expr})") } else { expr }
} else {
let expr = match &f.ty {
TypeRef::Named(n) if is_mixed => {
let core_type = format!("{core_import}::{n}");
format!(
"val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok()).unwrap_or_default()",
f.name
)
}
TypeRef::Named(_) if has_binding => {
format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
}
TypeRef::Named(_) => {
format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
}
TypeRef::Path => {
format!("val.{}.map(std::path::PathBuf::from).unwrap_or_default()", f.name)
}
TypeRef::Primitive(p) if needs_napi_cast(p) => {
let core_ty = core_prim_str(p);
format!("val.{}.map(|v| v as {core_ty}).unwrap_or_default()", f.name)
}
_ => {
format!("val.{}.unwrap_or_default()", f.name)
}
};
if f.is_boxed { format!("Box::new({expr})") } else { expr }
}
})
.collect();
if is_tuple {
writeln!(
out,
" \"{tag_value}\" => Self::{}({}),",
variant.name,
field_exprs.join(", ")
)
.ok();
} else {
let field_inits: Vec<String> = variant
.fields
.iter()
.zip(field_exprs.iter())
.map(|(f, expr)| format!("{}: {expr}", f.name))
.collect();
writeln!(
out,
" \"{tag_value}\" => Self::{} {{ {} }},",
variant.name,
field_inits.join(", ")
)
.ok();
}
}
}
if let Some(first) = enum_def.variants.first() {
if first.fields.is_empty() {
writeln!(out, " _ => Self::{},", first.name).ok();
} else {
let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
if is_tuple {
let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
writeln!(out, " _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
} else {
let defaults: Vec<String> = first
.fields
.iter()
.map(|f| format!("{}: Default::default()", f.name))
.collect();
writeln!(
out,
" _ => Self::{} {{ {} }},",
first.name,
defaults.join(", ")
)
.ok();
}
}
}
writeln!(out, " }}").ok();
writeln!(out, " }}").ok();
write!(out, "}}").ok();
out
}
fn gen_tagged_enum_core_to_binding(
enum_def: &EnumDef,
core_import: &str,
prefix: &str,
struct_names: &ahash::AHashSet<String>,
) -> String {
use std::fmt::Write;
let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
let binding_name = format!("{prefix}{}", enum_def.name);
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
let all_fields: Vec<String> = {
let mut fields = std::collections::BTreeSet::new();
for v in &enum_def.variants {
for f in &v.fields {
fields.insert(f.name.clone());
}
}
fields.into_iter().collect()
};
let mut out = String::with_capacity(512);
writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
writeln!(out, " fn from(val: {core_path}) -> Self {{").ok();
writeln!(out, " match val {{").ok();
for variant in &enum_def.variants {
let default_tag = variant.name.to_lowercase();
let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
let _variant_field_names: std::collections::BTreeSet<String> =
variant.fields.iter().map(|f| f.name.clone()).collect();
if variant.fields.is_empty() {
writeln!(
out,
" {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
variant.name,
all_fields
.iter()
.map(|f| format!("{f}: None"))
.collect::<Vec<_>>()
.join(", ")
)
.ok();
} else {
use alef_core::ir::TypeRef;
let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
let destructured: Vec<String> = variant
.fields
.iter()
.map(|f| {
if f.sanitized {
if is_tuple {
format!("_{}", f.name)
} else {
format!("{}: _{}", f.name, f.name)
}
} else {
f.name.clone()
}
})
.collect();
let field_inits: Vec<String> = all_fields
.iter()
.map(|f| {
if let Some(field) = variant_field_map.get(f.as_str()) {
let has_binding = fields_with_binding_struct.contains(f.as_str());
let is_mixed = mixed_named_fields.contains(f.as_str());
if field.optional {
match &field.ty {
TypeRef::Path => format!("{f}: {f}.map(|p| p.to_string_lossy().to_string())"),
TypeRef::Named(_) if is_mixed => {
format!("{f}: {f}.and_then(|v| serde_json::to_string(&v).ok())")
}
TypeRef::Named(_) if has_binding => {
format!("{f}: {f}.map(|v| v.into())")
}
TypeRef::Named(_) => {
format!("{f}: {f}.map(|v| v.into())")
}
_ => format!("{f}: {f}"),
}
} else if field.sanitized {
format!("{f}: None")
} else {
match &field.ty {
TypeRef::Named(_) if is_mixed => {
format!("{f}: serde_json::to_string(&{f}).ok()")
}
TypeRef::Named(_) if has_binding => format!("{f}: Some({f}.into())"),
TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
TypeRef::Path => format!("{f}: Some({f}.to_string_lossy().to_string())"),
TypeRef::Primitive(p) if needs_napi_cast(p) => {
match p {
alef_core::ir::PrimitiveType::F32 => format!("{f}: Some({f} as f64)"),
alef_core::ir::PrimitiveType::U64
| alef_core::ir::PrimitiveType::Usize
| alef_core::ir::PrimitiveType::Isize => format!("{f}: Some({f} as i64)"),
_ => format!("{f}: Some({f})"),
}
}
_ => format!("{f}: Some({f})"),
}
}
} else {
format!("{f}: None")
}
})
.collect();
if is_tuple {
writeln!(
out,
" {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
variant.name,
destructured.join(", "),
field_inits.join(", ")
)
.ok();
} else {
writeln!(
out,
" {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
variant.name,
destructured.join(", "),
field_inits.join(", ")
)
.ok();
}
}
}
writeln!(out, " }}").ok();
writeln!(out, " }}").ok();
write!(out, "}}").ok();
out
}
fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
use alef_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()
}
fn tagged_enum_binding_struct_fields<'a>(
enum_def: &'a EnumDef,
struct_names: &ahash::AHashSet<String>,
) -> ahash::AHashSet<&'a str> {
use alef_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)]
mod tests {
use super::*;
use alef_core::ir::{ParamDef, TypeRef};
fn make_param(name: &str, optional: bool) -> ParamDef {
ParamDef {
name: name.to_string(),
ty: TypeRef::String,
optional,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
}
}
#[test]
fn dts_params_reorders_required_after_optional() {
let params = vec![
make_param("ctx", false),
make_param("lang", true),
make_param("code", false),
];
let result = dts_params(¶ms, "Js");
let ctx_pos = result.find("ctx:").expect("ctx not found");
let code_pos = result.find("code:").expect("code not found");
let lang_pos = result.find("lang?:").expect("lang? not found");
assert!(ctx_pos < lang_pos, "ctx should come before lang?: {result}");
assert!(code_pos < lang_pos, "code should come before lang?: {result}");
}
#[test]
fn dts_params_preserves_already_valid_order() {
let params = vec![
make_param("ctx", false),
make_param("code", false),
make_param("lang", true),
];
let result = dts_params(¶ms, "Js");
assert_eq!(result, "ctx: string, code: string, lang?: string | undefined | null");
}
#[test]
fn dts_params_all_required_preserves_order() {
let params = vec![make_param("a", false), make_param("b", false), make_param("c", false)];
let result = dts_params(¶ms, "Js");
assert_eq!(result, "a: string, b: string, c: string");
}
}