use crate::type_map::NapiMapper;
use ahash::AHashSet;
use alef_codegen::builder::{ImplBuilder, StructBuilder};
use alef_codegen::generators::{self, 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::ir::{MethodDef, TypeDef, TypeRef};
use super::functions::{napi_apply_primitive_casts_to_call_args, napi_gen_call_args, napi_wrap_return};
pub(super) fn gen_struct(
typ: &TypeDef,
mapper: &NapiMapper,
prefix: &str,
has_serde: bool,
opaque_types: &ahash::AHashSet<String>,
never_skip_cfg_field_names: &[String],
) -> String {
let has_bytes_field = false;
let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
struct_builder.add_attr("napi(object)");
if !has_bytes_field {
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");
}
let _ = never_skip_cfg_field_names;
for field in &typ.fields {
let map_bytes_field_type = |ty: &TypeRef| -> String {
fn replace_bytes(ty: &TypeRef, mapper: &NapiMapper) -> String {
match ty {
TypeRef::Bytes => "Vec<u8>".to_string(),
TypeRef::Optional(inner) => format!("Option<{}>", replace_bytes(inner, mapper)),
TypeRef::Map(k, v) => {
format!("HashMap<{}, {}>", replace_bytes(k, mapper), replace_bytes(v, mapper))
}
TypeRef::Vec(inner) => format!("Vec<{}>", replace_bytes(inner, mapper)),
other => mapper.map_type(other),
}
}
replace_bytes(ty, mapper)
};
let (base_type, already_optional): (String, bool) = match &field.ty {
TypeRef::Named(name) if opaque_types.contains(name) => {
("napi::bindgen_prelude::Object<'static>".to_string(), false)
}
TypeRef::Optional(inner) => {
if let TypeRef::Named(name) = inner.as_ref() {
if opaque_types.contains(name) {
("Option<napi::bindgen_prelude::Object<'static>>".to_string(), true)
} else {
(map_bytes_field_type(&field.ty), true)
}
} else {
(map_bytes_field_type(&field.ty), true)
}
}
_ => (map_bytes_field_type(&field.ty), false),
};
let field_type = if (field.optional || typ.has_default) && !already_optional {
format!("Option<{base_type}>")
} else {
base_type
};
let js_name = field.serde_rename.clone().unwrap_or_else(|| to_node_name(&field.name));
let mut attrs = if js_name != field.name {
vec![format!("napi(js_name = \"{}\")", js_name)]
} else {
vec![]
};
let is_opaque_field = match &field.ty {
TypeRef::Named(name) if opaque_types.contains(name) => true,
TypeRef::Optional(inner) => {
matches!(inner.as_ref(), TypeRef::Named(name) if opaque_types.contains(name))
}
_ => false,
};
let skip_cfg_bridge_field = field.cfg.is_some() && never_skip_cfg_field_names.contains(&field.name);
if has_serde && (is_opaque_field || skip_cfg_bridge_field) {
attrs.push("serde(skip)".to_string());
}
struct_builder.add_field(&field.name, &field_type, attrs);
}
let mut out = struct_builder.build();
if has_bytes_field {
let struct_name = format!("{prefix}{}", typ.name);
out.push('\n');
out.push_str(&crate::template_env::render(
"clone_impl_header.jinja",
minijinja::context! {
struct_name => struct_name,
},
));
for field in &typ.fields {
let is_bytes_field = matches!(&field.ty, TypeRef::Bytes);
let is_opt_bytes =
matches!(&field.ty, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Bytes));
let is_opaque_named = matches!(&field.ty, TypeRef::Named(name) if opaque_types.contains(name));
let is_opt_opaque_named = matches!(&field.ty, TypeRef::Optional(inner)
if matches!(inner.as_ref(), TypeRef::Named(name) if opaque_types.contains(name)));
let field_is_optional_in_struct = field.optional
|| typ.has_default
|| is_opt_bytes
|| is_opt_opaque_named
|| matches!(&field.ty, TypeRef::Optional(_));
let _ = is_opaque_named;
let expr = if is_bytes_field && field_is_optional_in_struct {
format!("self.{}.as_ref().map(|b| b.to_vec().into())", field.name)
} else if is_bytes_field {
format!("self.{}.to_vec().into()", field.name)
} else if is_opt_bytes {
format!("self.{}.as_ref().map(|b| b.to_vec().into())", field.name)
} else {
format!("self.{}.clone()", field.name)
};
out.push_str(&crate::template_env::render(
"clone_impl_field.jinja",
minijinja::context! {
field_name => &field.name,
expr => expr,
},
));
}
out.push_str(" }\n }\n}\n");
}
out
}
pub(super) fn gen_opaque_struct_methods(
typ: &TypeDef,
mapper: &NapiMapper,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
prefix: &str,
adapter_bodies: &alef_adapters::AdapterBodies,
streaming_item_types: &ahash::AHashMap<String, String>,
capsule_type_names: &AHashSet<String>,
) -> 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;
}
let returns_capsule = match &method.return_type {
TypeRef::Named(name) => capsule_type_names.contains(name),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) => capsule_type_names.contains(name),
_ => false,
},
_ => false,
};
if returns_capsule {
continue;
}
let has_opaque_by_value_param = method.params.iter().any(|p| {
let inner_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
matches!(inner_ty, TypeRef::Named(name) if opaque_types.contains(name) && !p.is_ref)
});
if has_opaque_by_value_param && !adapter_bodies.contains_key(&adapter_key) {
continue;
}
impl_builder.add_method(&gen_opaque_instance_method(
method,
mapper,
typ,
cfg,
opaque_types,
prefix,
adapter_bodies,
streaming_item_types,
));
}
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()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn gen_opaque_instance_method(
method: &MethodDef,
mapper: &NapiMapper,
typ: &TypeDef,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
prefix: &str,
adapter_bodies: &alef_adapters::AdapterBodies,
streaming_item_types: &ahash::AHashMap<String, String>,
) -> String {
let params = function_params(&method.params, &|ty| mapper.map_type(ty));
let adapter_key_for_stream = format!("{}.{}", typ.name, method.name);
let stream_item = streaming_item_types.get(&adapter_key_for_stream);
let return_type = if let Some(item) = stream_item {
format!("Vec<{prefix}{item}>")
} else {
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
)
}
pub(super) 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
)
}
#[cfg(test)]
mod tests {
#[test]
fn struct_gen_function_exists() {
}
}