Skip to main content

alef_backend_napi/
gen_bindings.rs

1use crate::type_map::NapiMapper;
2use ahash::AHashSet;
3use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
4use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
5use alef_codegen::naming::to_node_name;
6use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
7use alef_codegen::type_mapper::TypeMapper;
8use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile, PostBuildStep};
9use alef_core::config::{AlefConfig, Language, resolve_output_dir};
10use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
11use std::path::PathBuf;
12
13pub struct NapiBackend;
14
15impl NapiBackend {
16    fn binding_config(core_import: &str) -> RustBindingConfig<'_> {
17        RustBindingConfig {
18            struct_attrs: &["napi"],
19            field_attrs: &[],
20            struct_derives: &["Clone"],
21            method_block_attr: Some("napi"),
22            constructor_attr: "#[napi(constructor)]",
23            static_attr: None,
24            function_attr: "#[napi]",
25            enum_attrs: &["napi(string_enum)"],
26            enum_derives: &["Clone"],
27            needs_signature: false,
28            signature_prefix: "",
29            signature_suffix: "",
30            core_import,
31            async_pattern: AsyncPattern::NapiNativeAsync,
32            has_serde: false,
33            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
34            type_name_prefix: "Js",
35            option_duration_on_defaults: true,
36        }
37    }
38}
39
40impl Backend for NapiBackend {
41    fn name(&self) -> &str {
42        "napi"
43    }
44
45    fn language(&self) -> Language {
46        Language::Node
47    }
48
49    fn capabilities(&self) -> Capabilities {
50        Capabilities {
51            supports_async: true,
52            supports_classes: true,
53            supports_enums: true,
54            supports_option: true,
55            supports_result: true,
56            ..Capabilities::default()
57        }
58    }
59
60    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
61        let mapper = NapiMapper;
62        let core_import = config.core_import();
63        let cfg = Self::binding_config(&core_import);
64
65        let mut builder = RustFileBuilder::new().with_generated_header();
66        builder.add_inner_attribute("allow(dead_code)");
67        builder.add_import("napi::*");
68        builder.add_import("napi_derive::napi");
69
70        // Import traits needed for trait method dispatch
71        for trait_path in generators::collect_trait_imports(api) {
72            builder.add_import(&trait_path);
73        }
74
75        // Only import HashMap when Map-typed fields or returns are present
76        let has_maps = api
77            .types
78            .iter()
79            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
80            || api
81                .functions
82                .iter()
83                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
84        if has_maps {
85            builder.add_import("std::collections::HashMap");
86        }
87
88        // Note: custom_modules for Node are TypeScript-only re-exports
89        // (used in generate_public_api), not Rust module declarations.
90
91        // Check if any function or method is async
92        let has_async =
93            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
94
95        if has_async {
96            builder.add_item(&gen_tokio_runtime());
97        }
98
99        // Check if we have opaque types and add Arc import if needed
100        let opaque_types: AHashSet<String> = api
101            .types
102            .iter()
103            .filter(|t| t.is_opaque)
104            .map(|t| t.name.clone())
105            .collect();
106        if !opaque_types.is_empty() {
107            builder.add_import("std::sync::Arc");
108        }
109
110        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
111        // and custom constructor. Use shared generators for enums and functions,
112        // but keep struct/method generation custom.
113        for typ in &api.types {
114            if typ.is_opaque {
115                builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, "Js"));
116                builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &cfg, &opaque_types));
117            } else {
118                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
119                // napi(object) structs cannot have #[napi] impl blocks.
120                // gen_struct adds Default to derives when typ.has_default is true.
121                builder.add_item(&gen_struct(typ, &mapper));
122            }
123        }
124
125        for enum_def in &api.enums {
126            builder.add_item(&gen_enum(enum_def));
127        }
128
129        for func in &api.functions {
130            builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types));
131        }
132
133        let binding_to_core = alef_codegen::conversions::convertible_types(api);
134        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
135        let input_types = alef_codegen::conversions::input_type_names(api);
136        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
137            type_name_prefix: "Js",
138            cast_large_ints_to_i64: true,
139            cast_f32_to_f64: true,
140            // optionalize_defaults: For types with has_default, conversion generators
141            // make all fields Option<T> and apply defaults via FromNapiValue,
142            // enabling JS users to pass partial objects and omit fields they want defaults for.
143            optionalize_defaults: true,
144            option_duration_on_defaults: true,
145            include_cfg_metadata: true,
146            ..Default::default()
147        };
148        // From/Into conversions using shared parameterized generators
149        for typ in &api.types {
150            if input_types.contains(&typ.name)
151                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
152            {
153                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
154                    typ,
155                    &core_import,
156                    &napi_conv_config,
157                ));
158            }
159            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
160                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
161                    typ,
162                    &core_import,
163                    &opaque_types,
164                    &napi_conv_config,
165                ));
166            }
167        }
168        for e in &api.enums {
169            let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
170            if is_tagged_data_enum {
171                // Tagged data enums use flattened struct — generate custom conversions
172                builder.add_item(&gen_tagged_enum_binding_to_core(e, &core_import));
173                builder.add_item(&gen_tagged_enum_core_to_binding(e, &core_import));
174            } else {
175                if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
176                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
177                        e,
178                        &core_import,
179                        &napi_conv_config,
180                    ));
181                }
182                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
183                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
184                        e,
185                        &core_import,
186                        &napi_conv_config,
187                    ));
188                }
189            }
190        }
191
192        // Error types (variant name constants + converter functions)
193        for error in &api.errors {
194            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
195            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
196        }
197
198        // Build adapter body map (consumed by generators via body substitution)
199        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
200
201        let content = builder.build();
202
203        let output_dir = resolve_output_dir(
204            config.output.node.as_ref(),
205            &config.crate_config.name,
206            "crates/{name}-node/src/",
207        );
208
209        Ok(vec![GeneratedFile {
210            path: PathBuf::from(&output_dir).join("lib.rs"),
211            content,
212            generated_header: false,
213        }])
214    }
215
216    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
217        // Separate exports into functions (plain export) and types (export type)
218        let mut type_exports = vec![];
219        let mut function_exports = vec![];
220
221        // Collect all types (exported with Js prefix from native module) - export type
222        for typ in &api.types {
223            type_exports.push(format!("Js{}", typ.name));
224        }
225
226        // Collect all enums as value exports (runtime objects).
227        // NAPI generates const enum in .d.ts, but we post-process it to regular enum
228        // so they can be re-exported as values with verbatimModuleSyntax.
229        for enum_def in &api.enums {
230            function_exports.push(format!("Js{}", enum_def.name));
231        }
232
233        // NAPI errors are thrown as native JS Error objects, not exported as TS types.
234        // Skip error types in the public API re-exports.
235
236        // Collect all functions (exported from native module) - plain export
237        for func in &api.functions {
238            // Convert snake_case to camelCase for JavaScript naming
239            let js_name = to_node_name(&func.name);
240            function_exports.push(js_name);
241        }
242
243        // Sort for consistent output
244        type_exports.sort();
245        function_exports.sort();
246
247        // Generate the index.ts re-export file using a single export block
248        // with inline `type` annotations for verbatimModuleSyntax compatibility.
249        let mut lines = vec![
250            "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
251            "".to_string(),
252        ];
253
254        // Separate value and type exports for isolatedModules compatibility.
255        // Value exports (functions + enums) in one block, type exports (structs) in another.
256        if !function_exports.is_empty() {
257            lines.push("export {".to_string());
258            for name in &function_exports {
259                lines.push(format!("  {name},"));
260            }
261            lines.push(format!("}} from '{}';", config.node_package_name()));
262            lines.push("".to_string());
263        }
264        if !type_exports.is_empty() {
265            lines.push("export type {".to_string());
266            for name in &type_exports {
267                lines.push(format!("  {name},"));
268            }
269            lines.push(format!("}} from '{}';", config.node_package_name()));
270        }
271
272        // Append re-exports for custom modules (from [custom_modules] node = [...])
273        let custom_mods = config.custom_modules.for_language(Language::Node);
274        for module_name in custom_mods {
275            lines.push(format!("export * from './{module_name}';"));
276        }
277
278        let content = lines.join("\n");
279
280        // Output path: packages/typescript/src/index.ts
281        let output_path = PathBuf::from("packages/typescript/src/index.ts");
282
283        Ok(vec![GeneratedFile {
284            path: output_path,
285            content,
286            generated_header: false,
287        }])
288    }
289
290    fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
291        let content = gen_dts(api);
292
293        // `config.output.node` points to the `src/` directory (e.g., `crates/{name}-node/src/`).
294        // `index.d.ts` belongs at the crate root, one level up from `src/`.
295        // When the configured path ends in `src/` or `src`, strip that suffix to get the crate root.
296        // Falls back to `crates/{name}-node/` if no node output is configured.
297        let src_dir = resolve_output_dir(
298            config.output.node.as_ref(),
299            &config.crate_config.name,
300            "crates/{name}-node/src/",
301        );
302        let crate_root = {
303            let p = PathBuf::from(&src_dir);
304            match p.file_name().and_then(|n| n.to_str()) {
305                Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
306                _ => p,
307            }
308        };
309
310        Ok(vec![GeneratedFile {
311            path: crate_root.join("index.d.ts"),
312            content,
313            generated_header: false,
314        }])
315    }
316
317    fn build_config(&self) -> Option<BuildConfig> {
318        Some(BuildConfig {
319            tool: "napi",
320            crate_suffix: "-node",
321            depends_on_ffi: false,
322            post_build: vec![PostBuildStep::PatchFile {
323                path: "index.d.ts",
324                find: "export declare const enum",
325                replace: "export declare enum",
326            }],
327        })
328    }
329}
330
331/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
332fn gen_struct(typ: &TypeDef, mapper: &NapiMapper) -> String {
333    let mut struct_builder = StructBuilder::new(&format!("Js{}", typ.name));
334    // Use napi(object) so the struct can be used as function/method parameters (FromNapiValue)
335    struct_builder.add_attr("napi(object)");
336    struct_builder.add_derive("Clone");
337    // Types with has_default get #[derive(Default)] instead of a manual impl.
338    if typ.has_default {
339        struct_builder.add_derive("Default");
340    }
341
342    for field in &typ.fields {
343        let mapped_type = mapper.map_type(&field.ty);
344        // For types with Default, make all fields optional so JS callers
345        // can pass partial objects (missing fields get defaults).
346        let field_type = if field.optional || typ.has_default {
347            format!("Option<{}>", mapped_type)
348        } else {
349            mapped_type
350        };
351        let js_name = to_node_name(&field.name);
352        let attrs = if js_name != field.name {
353            vec![format!("napi(js_name = \"{}\")", js_name)]
354        } else {
355            vec![]
356        };
357        struct_builder.add_field(&field.name, &field_type, attrs);
358    }
359
360    struct_builder.build()
361}
362
363/// Generate NAPI methods for an opaque struct (delegates to self.inner).
364fn gen_opaque_struct_methods(
365    typ: &TypeDef,
366    mapper: &NapiMapper,
367    cfg: &RustBindingConfig,
368    opaque_types: &AHashSet<String>,
369) -> String {
370    let mut impl_builder = ImplBuilder::new(&format!("Js{}", typ.name));
371    impl_builder.add_attr("napi");
372
373    let (instance, statics) = partition_methods(&typ.methods);
374
375    for method in &instance {
376        impl_builder.add_method(&gen_opaque_instance_method(method, mapper, typ, cfg, opaque_types));
377    }
378    for method in &statics {
379        impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types));
380    }
381
382    impl_builder.build()
383}
384
385/// Generate an opaque instance method that delegates to self.inner.
386fn gen_opaque_instance_method(
387    method: &MethodDef,
388    mapper: &NapiMapper,
389    typ: &TypeDef,
390    cfg: &RustBindingConfig,
391    opaque_types: &AHashSet<String>,
392) -> String {
393    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
394    let return_type = mapper.map_type(&method.return_type);
395    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
396
397    let js_name = to_node_name(&method.name);
398    let js_name_attr = if js_name != method.name {
399        format!("(js_name = \"{}\")", js_name)
400    } else {
401        String::new()
402    };
403
404    let async_kw = if method.is_async { "async " } else { "" };
405
406    let type_name = &typ.name;
407    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
408    let call_args = napi_gen_call_args(&method.params, opaque_types);
409
410    // Use the shared can_auto_delegate check for opaque instance methods.
411    let opaque_can_delegate = !method.sanitized
412        && (!is_owned_receiver || typ.is_clone)
413        && method
414            .params
415            .iter()
416            .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
417        && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
418
419    let make_core_call = |method_name: &str| -> String {
420        if is_owned_receiver {
421            format!("(*self.inner).clone().{method_name}({call_args})")
422        } else {
423            format!("self.inner.{method_name}({call_args})")
424        }
425    };
426
427    let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
428
429    let async_result_wrap = napi_wrap_return(
430        "result",
431        &method.return_type,
432        type_name,
433        opaque_types,
434        true,
435        method.returns_ref,
436    );
437
438    let body = if !opaque_can_delegate {
439        // Try serde-based param conversion for methods with non-opaque Named params
440        if cfg.has_serde
441            && !method.sanitized
442            && generators::has_named_params(&method.params, opaque_types)
443            && method.error_type.is_some()
444            && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
445        {
446            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
447            let serde_bindings =
448                generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
449            let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
450            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
451            if matches!(method.return_type, TypeRef::Unit) {
452                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
453            } else {
454                let wrap = napi_wrap_return(
455                    "result",
456                    &method.return_type,
457                    type_name,
458                    opaque_types,
459                    true,
460                    method.returns_ref,
461                );
462                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
463            }
464        } else {
465            generators::gen_unimplemented_body(
466                &method.return_type,
467                &format!("{type_name}.{}", method.name),
468                method.error_type.is_some(),
469                cfg,
470                &method.params,
471            )
472        }
473    } else if method.is_async {
474        let inner_clone_line = "let inner = self.inner.clone();\n    ";
475        let core_call_str = make_async_core_call(&method.name);
476        generators::gen_async_body(
477            &core_call_str,
478            cfg,
479            method.error_type.is_some(),
480            &async_result_wrap,
481            true,
482            inner_clone_line,
483            matches!(method.return_type, TypeRef::Unit),
484        )
485    } else {
486        let core_call = make_core_call(&method.name);
487        if method.error_type.is_some() {
488            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
489            if matches!(method.return_type, TypeRef::Unit) {
490                format!("{core_call}{err_conv}?;\n    Ok(())")
491            } else {
492                let wrap = napi_wrap_return(
493                    "result",
494                    &method.return_type,
495                    type_name,
496                    opaque_types,
497                    true,
498                    method.returns_ref,
499                );
500                format!("let result = {core_call}{err_conv}?;\n    Ok({wrap})")
501            }
502        } else {
503            napi_wrap_return(
504                &core_call,
505                &method.return_type,
506                type_name,
507                opaque_types,
508                true,
509                method.returns_ref,
510            )
511        }
512    };
513
514    let mut attrs = String::new();
515    // Per-item clippy suppression: too_many_arguments when >7 params (including &self)
516    if method.params.len() + 1 > 7 {
517        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
518    }
519    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
520    if method.error_type.is_some() {
521        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
522    }
523    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
524    if generators::is_trait_method_name(&method.name) {
525        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
526    }
527    format!(
528        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n    \
529         {body}\n}}",
530        method.name
531    )
532}
533
534/// Generate a static method binding.
535fn gen_static_method(
536    method: &MethodDef,
537    mapper: &NapiMapper,
538    typ: &TypeDef,
539    cfg: &RustBindingConfig,
540    opaque_types: &AHashSet<String>,
541) -> String {
542    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
543    let return_type = mapper.map_type(&method.return_type);
544    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
545
546    let js_name = to_node_name(&method.name);
547    let js_name_attr = if js_name != method.name {
548        format!("(js_name = \"{}\")", js_name)
549    } else {
550        String::new()
551    };
552
553    let type_name = &typ.name;
554    let core_type_path = typ.rust_path.replace('-', "_");
555    let call_args = napi_gen_call_args(&method.params, opaque_types);
556    let can_delegate_static = can_auto_delegate(method, opaque_types);
557
558    let async_kw = if method.is_async { "async " } else { "" };
559
560    let body = if !can_delegate_static {
561        generators::gen_unimplemented_body(
562            &method.return_type,
563            &format!("{type_name}::{}", method.name),
564            method.error_type.is_some(),
565            cfg,
566            &method.params,
567        )
568    } else if method.is_async {
569        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
570        let return_wrap = napi_wrap_return(
571            "result",
572            &method.return_type,
573            type_name,
574            opaque_types,
575            typ.is_opaque,
576            method.returns_ref,
577        );
578        generators::gen_async_body(
579            &core_call,
580            cfg,
581            method.error_type.is_some(),
582            &return_wrap,
583            false,
584            "",
585            matches!(method.return_type, TypeRef::Unit),
586        )
587    } else {
588        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
589        if method.error_type.is_some() {
590            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
591            let wrapped = napi_wrap_return(
592                "val",
593                &method.return_type,
594                type_name,
595                opaque_types,
596                typ.is_opaque,
597                method.returns_ref,
598            );
599            if wrapped == "val" {
600                format!("{core_call}{err_conv}")
601            } else {
602                format!("{core_call}.map(|val| {wrapped}){err_conv}")
603            }
604        } else {
605            napi_wrap_return(
606                &core_call,
607                &method.return_type,
608                type_name,
609                opaque_types,
610                typ.is_opaque,
611                method.returns_ref,
612            )
613        }
614    };
615
616    let mut attrs = String::new();
617    // Per-item clippy suppression: too_many_arguments when >7 params
618    if method.params.len() > 7 {
619        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
620    }
621    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
622    if method.error_type.is_some() {
623        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
624    }
625    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
626    if generators::is_trait_method_name(&method.name) {
627        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
628    }
629    format!(
630        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
631         {body}\n}}",
632        method.name
633    )
634}
635
636/// Generate a NAPI enum definition using string_enum with Js prefix.
637/// Generate a NAPI enum definition.
638/// For simple enums (no variant fields): generates `#[napi(string_enum)]`.
639/// For tagged enums with data fields: generates a flattened `#[napi(object)]` struct
640/// with a discriminant field and all variant fields as optional.
641fn gen_enum(enum_def: &EnumDef) -> String {
642    let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
643
644    if is_tagged_data_enum {
645        return gen_tagged_enum_as_object(enum_def);
646    }
647
648    // Simple string enum
649    let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
650        "snake_case" => Some("snake_case"),
651        "camelCase" => Some("camelCase"),
652        "kebab-case" => Some("kebab-case"),
653        "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
654        "lowercase" => Some("lowercase"),
655        "UPPERCASE" => Some("UPPERCASE"),
656        "PascalCase" => Some("PascalCase"),
657        _ => None,
658    });
659
660    let string_enum_attr = match napi_case {
661        Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
662        None => "#[napi(string_enum)]".to_string(),
663    };
664
665    let mut lines = vec![
666        string_enum_attr,
667        "#[derive(Clone)]".to_string(),
668        format!("pub enum Js{} {{", enum_def.name),
669    ];
670
671    for variant in &enum_def.variants {
672        lines.push(format!("    {},", variant.name));
673    }
674
675    lines.push("}".to_string());
676
677    // Default impl for config constructor unwrap_or_default()
678    if let Some(first) = enum_def.variants.first() {
679        lines.push(String::new());
680        lines.push("#[allow(clippy::derivable_impls)]".to_string());
681        lines.push(format!("impl Default for Js{} {{", enum_def.name));
682        lines.push(format!("    fn default() -> Self {{ Self::{} }}", first.name));
683        lines.push("}".to_string());
684    }
685
686    lines.join("\n")
687}
688
689/// Generate a tagged enum as a flattened `#[napi(object)]` struct.
690/// E.g. `AuthConfig { Basic { username, password }, Bearer { token } }` becomes:
691/// ```rust,ignore
692/// #[napi(object)]
693/// struct JsAuthConfig {
694///     #[napi(js_name = "type")]
695///     pub auth_type: String,
696///     pub username: Option<String>,
697///     pub password: Option<String>,
698///     pub token: Option<String>,
699/// }
700/// ```
701fn gen_tagged_enum_as_object(enum_def: &EnumDef) -> String {
702    use alef_codegen::type_mapper::TypeMapper;
703    let mapper = NapiMapper;
704
705    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
706
707    let mut lines = vec![
708        "#[derive(Clone)]".to_string(),
709        "#[napi(object)]".to_string(),
710        format!("pub struct Js{} {{", enum_def.name),
711        format!("    #[napi(js_name = \"{tag_field}\")]"),
712        format!("    pub {tag_field}_tag: String,"),
713    ];
714
715    // Collect all unique fields across all variants (all made optional)
716    let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
717    for variant in &enum_def.variants {
718        for field in &variant.fields {
719            if seen_fields.insert(field.name.clone()) {
720                let field_type = mapper.map_type(&field.ty);
721                let js_name = alef_codegen::naming::to_node_name(&field.name);
722                if js_name != field.name {
723                    lines.push(format!("    #[napi(js_name = \"{js_name}\")]"));
724                }
725                lines.push(format!("    pub {}: Option<{field_type}>,", field.name));
726            }
727        }
728    }
729
730    lines.push("}".to_string());
731
732    // Default impl
733    lines.push(String::new());
734    lines.push("#[allow(clippy::derivable_impls)]".to_string());
735    lines.push(format!("impl Default for Js{} {{", enum_def.name));
736    lines.push(format!(
737        "    fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
738        seen_fields
739            .iter()
740            .map(|f| format!("{f}: None"))
741            .collect::<Vec<_>>()
742            .join(", ")
743    ));
744    lines.push("}".to_string());
745
746    lines.join("\n")
747}
748
749/// Generate a free function binding.
750fn gen_function(
751    func: &FunctionDef,
752    mapper: &NapiMapper,
753    cfg: &RustBindingConfig,
754    opaque_types: &AHashSet<String>,
755) -> String {
756    let params = function_params(&func.params, &|ty| {
757        // Opaque Named params must be received by reference since NAPI opaque
758        // structs don't implement FromNapiValue (they use Arc<T> internally).
759        if let TypeRef::Named(n) = ty {
760            if opaque_types.contains(n.as_str()) {
761                return format!("&Js{n}");
762            }
763        }
764        mapper.map_type(ty)
765    });
766    let return_type = mapper.map_type(&func.return_type);
767    let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
768
769    let js_name = to_node_name(&func.name);
770    let js_name_attr = if js_name != func.name {
771        format!("(js_name = \"{}\")", js_name)
772    } else {
773        String::new()
774    };
775
776    let core_import = cfg.core_import;
777    let core_fn_path = {
778        let path = func.rust_path.replace('-', "_");
779        if path.starts_with(core_import) {
780            path
781        } else {
782            format!("{core_import}::{}", func.name)
783        }
784    };
785
786    // Use let-binding pattern for non-opaque Named params
787    let use_let_bindings = generators::has_named_params(&func.params, opaque_types);
788    let call_args = if use_let_bindings {
789        generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
790    } else {
791        napi_gen_call_args(&func.params, opaque_types)
792    };
793
794    let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
795
796    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
797
798    let async_kw = if func.is_async { "async " } else { "" };
799
800    let body = if !can_delegate_fn {
801        // Try serde-based conversion for non-delegatable functions with Named params
802        if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
803            let serde_bindings =
804                generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, "    ");
805            let core_call = format!("{core_fn_path}({call_args})");
806
807            if matches!(func.return_type, TypeRef::Unit) {
808                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
809            } else {
810                let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
811                if wrapped == "val" {
812                    format!("{serde_bindings}{core_call}{err_conv}")
813                } else {
814                    format!("{serde_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
815                }
816            }
817        } else {
818            generators::gen_unimplemented_body(
819                &func.return_type,
820                &func.name,
821                func.error_type.is_some(),
822                cfg,
823                &func.params,
824            )
825        }
826    } else if func.is_async {
827        let core_call = format!("{core_fn_path}({call_args})");
828        let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref);
829        generators::gen_async_body(
830            &core_call,
831            cfg,
832            func.error_type.is_some(),
833            &return_wrap,
834            false,
835            "",
836            matches!(func.return_type, TypeRef::Unit),
837        )
838    } else {
839        let core_call = format!("{core_fn_path}({call_args})");
840        // Generate let bindings for Named params if needed
841        let let_bindings = if use_let_bindings {
842            generators::gen_named_let_bindings_pub(&func.params, opaque_types)
843        } else {
844            String::new()
845        };
846
847        if func.error_type.is_some() {
848            let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
849            if wrapped == "val" {
850                format!("{let_bindings}{core_call}{err_conv}")
851            } else {
852                format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
853            }
854        } else {
855            format!(
856                "{let_bindings}{}",
857                napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref)
858            )
859        }
860    };
861
862    let mut attrs = String::new();
863    // Per-item clippy suppression: too_many_arguments when >7 params
864    if func.params.len() > 7 {
865        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
866    }
867    // Per-item clippy suppression: missing_errors_doc for Result-returning functions
868    if func.error_type.is_some() {
869        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
870    }
871    format!(
872        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
873         {body}\n}}",
874        func.name
875    )
876}
877
878/// NAPI-specific call args that casts i64 params to u64/usize where the core expects it.
879fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
880    params
881        .iter()
882        .map(|p| match &p.ty {
883            TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
884                let core_ty = core_prim_str(prim);
885                if p.optional {
886                    format!("{}.map(|v| v as {})", p.name, core_ty)
887                } else {
888                    format!("{} as {}", p.name, core_ty)
889                }
890            }
891            TypeRef::Duration => {
892                if p.optional {
893                    format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
894                } else {
895                    format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
896                }
897            }
898            TypeRef::Named(name) if opaque_types.contains(name.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(_) => {
906                if p.optional {
907                    format!("{}.map(Into::into)", p.name)
908                } else {
909                    format!("{}.into()", p.name)
910                }
911            }
912            TypeRef::String | TypeRef::Char => format!("&{}", p.name),
913            TypeRef::Path => format!("std::path::PathBuf::from({})", p.name),
914            TypeRef::Bytes => format!("&{}", p.name),
915            _ => p.name.clone(),
916        })
917        .collect::<Vec<_>>()
918        .join(", ")
919}
920
921/// NAPI-specific return wrapping for opaque instance methods.
922/// Extends the shared `wrap_return` with i64 casts for u64/usize/isize primitives.
923fn napi_wrap_return(
924    expr: &str,
925    return_type: &TypeRef,
926    type_name: &str,
927    opaque_types: &AHashSet<String>,
928    self_is_opaque: bool,
929    returns_ref: bool,
930) -> String {
931    match return_type {
932        TypeRef::Primitive(p) if needs_napi_cast(p) => {
933            format!("{expr} as i64")
934        }
935        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
936        // Opaque Named returns need Js prefix
937        TypeRef::Named(n) if n == type_name && self_is_opaque => {
938            if returns_ref {
939                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
940            } else {
941                format!("Self {{ inner: Arc::new({expr}) }}")
942            }
943        }
944        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
945            if returns_ref {
946                format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
947            } else {
948                format!("Js{n} {{ inner: Arc::new({expr}) }}")
949            }
950        }
951        TypeRef::Named(_) => {
952            if returns_ref {
953                format!("{expr}.clone().into()")
954            } else {
955                format!("{expr}.into()")
956            }
957        }
958        _ => generators::wrap_return(
959            expr,
960            return_type,
961            type_name,
962            opaque_types,
963            self_is_opaque,
964            returns_ref,
965            false,
966        ),
967    }
968}
969
970/// NAPI-specific return wrapping for free functions (no type_name context).
971fn napi_wrap_return_fn(
972    expr: &str,
973    return_type: &TypeRef,
974    opaque_types: &AHashSet<String>,
975    returns_ref: bool,
976) -> String {
977    match return_type {
978        TypeRef::Primitive(p) if needs_napi_cast(p) => {
979            format!("{expr} as i64")
980        }
981        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
982        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
983            if returns_ref {
984                format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
985            } else {
986                format!("Js{n} {{ inner: Arc::new({expr}) }}")
987            }
988        }
989        TypeRef::Named(_) => {
990            if returns_ref {
991                format!("{expr}.clone().into()")
992            } else {
993                format!("{expr}.into()")
994            }
995        }
996        TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
997            if returns_ref {
998                format!("{expr}.into()")
999            } else {
1000                expr.to_string()
1001            }
1002        }
1003        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1004        TypeRef::Json => format!("{expr}.to_string()"),
1005        TypeRef::Optional(inner) => match inner.as_ref() {
1006            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1007                if returns_ref {
1008                    format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v.clone()) }})")
1009                } else {
1010                    format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v) }})")
1011                }
1012            }
1013            TypeRef::Named(_) => {
1014                if returns_ref {
1015                    format!("{expr}.map(|v| v.clone().into())")
1016                } else {
1017                    format!("{expr}.map(Into::into)")
1018                }
1019            }
1020            TypeRef::Path => {
1021                format!("{expr}.map(Into::into)")
1022            }
1023            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1024                if returns_ref {
1025                    format!("{expr}.map(Into::into)")
1026                } else {
1027                    expr.to_string()
1028                }
1029            }
1030            _ => expr.to_string(),
1031        },
1032        TypeRef::Vec(inner) => match inner.as_ref() {
1033            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1034                if returns_ref {
1035                    format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1036                } else {
1037                    format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v) }}).collect()")
1038                }
1039            }
1040            TypeRef::Named(_) => {
1041                if returns_ref {
1042                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1043                } else {
1044                    format!("{expr}.into_iter().map(Into::into).collect()")
1045                }
1046            }
1047            TypeRef::Path => {
1048                format!("{expr}.into_iter().map(Into::into).collect()")
1049            }
1050            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1051                if returns_ref {
1052                    format!("{expr}.into_iter().map(Into::into).collect()")
1053                } else {
1054                    expr.to_string()
1055                }
1056            }
1057            _ => expr.to_string(),
1058        },
1059        _ => expr.to_string(),
1060    }
1061}
1062
1063fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1064    matches!(
1065        p,
1066        alef_core::ir::PrimitiveType::U64 | alef_core::ir::PrimitiveType::Usize | alef_core::ir::PrimitiveType::Isize
1067    )
1068}
1069
1070fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1071    match p {
1072        alef_core::ir::PrimitiveType::U64 => "u64",
1073        alef_core::ir::PrimitiveType::Usize => "usize",
1074        alef_core::ir::PrimitiveType::Isize => "isize",
1075        _ => unreachable!(),
1076    }
1077}
1078
1079/// Generate a global Tokio runtime for NAPI async support.
1080fn gen_tokio_runtime() -> String {
1081    "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1082    tokio::runtime::Builder::new_multi_thread()
1083        .enable_all()
1084        .build()
1085        .expect(\"Failed to create Tokio runtime\")
1086});"
1087    .to_string()
1088}
1089
1090/// Generate an `index.d.ts` file for the NAPI binding crate.
1091///
1092/// NAPI-RS generates `const enum` in its auto-generated `.d.ts`, which is incompatible
1093/// with `verbatimModuleSyntax` (const enums cannot be re-exported as values). This
1094/// function produces an equivalent `.d.ts` with `export declare enum` (regular enum)
1095/// so the file can be committed and used directly without a post-build patch step.
1096///
1097/// The output format matches what NAPI-RS would generate after patching, using the same
1098/// alphabetical ordering and type declarations seen in the committed `index.d.ts` files.
1099fn gen_dts(api: &ApiSurface) -> String {
1100    let mut lines: Vec<String> = vec![
1101        "/* auto-generated by alef */".to_string(),
1102        "/* eslint-disable */".to_string(),
1103    ];
1104
1105    // Collect all declarations: opaque types (classes), plain structs (interfaces), enums, functions.
1106    // Sort each group alphabetically to produce stable, deterministic output.
1107
1108    // Opaque types → `export declare class`
1109    let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1110    opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1111
1112    // Plain structs → `export interface`
1113    let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1114    plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1115
1116    // Enums → `export declare enum`
1117    let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1118    sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1119
1120    // Functions → `export declare function`
1121    let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1122    sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1123
1124    // Build a merged list of all declarations sorted by their Js-prefixed name so the
1125    // output is fully alphabetical (matching the committed index.d.ts format).
1126    enum Decl<'a> {
1127        Class(&'a TypeDef),
1128        Interface(&'a TypeDef),
1129        Enum(&'a EnumDef),
1130        Function(&'a FunctionDef),
1131    }
1132
1133    let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1134    for t in &opaque_types {
1135        all_decls.push((format!("Js{}", t.name), Decl::Class(t)));
1136    }
1137    for t in &plain_types {
1138        all_decls.push((format!("Js{}", t.name), Decl::Interface(t)));
1139    }
1140    for e in &sorted_enums {
1141        all_decls.push((format!("Js{}", e.name), Decl::Enum(e)));
1142    }
1143    for f in &sorted_fns {
1144        all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1145    }
1146    all_decls.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
1147
1148    for (_, decl) in &all_decls {
1149        lines.push(String::new());
1150        match decl {
1151            Decl::Class(typ) => {
1152                lines.push(format!("export declare class Js{} {{", typ.name));
1153                for method in &typ.methods {
1154                    let js_name = to_node_name(&method.name);
1155                    let params = dts_params(&method.params);
1156                    let ret = dts_return_type(&method.return_type, method.error_type.is_some(), method.is_async);
1157                    if method.is_static {
1158                        lines.push(format!("  static {js_name}({params}): {ret}"));
1159                    } else {
1160                        lines.push(format!("  {js_name}({params}): {ret}"));
1161                    }
1162                }
1163                lines.push("}".to_string());
1164            }
1165            Decl::Interface(typ) => {
1166                lines.push(format!("export interface Js{} {{", typ.name));
1167                for field in &typ.fields {
1168                    let js_name = to_node_name(&field.name);
1169                    let ts_ty = dts_type(&field.ty);
1170                    // All fields on plain structs are optional (NAPI napi(object) makes them Option).
1171                    lines.push(format!("  {js_name}?: {ts_ty}"));
1172                }
1173                lines.push("}".to_string());
1174            }
1175            Decl::Enum(e) => {
1176                lines.push(format!("export declare enum Js{} {{", e.name));
1177                for variant in &e.variants {
1178                    // NAPI string_enum: variant values are the variant name as a string literal.
1179                    let value = variant.serde_rename.as_deref().unwrap_or(variant.name.as_str());
1180                    lines.push(format!("  {} = '{}',", variant.name, value));
1181                }
1182                lines.push("}".to_string());
1183            }
1184            Decl::Function(func) => {
1185                let js_name = to_node_name(&func.name);
1186                let params = dts_params(&func.params);
1187                let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async);
1188                lines.push(format!("export declare function {js_name}({params}): {ret}"));
1189            }
1190        }
1191    }
1192
1193    lines.push(String::new());
1194    lines.join("\n")
1195}
1196
1197/// Map an IR `TypeRef` to its TypeScript equivalent for `.d.ts` generation.
1198fn dts_type(ty: &TypeRef) -> String {
1199    match ty {
1200        TypeRef::Primitive(p) => match p {
1201            alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1202            alef_core::ir::PrimitiveType::U8
1203            | alef_core::ir::PrimitiveType::U16
1204            | alef_core::ir::PrimitiveType::U32
1205            | alef_core::ir::PrimitiveType::I8
1206            | alef_core::ir::PrimitiveType::I16
1207            | alef_core::ir::PrimitiveType::I32
1208            | alef_core::ir::PrimitiveType::F32
1209            | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1210            // NAPI maps u64/usize/isize to i64 on the Rust side; JS sees it as number.
1211            alef_core::ir::PrimitiveType::U64
1212            | alef_core::ir::PrimitiveType::I64
1213            | alef_core::ir::PrimitiveType::Usize
1214            | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1215        },
1216        TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1217        TypeRef::Bytes => "Uint8Array".to_string(),
1218        TypeRef::Json => "string".to_string(),
1219        TypeRef::Duration => "number".to_string(),
1220        TypeRef::Unit => "void".to_string(),
1221        TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner)),
1222        TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner)),
1223        TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k), dts_type(v)),
1224        TypeRef::Named(name) => format!("Js{name}"),
1225    }
1226}
1227
1228/// Render a list of parameters as a TypeScript parameter string for `.d.ts`.
1229fn dts_params(params: &[ParamDef]) -> String {
1230    params
1231        .iter()
1232        .map(|p| {
1233            let js_name = to_node_name(&p.name);
1234            let ts_ty = dts_type(&p.ty);
1235            if p.optional {
1236                format!("{js_name}?: {ts_ty} | undefined | null")
1237            } else {
1238                format!("{js_name}: {ts_ty}")
1239            }
1240        })
1241        .collect::<Vec<_>>()
1242        .join(", ")
1243}
1244
1245/// Render the TypeScript return type for a function/method in `.d.ts`.
1246///
1247/// Async functions return `Promise<T>`. Functions that can error still return `T`
1248/// (NAPI throws JS exceptions on error, so the `.d.ts` signature just shows the success type).
1249fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool) -> String {
1250    let base = match ret {
1251        TypeRef::Unit => "void".to_string(),
1252        other => dts_type(other),
1253    };
1254    if is_async { format!("Promise<{base}>") } else { base }
1255}
1256
1257/// Generate `From<JsTaggedEnum> for core::TaggedEnum` for a flattened struct representation.
1258fn gen_tagged_enum_binding_to_core(enum_def: &EnumDef, core_import: &str) -> String {
1259    use alef_core::ir::TypeRef;
1260    use std::fmt::Write;
1261    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1262    let binding_name = format!("Js{}", enum_def.name);
1263    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1264
1265    let mut out = String::with_capacity(512);
1266    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1267    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
1268    writeln!(out, "        match val.{tag_field}_tag.as_str() {{").ok();
1269
1270    for variant in &enum_def.variants {
1271        let default_tag = variant.name.to_lowercase();
1272        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1273        if variant.fields.is_empty() {
1274            writeln!(out, "            \"{tag_value}\" => Self::{},", variant.name).ok();
1275        } else {
1276            let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1277            let field_exprs: Vec<String> = variant
1278                .fields
1279                .iter()
1280                .map(|f| {
1281                    if f.optional {
1282                        format!("val.{}", f.name)
1283                    } else if f.sanitized {
1284                        "Default::default()".to_string()
1285                    } else {
1286                        match &f.ty {
1287                            TypeRef::Named(_) => {
1288                                format!("val.{}.unwrap_or_default().into()", f.name)
1289                            }
1290                            _ => {
1291                                format!("val.{}.unwrap_or_default()", f.name)
1292                            }
1293                        }
1294                    }
1295                })
1296                .collect();
1297            if is_tuple {
1298                writeln!(
1299                    out,
1300                    "            \"{tag_value}\" => Self::{}({}),",
1301                    variant.name,
1302                    field_exprs.join(", ")
1303                )
1304                .ok();
1305            } else {
1306                let field_inits: Vec<String> = variant
1307                    .fields
1308                    .iter()
1309                    .zip(field_exprs.iter())
1310                    .map(|(f, expr)| format!("{}: {expr}", f.name))
1311                    .collect();
1312                writeln!(
1313                    out,
1314                    "            \"{tag_value}\" => Self::{} {{ {} }},",
1315                    variant.name,
1316                    field_inits.join(", ")
1317                )
1318                .ok();
1319            }
1320        }
1321    }
1322
1323    // Default fallback to first variant
1324    if let Some(first) = enum_def.variants.first() {
1325        if first.fields.is_empty() {
1326            writeln!(out, "            _ => Self::{},", first.name).ok();
1327        } else {
1328            let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1329            if is_tuple {
1330                let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1331                writeln!(out, "            _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1332            } else {
1333                let defaults: Vec<String> = first
1334                    .fields
1335                    .iter()
1336                    .map(|f| format!("{}: Default::default()", f.name))
1337                    .collect();
1338                writeln!(
1339                    out,
1340                    "            _ => Self::{} {{ {} }},",
1341                    first.name,
1342                    defaults.join(", ")
1343                )
1344                .ok();
1345            }
1346        }
1347    }
1348
1349    writeln!(out, "        }}").ok();
1350    writeln!(out, "    }}").ok();
1351    write!(out, "}}").ok();
1352    out
1353}
1354
1355/// Generate `From<core::TaggedEnum> for JsTaggedEnum` for a flattened struct representation.
1356fn gen_tagged_enum_core_to_binding(enum_def: &EnumDef, core_import: &str) -> String {
1357    use std::fmt::Write;
1358    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1359    let binding_name = format!("Js{}", enum_def.name);
1360    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1361
1362    // Collect all field names across all variants
1363    let all_fields: Vec<String> = {
1364        let mut fields = std::collections::BTreeSet::new();
1365        for v in &enum_def.variants {
1366            for f in &v.fields {
1367                fields.insert(f.name.clone());
1368            }
1369        }
1370        fields.into_iter().collect()
1371    };
1372
1373    let mut out = String::with_capacity(512);
1374    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1375    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
1376    writeln!(out, "        match val {{").ok();
1377
1378    for variant in &enum_def.variants {
1379        let default_tag = variant.name.to_lowercase();
1380        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1381        let _variant_field_names: std::collections::BTreeSet<String> =
1382            variant.fields.iter().map(|f| f.name.clone()).collect();
1383
1384        if variant.fields.is_empty() {
1385            writeln!(
1386                out,
1387                "            {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1388                variant.name,
1389                all_fields
1390                    .iter()
1391                    .map(|f| format!("{f}: None"))
1392                    .collect::<Vec<_>>()
1393                    .join(", ")
1394            )
1395            .ok();
1396        } else {
1397            use alef_core::ir::TypeRef;
1398            let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1399            let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
1400                variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
1401            let destructured: Vec<String> = variant
1402                .fields
1403                .iter()
1404                .map(|f| {
1405                    if f.sanitized {
1406                        if is_tuple {
1407                            format!("_{}", f.name)
1408                        } else {
1409                            format!("{}: _{}", f.name, f.name)
1410                        }
1411                    } else {
1412                        f.name.clone()
1413                    }
1414                })
1415                .collect();
1416            let field_inits: Vec<String> = all_fields
1417                .iter()
1418                .map(|f| {
1419                    if let Some(field) = variant_field_map.get(f.as_str()) {
1420                        if field.optional {
1421                            format!("{f}: {f}")
1422                        } else if field.sanitized {
1423                            format!("{f}: None")
1424                        } else {
1425                            match &field.ty {
1426                                TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
1427                                _ => format!("{f}: Some({f})"),
1428                            }
1429                        }
1430                    } else {
1431                        format!("{f}: None")
1432                    }
1433                })
1434                .collect();
1435            if is_tuple {
1436                writeln!(
1437                    out,
1438                    "            {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1439                    variant.name,
1440                    destructured.join(", "),
1441                    field_inits.join(", ")
1442                )
1443                .ok();
1444            } else {
1445                writeln!(
1446                    out,
1447                    "            {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1448                    variant.name,
1449                    destructured.join(", "),
1450                    field_inits.join(", ")
1451                )
1452                .ok();
1453            }
1454        }
1455    }
1456
1457    writeln!(out, "        }}").ok();
1458    writeln!(out, "    }}").ok();
1459    write!(out, "}}").ok();
1460    out
1461}