Skip to main content

alef_backend_napi/
trait_bridge.rs

1//! NAPI-RS-specific trait bridge code generation.
2//!
3//! Generates Rust wrapper structs that implement Rust traits by delegating
4//! to JavaScript objects via NAPI-RS.
5
6use alef_codegen::generators::trait_bridge::{
7    BridgeOutput, TraitBridgeGenerator, TraitBridgeSpec, bridge_param_type as param_type, gen_bridge_all,
8    host_function_path, to_camel_case, visitor_param_type,
9};
10use alef_core::config::TraitBridgeConfig;
11use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
12use std::collections::HashMap;
13
14/// Find the first parameter index and bridge config where the parameter's named type
15/// matches a trait bridge's `type_alias`.
16///
17/// Returns `None` when no bridge applies.
18pub use alef_codegen::generators::trait_bridge::find_bridge_param;
19
20/// Find a bridge config that uses options_field binding and a parameter of the options_type.
21/// This complements find_bridge_param which only handles FunctionParam bindings.
22pub fn find_options_field_binding<'a>(
23    func: &alef_core::ir::FunctionDef,
24    bridges: &'a [TraitBridgeConfig],
25) -> Option<(usize, &'a TraitBridgeConfig)> {
26    for bridge in bridges {
27        if bridge.bind_via != alef_core::config::BridgeBinding::OptionsField {
28            continue;
29        }
30        if let Some(options_type) = &bridge.options_type {
31            for (idx, param) in func.params.iter().enumerate() {
32                // Check if param type is Named(options_type) or Optional(Named(options_type))
33                let matches = match &param.ty {
34                    alef_core::ir::TypeRef::Named(n) => n == options_type,
35                    alef_core::ir::TypeRef::Optional(inner) => {
36                        if let alef_core::ir::TypeRef::Named(n) = inner.as_ref() {
37                            n == options_type
38                        } else {
39                            false
40                        }
41                    }
42                    _ => false,
43                };
44                if matches {
45                    return Some((idx, bridge));
46                }
47            }
48        }
49    }
50    None
51}
52
53/// NAPI-specific trait bridge generator.
54/// Implements code generation for bridging JavaScript objects to Rust traits.
55pub struct NapiBridgeGenerator {
56    /// Core crate import path (e.g., `"kreuzberg"`).
57    pub core_import: String,
58    /// Map of type name → fully-qualified Rust path for type references.
59    pub type_paths: HashMap<String, String>,
60    /// Error type name (e.g., `"KreuzbergError"`).
61    pub error_type: String,
62}
63
64impl TraitBridgeGenerator for NapiBridgeGenerator {
65    fn foreign_object_type(&self) -> &str {
66        "napi::bindgen_prelude::Object<'static>"
67    }
68
69    fn bridge_imports(&self) -> Vec<String> {
70        vec![
71            "napi::bindgen_prelude::{JsObjectValue, ToNapiValue, Unknown, Object}".to_string(),
72            "napi::JsValue".to_string(),
73            "std::sync::Arc".to_string(),
74        ]
75    }
76
77    fn gen_sync_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
78        let name = &method.name;
79        let has_error = method.error_type.is_some();
80
81        // Get the JS function from the object
82        let js_args_exprs = build_napi_args(method);
83        let inner_tuple_ty = unknown_tuple_type(js_args_exprs.len());
84        let args_tuple_ty = if js_args_exprs.is_empty() {
85            inner_tuple_ty.clone()
86        } else {
87            format!("napi::bindgen_prelude::FnArgs<{inner_tuple_ty}>")
88        };
89
90        let empty_args = js_args_exprs.is_empty();
91        let tuple_args = if empty_args {
92            String::new()
93        } else if js_args_exprs.len() == 1 {
94            format!("({},)", js_args_exprs[0])
95        } else {
96            format!("({})", js_args_exprs.join(", "))
97        };
98
99        let error_lookup =
100            spec.make_error("format!(\"Method '{}' not found on bridge object: {}\", self.cached_name, e)");
101        let error_call = spec.make_error(&format!(
102            "format!(\"Plugin '{{}}' method '{}' failed: {{}}\", self.cached_name, e)",
103            name
104        ));
105        let error_coercion = spec.make_error(&format!(
106            "format!(\"Failed to extract return value from method '{}': {{}}\", e)",
107            name
108        ));
109        let error_parse = spec.make_error(&format!(
110            "format!(\"Failed to parse return value for method '{}'\", self.cached_name)",
111            name
112        ));
113
114        if matches!(method.return_type, TypeRef::Unit) {
115            crate::template_env::render(
116                "sync_method_unit_return.jinja",
117                minijinja::context! {
118                    method_name => name,
119                    args_tuple_ty => args_tuple_ty,
120                    has_error => has_error,
121                    empty_args => empty_args,
122                    tuple_args => tuple_args,
123                    error_lookup => error_lookup,
124                    error_call => error_call,
125                },
126            )
127        } else {
128            crate::template_env::render(
129                "sync_method_non_unit_return.jinja",
130                minijinja::context! {
131                    method_name => name,
132                    args_tuple_ty => args_tuple_ty,
133                    has_error => has_error,
134                    empty_args => empty_args,
135                    tuple_args => tuple_args,
136                    error_lookup => error_lookup,
137                    error_call => error_call,
138                    error_coercion => error_coercion,
139                    error_parse => error_parse,
140                },
141            )
142        }
143    }
144
145    fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
146        let name = &method.name;
147
148        // Build the JS function call
149        let js_args_exprs = build_napi_args(method);
150        let inner_tuple_ty = unknown_tuple_type(js_args_exprs.len());
151        let args_tuple_ty = if js_args_exprs.is_empty() {
152            inner_tuple_ty.clone()
153        } else {
154            format!("napi::bindgen_prelude::FnArgs<{inner_tuple_ty}>")
155        };
156
157        let empty_args = js_args_exprs.is_empty();
158        let tuple_args = if empty_args {
159            String::new()
160        } else if js_args_exprs.len() == 1 {
161            format!("({},)", js_args_exprs[0])
162        } else {
163            format!("({})", js_args_exprs.join(", "))
164        };
165
166        let error_lookup = spec.make_error("format!(\"Method '{}' not found on bridge object: {}\", cached_name, e)");
167        let error_call = spec.make_error(&format!(
168            "format!(\"Plugin '{{}}' method '{}' failed: {{}}\", cached_name, e)",
169            name
170        ));
171        let error_coercion = spec.make_error(&format!(
172            "format!(\"Failed to extract return value from method '{}': {{}}\", e)",
173            name
174        ));
175        let error_parse = spec.make_error(&format!(
176            "\"Failed to parse return value for method '{}'\".to_string()",
177            name
178        ));
179
180        if matches!(method.return_type, TypeRef::Unit) {
181            crate::template_env::render(
182                "async_method_unit_return.jinja",
183                minijinja::context! {
184                    method_name => name,
185                    args_tuple_ty => args_tuple_ty,
186                    empty_args => empty_args,
187                    tuple_args => tuple_args,
188                    error_lookup => error_lookup,
189                    error_call => error_call,
190                },
191            )
192        } else {
193            crate::template_env::render(
194                "async_method_non_unit_return.jinja",
195                minijinja::context! {
196                    method_name => name,
197                    args_tuple_ty => args_tuple_ty,
198                    empty_args => empty_args,
199                    tuple_args => tuple_args,
200                    error_lookup => error_lookup,
201                    error_call => error_call,
202                    error_coercion => error_coercion,
203                    error_parse => error_parse,
204                },
205            )
206        }
207    }
208
209    fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
210        let wrapper = spec.wrapper_name();
211        let required_methods = spec
212            .required_methods()
213            .iter()
214            .map(|m| {
215                minijinja::context! {
216                    name => &m.name,
217                }
218            })
219            .collect::<Vec<_>>();
220
221        crate::template_env::render(
222            "trait_bridge_constructor.jinja",
223            minijinja::context! {
224                wrapper_name => wrapper,
225                required_methods => required_methods,
226            },
227        )
228    }
229
230    fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
231        let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
232            return String::new();
233        };
234        let host_path = host_function_path(spec, unregister_fn);
235        let camel = to_camel_case(unregister_fn);
236        crate::template_env::render(
237            "unregistration_fn.jinja",
238            minijinja::context! {
239                unregister_fn => unregister_fn,
240                camel_fn_name => camel,
241                host_path => host_path,
242            },
243        )
244    }
245
246    fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
247        let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
248            return String::new();
249        };
250        let host_path = host_function_path(spec, clear_fn);
251        let camel = to_camel_case(clear_fn);
252        crate::template_env::render(
253            "clear_fn.jinja",
254            minijinja::context! {
255                clear_fn => clear_fn,
256                camel_fn_name => camel,
257                host_path => host_path,
258            },
259        )
260    }
261
262    fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
263        let Some(register_fn) = spec.bridge_config.register_fn.as_deref() else {
264            return String::new();
265        };
266        let Some(registry_getter) = spec.bridge_config.registry_getter.as_deref() else {
267            return String::new();
268        };
269        let wrapper = spec.wrapper_name();
270        let trait_path = spec.trait_path();
271
272        let extra = spec
273            .bridge_config
274            .register_extra_args
275            .as_deref()
276            .map(|a| format!(", {a}"))
277            .unwrap_or_default();
278
279        crate::template_env::render(
280            "registration_fn.jinja",
281            minijinja::context! {
282                register_fn => register_fn,
283                wrapper => wrapper,
284                trait_path => trait_path,
285                registry_getter => registry_getter,
286                extra_args => extra,
287            },
288        )
289    }
290}
291
292/// Generate all trait bridge code for a given trait type and bridge config.
293pub fn gen_trait_bridge(
294    trait_type: &TypeDef,
295    bridge_cfg: &TraitBridgeConfig,
296    core_import: &str,
297    error_type: &str,
298    error_constructor: &str,
299    api: &ApiSurface,
300) -> BridgeOutput {
301    // Build type name → rust_path lookup (converted to String-owned HashMap)
302    let type_paths: HashMap<String, String> = api
303        .types
304        .iter()
305        .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
306        .chain(
307            api.enums
308                .iter()
309                .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
310        )
311        .collect();
312
313    // Visitor-style bridge: all methods have defaults, no registry, no super-trait.
314    let is_visitor_bridge = bridge_cfg.type_alias.is_some()
315        && bridge_cfg.register_fn.is_none()
316        && bridge_cfg.super_trait.is_none()
317        && trait_type.methods.iter().all(|m| m.has_default_impl);
318
319    if is_visitor_bridge {
320        let struct_name = format!("Js{}Bridge", bridge_cfg.trait_name);
321        let trait_path = trait_type.rust_path.replace('-', "_");
322        let code = gen_visitor_bridge(
323            trait_type,
324            bridge_cfg,
325            &struct_name,
326            &trait_path,
327            core_import,
328            &type_paths,
329        );
330        BridgeOutput { imports: vec![], code }
331    } else {
332        // Use the IR-driven TraitBridgeGenerator infrastructure
333        let generator = NapiBridgeGenerator {
334            core_import: core_import.to_string(),
335            type_paths: type_paths.clone(),
336            error_type: error_type.to_string(),
337        };
338        let spec = TraitBridgeSpec {
339            trait_def: trait_type,
340            bridge_config: bridge_cfg,
341            core_import,
342            wrapper_prefix: "Js",
343            type_paths,
344            error_type: error_type.to_string(),
345            error_constructor: error_constructor.to_string(),
346        };
347        gen_bridge_all(&spec, &generator)
348    }
349}
350
351/// Generate a visitor-style bridge wrapping a `napi::bindgen_prelude::Object`.
352///
353/// Every trait method checks if the JS object has a matching camelCase property,
354/// then calls it with converted arguments and maps the JS return value to `VisitResult`.
355fn gen_visitor_bridge(
356    trait_type: &TypeDef,
357    _bridge_cfg: &TraitBridgeConfig,
358    struct_name: &str,
359    trait_path: &str,
360    core_crate: &str,
361    type_paths: &HashMap<String, String>,
362) -> String {
363    let mut method_impls = String::with_capacity(4096);
364    for method in &trait_type.methods {
365        if method.trait_source.is_some() {
366            continue;
367        }
368        gen_visitor_method_napi(&mut method_impls, method, trait_path, core_crate, type_paths);
369    }
370
371    crate::template_env::render(
372        "visitor_bridge.jinja",
373        minijinja::context! {
374            core_crate => core_crate,
375            struct_name => struct_name,
376            trait_path => trait_path,
377            method_impls => method_impls,
378        },
379    )
380}
381
382/// Build the Function args tuple type string for a given number of Unknown args.
383fn unknown_tuple_type(count: usize) -> String {
384    if count == 0 {
385        return "()".to_string();
386    }
387    let parts = vec!["napi::bindgen_prelude::Unknown"; count];
388    format!("({}{})", parts.join(", "), if count == 1 { "," } else { "" })
389}
390
391/// Generate a single visitor method that checks for a camelCase JS property and calls it.
392fn gen_visitor_method_napi(
393    out: &mut String,
394    method: &MethodDef,
395    _trait_path: &str,
396    _core_crate: &str,
397    type_paths: &HashMap<String, String>,
398) {
399    let name = &method.name;
400    let js_method_name = to_camel_case(name);
401
402    let mut sig_parts = vec!["&mut self".to_string()];
403    for p in &method.params {
404        let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
405        sig_parts.push(format!("{}: {}", p.name, ty_str));
406    }
407    let signature = sig_parts.join(", ");
408
409    let return_type = match &method.return_type {
410        TypeRef::Named(n) => type_paths
411            .get(n.as_str())
412            .map(|p| p.replace('-', "_"))
413            .unwrap_or_else(|| n.clone()),
414        other => param_type(other, "", false, type_paths),
415    };
416
417    let arg_count = method.params.len();
418    let empty_args = arg_count == 0;
419    let inner_tuple_ty = unknown_tuple_type(arg_count);
420    let args_tuple_ty = if empty_args {
421        inner_tuple_ty
422    } else {
423        format!("napi::bindgen_prelude::FnArgs<{inner_tuple_ty}>")
424    };
425
426    let js_args_exprs = build_napi_args(method);
427    let arg_exprs: Vec<String> = js_args_exprs
428        .iter()
429        .map(|expr| expr.replace("self.env()", "__env"))
430        .collect();
431
432    let tuple_args = if arg_count == 1 {
433        "(arg_0,)".to_string()
434    } else if arg_count > 0 {
435        let arg_names: Vec<String> = (0..arg_count).map(|i| format!("arg_{i}")).collect();
436        format!("({})", arg_names.join(", "))
437    } else {
438        String::new()
439    };
440
441    out.push_str(&crate::template_env::render(
442        "visitor_method.jinja",
443        minijinja::context! {
444            method_name => name,
445            js_method_name => js_method_name,
446            signature => signature,
447            return_type => return_type,
448            empty_args => empty_args,
449            arg_exprs => arg_exprs,
450            tuple_args => tuple_args,
451            args_tuple_ty => args_tuple_ty,
452        },
453    ));
454}
455
456/// Build NAPI argument expressions for a visitor method.
457///
458/// Returns one expression per parameter, each producing a `napi::bindgen_prelude::Unknown`.
459fn build_napi_args(method: &MethodDef) -> Vec<String> {
460    method
461        .params
462        .iter()
463
464        .map(|p| {
465            if let TypeRef::Named(n) = &p.ty {
466                if n == "NodeContext" {
467                    return format!(
468                        "match nodecontext_to_js_object(&self.env(), {}{}) {{ Ok(o) => o.to_unknown(), Err(_) => unsafe {{ \
469                         let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
470                         napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
471                        }}",
472                        if p.is_ref { "" } else { "&" },
473                        p.name
474                    );
475                }
476            }
477            // Option<&str>
478            if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
479                return format!(
480                    "match {name} {{ \
481                     Some(s) => match self.env().create_string(s) {{ \
482                       Ok(v) => v.to_unknown(), \
483                       Err(_) => unsafe {{ \
484                       let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
485                       napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
486                     }}, \
487                     None => unsafe {{ \
488                       let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
489                       napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
490                    }}",
491                    name = p.name
492                );
493            }
494            // &str
495            if matches!(&p.ty, TypeRef::String) && p.is_ref {
496                return format!(
497                    "match self.env().create_string({name}) {{ \
498                     Ok(s) => s.to_unknown(), \
499                     Err(_) => unsafe {{ \
500                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
501                     napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
502                    }}",
503                    name = p.name
504                );
505            }
506            // String (owned)
507            if matches!(&p.ty, TypeRef::String) {
508                return format!(
509                    "match self.env().create_string({name}.as_str()) {{ \
510                     Ok(s) => s.to_unknown(), \
511                     Err(_) => unsafe {{ \
512                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
513                     napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
514                    }}",
515                    name = p.name
516                );
517            }
518            // Bool
519            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
520                return format!(
521                    "unsafe {{ \
522                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), {name}).unwrap_or(std::ptr::null_mut()); \
523                     napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }}",
524                    name = p.name
525                );
526            }
527            // u32 / usize: create_uint32 needs a u32; usize requires the cast but u32 does not.
528            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::U32)) {
529                return format!(
530                    "match self.env().create_uint32({name}) {{ Ok(n) => n.to_unknown(), Err(_) => unsafe {{ \
531                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
532                     napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
533                    }}",
534                    name = p.name
535                );
536            }
537            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Usize)) {
538                return format!(
539                    "match self.env().create_uint32({name} as u32) {{ Ok(n) => n.to_unknown(), Err(_) => unsafe {{ \
540                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
541                     napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
542                    }}",
543                    name = p.name
544                );
545            }
546            // Vec<String> or &[String] - serialize to JSON string as fallback
547            // Default: serialize as debug string
548            format!(
549                "match self.env().create_string(&format!(\"{{:?}}\", {name})) {{ Ok(s) => s.to_unknown(), Err(_) => unsafe {{ \
550                 let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
551                 napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }} \
552                }}",
553                name = p.name
554            )
555        })
556        .collect()
557}
558
559/// Generate a NAPI free function that has one parameter replaced by
560/// `Option<napi::bindgen_prelude::Object>` (a trait bridge). The bridge is constructed
561/// before calling the core function.
562#[allow(clippy::too_many_arguments)]
563pub fn gen_bridge_function(
564    func: &alef_core::ir::FunctionDef,
565    bridge_param_idx: usize,
566    bridge_cfg: &TraitBridgeConfig,
567    mapper: &dyn alef_codegen::type_mapper::TypeMapper,
568    _cfg: &alef_codegen::generators::RustBindingConfig<'_>,
569    _adapter_bodies: &alef_codegen::generators::AdapterBodies,
570    opaque_types: &ahash::AHashSet<String>,
571    core_import: &str,
572) -> String {
573    use alef_core::ir::TypeRef;
574
575    let struct_name = format!("Js{}Bridge", bridge_cfg.trait_name);
576    let handle_path = format!("{core_import}::visitor::VisitorHandle");
577    let param_name = &func.params[bridge_param_idx].name;
578    let bridge_param = &func.params[bridge_param_idx];
579    let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
580
581    // Check if this is an options_field binding pattern (visitor embedded in options struct)
582    let is_options_field_binding = matches!(bridge_cfg.bind_via, alef_core::config::BridgeBinding::OptionsField);
583
584    // Find the options parameter when using options_field binding
585    let options_param_idx = if is_options_field_binding {
586        func.params.iter().enumerate().find(|(_, p)| {
587            matches!(&p.ty, TypeRef::Named(n) if bridge_cfg.options_type.as_ref().is_some_and(|opt_type| n == opt_type))
588        }).map(|(i, _)| i)
589    } else {
590        None
591    };
592
593    // Build parameter list: bridge param becomes Option<Object>, no explicit env param
594    // (napi v3 does not implement FromNapiValue for Env; env is obtained from the Object)
595    let mut sig_parts = vec![];
596    for (idx, p) in func.params.iter().enumerate() {
597        if is_options_field_binding && Some(idx) == options_param_idx {
598            // For options_field binding, visitor is extracted from options, not a separate param
599            let ty = if p.optional || (idx > 0 && func.params[..idx].iter().any(|pp| pp.optional)) {
600                format!("Option<{}>", mapper.map_type(&p.ty))
601            } else {
602                mapper.map_type(&p.ty)
603            };
604            sig_parts.push(format!("{}: {}", p.name, ty));
605        } else if idx == bridge_param_idx {
606            if is_optional {
607                sig_parts.push(format!("{}: Option<napi::bindgen_prelude::Object>", p.name));
608            } else {
609                sig_parts.push(format!("{}: napi::bindgen_prelude::Object", p.name));
610            }
611        } else {
612            let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
613            let ty = if p.optional || promoted {
614                format!("Option<{}>", mapper.map_type(&p.ty))
615            } else {
616                mapper.map_type(&p.ty)
617            };
618            sig_parts.push(format!("{}: {}", p.name, ty));
619        }
620    }
621
622    let params_str = sig_parts.join(", ");
623    let return_type = mapper.map_type(&func.return_type);
624    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
625
626    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
627
628    // Bridge wrapping code: constructor is infallible (transmute-based).
629    let bridge_wrap = if is_optional {
630        format!(
631            "let {param_name} = {param_name}.map(|v| {{\n        \
632             let bridge = {struct_name}::new(v);\n        \
633             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
634             }});"
635        )
636    } else {
637        format!(
638            "let {param_name} = {{\n        \
639             let bridge = {struct_name}::new({param_name});\n        \
640             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
641             }};"
642        )
643    };
644
645    // Use From/Into for non-bridge Named params — the generated bindings have From impls.
646    let serde_bindings: String = func
647        .params
648        .iter()
649        .enumerate()
650        .filter(|(idx, p)| {
651            if *idx == bridge_param_idx {
652                return false;
653            }
654            let named = match &p.ty {
655                TypeRef::Named(n) => Some(n.as_str()),
656                TypeRef::Optional(inner) => {
657                    if let TypeRef::Named(n) = inner.as_ref() {
658                        Some(n.as_str())
659                    } else {
660                        None
661                    }
662                }
663                _ => None,
664            };
665            named.is_some_and(|n| !opaque_types.contains(n))
666        })
667        .map(|(_, p)| {
668            let name = &p.name;
669            let core_path = format!(
670                "{core_import}::{}",
671                match &p.ty {
672                    TypeRef::Named(n) => n.clone(),
673                    TypeRef::Optional(inner) =>
674                        if let TypeRef::Named(n) = inner.as_ref() {
675                            n.clone()
676                        } else {
677                            String::new()
678                        },
679                    _ => String::new(),
680                }
681            );
682            if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
683                format!("let {name}_core: Option<{core_path}> = {name}.map(|v| v.into());\n    ")
684            } else {
685                format!("let {name}_core: {core_path} = {name}.into();\n    ")
686            }
687        })
688        .collect();
689
690    // Build call args
691    let call_args: Vec<String> = func
692        .params
693        .iter()
694        .enumerate()
695        .map(|(idx, p)| {
696            if idx == bridge_param_idx {
697                return p.name.clone();
698            }
699            match &p.ty {
700                TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
701                    if p.optional {
702                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
703                    } else {
704                        format!("&{}.inner", p.name)
705                    }
706                }
707                TypeRef::Named(_) => format!("{}_core", p.name),
708                TypeRef::Optional(inner) => {
709                    if let TypeRef::Named(n) = inner.as_ref() {
710                        if opaque_types.contains(n.as_str()) {
711                            format!("{}.as_ref().map(|v| &v.inner)", p.name)
712                        } else {
713                            format!("{}_core", p.name)
714                        }
715                    } else {
716                        p.name.clone()
717                    }
718                }
719                TypeRef::String | TypeRef::Char => {
720                    if p.is_ref {
721                        format!("&{}", p.name)
722                    } else {
723                        p.name.clone()
724                    }
725                }
726                _ => p.name.clone(),
727            }
728        })
729        .collect();
730    let call_args_str = call_args.join(", ");
731
732    let core_fn_path = {
733        let path = func.rust_path.replace('-', "_");
734        if path.starts_with(core_import) {
735            path
736        } else {
737            format!("{core_import}::{}", func.name)
738        }
739    };
740    let core_call = format!("{core_fn_path}({call_args_str})");
741
742    let return_wrap = match &func.return_type {
743        TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
744            format!("{name} {{ inner: std::sync::Arc::new(val) }}")
745        }
746        TypeRef::Named(_) => "val.into()".to_string(),
747        TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
748        _ => "val".to_string(),
749    };
750
751    let body = if func.error_type.is_some() {
752        if return_wrap == "val" {
753            format!("{bridge_wrap}\n    {serde_bindings}{core_call}{err_conv}")
754        } else {
755            format!("{bridge_wrap}\n    {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
756        }
757    } else {
758        format!("{bridge_wrap}\n    {serde_bindings}{core_call}")
759    };
760
761    let js_name = {
762        let mut result = String::with_capacity(func.name.len());
763        let mut capitalize_next = false;
764        for (i, c) in func.name.chars().enumerate() {
765            if c == '_' {
766                capitalize_next = true;
767            } else if capitalize_next {
768                result.extend(c.to_uppercase());
769                capitalize_next = false;
770            } else if i == 0 {
771                result.extend(c.to_lowercase());
772            } else {
773                result.push(c);
774            }
775        }
776        result
777    };
778    let js_name_attr = if js_name != func.name {
779        format!("(js_name = \"{}\")", js_name)
780    } else {
781        String::new()
782    };
783
784    let func_name = &func.name;
785    crate::template_env::render(
786        "bridge_function.jinja",
787        minijinja::context! {
788            has_error => func.error_type.is_some(),
789            js_name_attr => js_name_attr,
790            func_name => func_name,
791            params_str => params_str,
792            ret => ret,
793            body => body,
794        },
795    )
796}
797
798/// Generate a NAPI free function where a trait bridge is embedded in an options struct field.
799/// The visitor is extracted from options before the Into conversion, wrapped in a bridge,
800/// and manually injected back into the converted core options.
801#[allow(clippy::too_many_arguments)]
802pub fn gen_options_field_bridge_function(
803    func: &alef_core::ir::FunctionDef,
804    options_param_idx: usize,
805    bridge_cfg: &TraitBridgeConfig,
806    mapper: &dyn alef_codegen::type_mapper::TypeMapper,
807    _cfg: &alef_codegen::generators::RustBindingConfig<'_>,
808    opaque_types: &ahash::AHashSet<String>,
809    core_import: &str,
810) -> String {
811    use alef_core::ir::TypeRef;
812
813    let struct_name = format!("Js{}Bridge", bridge_cfg.trait_name);
814    let handle_path = format!("{core_import}::visitor::VisitorHandle");
815    let options_param = &func.params[options_param_idx];
816    let options_name = &options_param.name;
817
818    // Bridge functions always treat the options param as optional: callers may pass
819    // undefined/null (no options) or an options object (with or without visitor).
820    // Even if the IR marks the param as non-optional (e.g. because has_default types
821    // get their Option<> stripped during IR parsing), we force Option<T> behavior here.
822    let is_param_optional = true;
823
824    // Whether the IR already marks the options param as Optional<T>.
825    let ir_param_optional = matches!(&options_param.ty, TypeRef::Optional(_));
826
827    // Build parameter list; force the options param to Option<T> if the IR didn't already.
828    let params_str = {
829        let mut sig_parts = vec![];
830        for (i, p) in func.params.iter().enumerate() {
831            let ty = mapper.map_type(&p.ty);
832            if i == options_param_idx && !ir_param_optional {
833                sig_parts.push(format!("{}: Option<{ty}>", p.name));
834            } else {
835                sig_parts.push(format!("{}: {ty}", p.name));
836            }
837        }
838        sig_parts.join(", ")
839    };
840
841    let return_type = mapper.map_type(&func.return_type);
842    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
843
844    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
845
846    // Generate visitor extraction and bridge creation
847    let visitor_extract = if is_param_optional {
848        format!(
849            "let visitor_handle = {options_name}.as_ref().and_then(|o| o.visitor.clone()).map(|v| {{\n    \
850             let bridge = {struct_name}::new(v);\n    \
851             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n\
852             }});"
853        )
854    } else {
855        format!(
856            "let visitor_handle = {options_name}.visitor.clone().map(|v| {{\n    \
857             let bridge = {struct_name}::new(v);\n    \
858             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n\
859             }});"
860        )
861    };
862
863    // Generate options conversion with visitor preservation.
864    // To avoid the From impl dropping the visitor field (it's marked as Default::default()),
865    // we clear it from the cloned options before conversion, then re-inject the extracted handle.
866    // This ensures the bridge wrapper survives the conversion.
867    let options_convert = if is_param_optional {
868        format!(
869            "let {options_name}_core: Option<{core_import}::ConversionOptions> = {options_name}.map(|mut o| {{\n    \
870             o.visitor = None;\n    \
871             let mut result: {core_import}::ConversionOptions = o.into();\n    \
872             result.visitor = visitor_handle.clone();\n    \
873             result\n    \
874             }});"
875        )
876    } else {
877        format!(
878            "let {options_name}_core: Option<{core_import}::ConversionOptions> = {{\n    \
879             let mut o = {options_name}.clone();\n    \
880             o.visitor = None;\n    \
881             let mut result: {core_import}::ConversionOptions = o.into();\n    \
882             result.visitor = visitor_handle.clone();\n    \
883             Some(result)\n    \
884             }};"
885        )
886    };
887
888    // Build call args, replacing options param with the _core version
889    let call_args: String = func
890        .params
891        .iter()
892        .enumerate()
893        .map(|(idx, p)| {
894            if idx == options_param_idx {
895                format!("{options_name}_core")
896            } else {
897                match &p.ty {
898                    TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
899                        if p.optional {
900                            format!("{}.as_ref().map(|v| &v.inner)", p.name)
901                        } else {
902                            format!("&{}.inner", p.name)
903                        }
904                    }
905                    TypeRef::Named(_) => format!("{}.into()", p.name),
906                    TypeRef::Optional(inner) => {
907                        if let TypeRef::Named(n) = inner.as_ref() {
908                            if opaque_types.contains(n.as_str()) {
909                                format!("{}.as_ref().map(|v| &v.inner)", p.name)
910                            } else {
911                                format!("{}.map(Into::into)", p.name)
912                            }
913                        } else {
914                            p.name.clone()
915                        }
916                    }
917                    TypeRef::String | TypeRef::Char => {
918                        if p.is_ref {
919                            format!("&{}", p.name)
920                        } else {
921                            p.name.clone()
922                        }
923                    }
924                    _ => p.name.clone(),
925                }
926            }
927        })
928        .collect::<Vec<_>>()
929        .join(", ");
930
931    let core_fn_path = {
932        let path = func.rust_path.replace('-', "_");
933        if path.starts_with(core_import) {
934            path
935        } else {
936            format!("{core_import}::{}", func.name)
937        }
938    };
939    let core_call = format!("{core_fn_path}({call_args})");
940
941    let return_wrap = match &func.return_type {
942        TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
943            format!("{name} {{ inner: std::sync::Arc::new(val) }}")
944        }
945        TypeRef::Named(_) => "val.into()".to_string(),
946        TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
947        _ => "val".to_string(),
948    };
949
950    let body = if func.error_type.is_some() {
951        if return_wrap == "val" {
952            format!("{visitor_extract}\n    {options_convert}\n    {core_call}{err_conv}")
953        } else {
954            format!("{visitor_extract}\n    {options_convert}\n    {core_call}.map(|val| {return_wrap}){err_conv}")
955        }
956    } else {
957        format!("{visitor_extract}\n    {options_convert}\n    {core_call}")
958    };
959
960    let mut out = String::with_capacity(1024);
961    if func.error_type.is_some() {
962        out.push_str("#[allow(clippy::missing_errors_doc)]\n");
963    }
964    out.push_str("#[napi]\n");
965    let func_name = &func.name;
966    out.push_str(&crate::template_env::render(
967        "trait_bridge_fn_wrapper.jinja",
968        minijinja::context! {
969            func_name => func_name,
970            params_str => params_str,
971            return_type => ret,
972            body => body,
973        },
974    ));
975
976    out
977}