use crate::codegen::generators::trait_bridge::{bridge_param_type as param_type, to_camel_case, visitor_param_type};
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{ApiSurface, MethodDef, ParamDef, TypeDef, TypeRef};
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub(crate) struct UnsupportedArg {
pub trait_name: String,
pub method_name: String,
pub param_name: String,
pub type_desc: String,
}
pub(super) fn gen_visitor_bridge(
trait_type: &TypeDef,
bridge_cfg: &TraitBridgeConfig,
struct_name: &str,
trait_path: &str,
core_crate: &str,
type_paths: &HashMap<String, String>,
api: &ApiSurface,
) -> anyhow::Result<String> {
let result_metadata = crate::codegen::visitor_result::required_visitor_result_metadata(api, bridge_cfg)?;
let context_helper = crate::codegen::visitor_context::visitor_context_helper(
api,
bridge_cfg,
core_crate,
crate::codegen::visitor_context::VisitorContextBackend::Napi,
)?;
let mut method_impls = String::with_capacity(4096);
let unsupported: RefCell<Vec<UnsupportedArg>> = RefCell::new(Vec::new());
let json_safe = json_safe_named_types(api);
for method in crate::codegen::generators::trait_bridge::visitor_callback_methods(trait_type, bridge_cfg) {
gen_visitor_method_napi(
&mut method_impls,
method,
trait_path,
core_crate,
bridge_cfg,
type_paths,
&result_metadata,
&trait_type.name,
&unsupported,
&json_safe,
);
}
let unsupported = unsupported.into_inner();
if !unsupported.is_empty() {
let sites = unsupported
.iter()
.map(|u| {
format!(
" - {}::{}({}: {})",
u.trait_name, u.method_name, u.param_name, u.type_desc
)
})
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(
"napi visitor bridge for '{}' cannot marshal {} parameter(s) to JS natively:\n{}",
trait_type.name,
unsupported.len(),
sites
);
}
Ok(crate::backends::napi::template_env::render(
"visitor_bridge.jinja",
minijinja::context! {
core_crate => core_crate,
context_type_path => context_helper.type_path,
context_field_lines => context_helper.field_lines,
struct_name => struct_name,
trait_path => trait_path,
method_impls => method_impls,
},
))
}
pub(super) fn json_safe_named_types(api: &ApiSurface) -> std::collections::HashSet<String> {
api.enums
.iter()
.filter(|e| e.has_serde && !e.binding_excluded)
.map(|e| e.name.clone())
.collect()
}
pub(super) fn unknown_tuple_type(count: usize) -> String {
if count == 0 {
return "()".to_string();
}
let parts = vec!["napi::bindgen_prelude::Unknown"; count];
format!("({}{})", parts.join(", "), if count == 1 { "," } else { "" })
}
#[allow(clippy::too_many_arguments)]
fn gen_visitor_method_napi(
out: &mut String,
method: &MethodDef,
_trait_path: &str,
_core_crate: &str,
bridge_cfg: &TraitBridgeConfig,
type_paths: &HashMap<String, String>,
result_metadata: &crate::codegen::visitor_result::VisitorResultMetadata,
trait_name: &str,
unsupported: &RefCell<Vec<UnsupportedArg>>,
json_safe_named_types: &std::collections::HashSet<String>,
) {
let name = &method.name;
let js_method_name = to_camel_case(name);
let mut sig_parts = vec!["&mut self".to_string()];
for p in &method.params {
let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
sig_parts.push(format!("{}: {}", p.name, ty_str));
}
let signature = sig_parts.join(", ");
let return_type = match &method.return_type {
TypeRef::Named(n) => type_paths
.get(n.as_str())
.map(|p| p.replace('-', "_"))
.unwrap_or_else(|| n.clone()),
other => param_type(other, "", false, type_paths),
};
let arg_count = method.params.len();
let empty_args = arg_count == 0;
let inner_tuple_ty = unknown_tuple_type(arg_count);
let args_tuple_ty = if empty_args {
inner_tuple_ty
} else {
format!("napi::bindgen_prelude::FnArgs<{inner_tuple_ty}>")
};
let js_args_exprs = build_napi_args(
method,
bridge_cfg,
&std::collections::HashSet::new(),
"Js",
trait_name,
unsupported,
json_safe_named_types,
);
let arg_exprs: Vec<String> = js_args_exprs
.iter()
.map(|expr| expr.replace("self.env()", "__env"))
.collect();
let tuple_args = if arg_count == 1 {
"(arg_0,)".to_string()
} else if arg_count > 0 {
let arg_names: Vec<String> = (0..arg_count).map(|i| format!("arg_{i}")).collect();
format!("({})", arg_names.join(", "))
} else {
String::new()
};
out.push_str(&crate::backends::napi::template_env::render(
"visitor_method.jinja",
minijinja::context! {
method_name => name,
js_method_name => js_method_name,
signature => signature,
return_type => return_type,
default_result_expr => crate::codegen::visitor_result::default_result_expr(&return_type, result_metadata),
unknown_string_result_expr => crate::codegen::visitor_result::unknown_string_result_expr(
&return_type,
result_metadata,
"s",
),
unit_result_variants => crate::codegen::visitor_result::variant_contexts(&result_metadata.unit_variants),
payload_result_variants => crate::codegen::visitor_result::variant_contexts(
&result_metadata.string_payload_variants,
),
empty_args => empty_args,
arg_exprs => arg_exprs,
tuple_args => tuple_args,
args_tuple_ty => args_tuple_ty,
},
));
}
pub(super) fn is_napi_encodable(ty: &TypeRef) -> bool {
match ty {
TypeRef::String => true,
TypeRef::Primitive(_) => true,
TypeRef::Vec(inner) => is_napi_encodable(inner),
_ => false,
}
}
pub(super) fn is_napi_decodable(ty: &TypeRef) -> bool {
use crate::core::ir::PrimitiveType;
match ty {
TypeRef::String => true,
TypeRef::Primitive(p) => !matches!(
p,
PrimitiveType::F32 | PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize
),
TypeRef::Vec(inner) => is_napi_decodable(inner),
_ => false,
}
}
pub(super) fn f32_bridge_target(ty: &TypeRef) -> Option<TypeRef> {
match ty {
TypeRef::Primitive(crate::core::ir::PrimitiveType::F32) => {
Some(TypeRef::Primitive(crate::core::ir::PrimitiveType::F64))
}
TypeRef::Vec(inner) => f32_bridge_target(inner).map(|t| TypeRef::Vec(Box::new(t))),
_ => None,
}
}
pub(super) fn f32_bridge_cast_expr(ty: &TypeRef, var: &str) -> String {
match ty {
TypeRef::Vec(inner) => {
let inner_expr = f32_bridge_cast_expr(inner, "v");
format!("{var}.into_iter().map(|v| {inner_expr}).collect()")
}
_ => format!("{var} as f32"),
}
}
fn napi_null_expr() -> String {
"unsafe { \
let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }"
.to_string()
}
fn to_napi_value_expr(value_expr: &str) -> String {
format!(
"unsafe {{ \
let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), {value_expr}).unwrap_or(std::ptr::null_mut()); \
napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }}"
)
}
fn json_encode_expr(value_expr: &str) -> String {
to_napi_value_expr(&format!(
"serde_json::to_value({value_expr}).unwrap_or(serde_json::Value::Null)"
))
}
pub(super) fn build_napi_args(
method: &MethodDef,
bridge_cfg: &TraitBridgeConfig,
struct_param_types: &std::collections::HashSet<String>,
type_prefix: &str,
trait_name: &str,
unsupported: &RefCell<Vec<UnsupportedArg>>,
json_safe_named_types: &std::collections::HashSet<String>,
) -> Vec<String> {
method
.params
.iter()
.map(|p| {
build_one_napi_arg(
p,
method,
bridge_cfg,
struct_param_types,
type_prefix,
trait_name,
unsupported,
json_safe_named_types,
)
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn build_one_napi_arg(
p: &ParamDef,
method: &MethodDef,
bridge_cfg: &TraitBridgeConfig,
struct_param_types: &std::collections::HashSet<String>,
type_prefix: &str,
trait_name: &str,
unsupported: &RefCell<Vec<UnsupportedArg>>,
json_safe_named_types: &std::collections::HashSet<String>,
) -> String {
use crate::core::ir::PrimitiveType;
if let TypeRef::Named(n) = &p.ty {
if Some(n.as_str()) == bridge_cfg.context_type.as_deref() {
return crate::backends::napi::template_env::render(
"visitor_context_arg_expr.jinja",
minijinja::context! { ref_prefix => if p.is_ref { "" } else { "&" }, name => p.name.as_str() },
)
.trim_end()
.to_string();
}
if struct_param_types.contains(n.as_str()) {
let owned = if p.is_ref {
format!("(*{}).clone()", p.name)
} else {
p.name.clone()
};
return format!(
"unsafe {{ \
let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), {prefix}{ty}::from({owned})).unwrap_or(std::ptr::null_mut()); \
napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }}",
prefix = type_prefix,
ty = n,
);
}
if json_safe_named_types.contains(n.as_str()) {
return json_encode_expr(&p.name);
}
return to_napi_value_expr(&format!("format!(\"{{:?}}\", {})", p.name));
}
if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
return format!(
"match {name} {{ Some(s) => {some}, None => {null} }}",
name = p.name,
some = to_napi_value_expr("s"),
null = napi_null_expr()
);
}
if p.optional && !matches!(&p.ty, TypeRef::String) {
if let TypeRef::Named(n) = &p.ty
&& !json_safe_named_types.contains(n.as_str())
{
unsupported.borrow_mut().push(UnsupportedArg {
trait_name: trait_name.to_string(),
method_name: method.name.clone(),
param_name: p.name.clone(),
type_desc: format!("Optional({:?})", p.ty),
});
return napi_null_expr();
}
return json_encode_expr(&p.name);
}
if matches!(&p.ty, TypeRef::String) && p.is_ref {
return to_napi_value_expr(&p.name);
}
if matches!(&p.ty, TypeRef::String) {
return to_napi_value_expr(&format!("{}.clone()", p.name));
}
if matches!(&p.ty, TypeRef::Char) {
return to_napi_value_expr(&format!("{}.to_string()", p.name));
}
if matches!(&p.ty, TypeRef::Path) {
return to_napi_value_expr(&format!("{}.to_string_lossy().into_owned()", p.name));
}
if matches!(&p.ty, TypeRef::Bytes) {
let owned = if p.is_ref {
format!("{}.to_vec()", p.name)
} else {
format!("{}.clone()", p.name)
};
return to_napi_value_expr(&format!("napi::bindgen_prelude::Buffer::from({owned})"));
}
if matches!(&p.ty, TypeRef::Duration) {
return to_napi_value_expr(&format!("{}.as_secs_f64()", p.name));
}
if matches!(&p.ty, TypeRef::Json) {
let owned = if p.is_ref {
format!("{}.clone()", p.name)
} else {
p.name.clone()
};
return to_napi_value_expr(&owned);
}
if matches!(&p.ty, TypeRef::Map(_, _)) {
return json_encode_expr(&p.name);
}
if matches!(&p.ty, TypeRef::Primitive(PrimitiveType::Bool)) {
return to_napi_value_expr(&p.name);
}
if matches!(
&p.ty,
TypeRef::Primitive(
PrimitiveType::U32
| PrimitiveType::I32
| PrimitiveType::I64
| PrimitiveType::U64
| PrimitiveType::F32
| PrimitiveType::F64
| PrimitiveType::I8
| PrimitiveType::I16
| PrimitiveType::U8
| PrimitiveType::U16
| PrimitiveType::Isize
)
) {
return to_napi_value_expr(&p.name);
}
if matches!(&p.ty, TypeRef::Primitive(PrimitiveType::Usize)) {
return to_napi_value_expr(&format!("{} as u32", p.name));
}
if let TypeRef::Vec(inner) = &p.ty {
if is_napi_encodable(inner) {
let owned = if p.is_ref {
format!("{}.to_vec()", p.name)
} else {
format!("{}.clone()", p.name)
};
return to_napi_value_expr(&owned);
}
if let TypeRef::Named(n) = inner.as_ref()
&& !json_safe_named_types.contains(n.as_str())
{
unsupported.borrow_mut().push(UnsupportedArg {
trait_name: trait_name.to_string(),
method_name: method.name.clone(),
param_name: p.name.clone(),
type_desc: format!("{:?}", p.ty),
});
return napi_null_expr();
}
return json_encode_expr(&p.name);
}
unsupported.borrow_mut().push(UnsupportedArg {
trait_name: trait_name.to_string(),
method_name: method.name.clone(),
param_name: p.name.clone(),
type_desc: format!("{:?}", p.ty),
});
napi_null_expr()
}