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, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
9use alef_core::config::{AlefConfig, Language, resolve_output_dir};
10use alef_core::hash::{self, CommentStyle};
11use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
12use std::path::PathBuf;
13
14pub struct NapiBackend;
15
16impl NapiBackend {
17 fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
18 RustBindingConfig {
19 struct_attrs: &["napi"],
20 field_attrs: &[],
21 struct_derives: &["Clone"],
22 method_block_attr: Some("napi"),
23 constructor_attr: "#[napi(constructor)]",
24 static_attr: None,
25 function_attr: "#[napi]",
26 enum_attrs: &["napi(string_enum)"],
27 enum_derives: &["Clone"],
28 needs_signature: false,
29 signature_prefix: "",
30 signature_suffix: "",
31 core_import,
32 async_pattern: AsyncPattern::NapiNativeAsync,
33 has_serde,
34 type_name_prefix: prefix,
36 option_duration_on_defaults: true,
37 opaque_type_names: &[],
38 }
39 }
40}
41
42impl Backend for NapiBackend {
43 fn name(&self) -> &str {
44 "napi"
45 }
46
47 fn language(&self) -> Language {
48 Language::Node
49 }
50
51 fn capabilities(&self) -> Capabilities {
52 Capabilities {
53 supports_async: true,
54 supports_classes: true,
55 supports_enums: true,
56 supports_option: true,
57 supports_result: true,
58 ..Capabilities::default()
59 }
60 }
61
62 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
63 let prefix = config.node_type_prefix();
64 let mapper = NapiMapper::new(prefix.clone());
65 let core_import = config.core_import();
66
67 let output_dir = resolve_output_dir(
69 config.output.node.as_ref(),
70 &config.crate_config.name,
71 "crates/{name}-node/src/",
72 );
73 let has_serde = alef_core::config::detect_serde_available(&output_dir);
74 let cfg = Self::binding_config(&core_import, &prefix, has_serde);
75
76 let mut builder = RustFileBuilder::new().with_generated_header();
77 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
78 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
79 builder.add_inner_attribute(
84 "allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::default_trait_access, clippy::useless_conversion, clippy::unsafe_derive_deserialize, clippy::must_use_candidate, clippy::return_self_not_must_use, clippy::use_self, clippy::missing_const_for_fn, clippy::missing_errors_doc, clippy::needless_pass_by_value, clippy::doc_markdown, clippy::derive_partial_eq_without_eq, clippy::uninlined_format_args, clippy::redundant_clone, clippy::implicit_clone, clippy::redundant_closure_for_method_calls, clippy::wildcard_imports, clippy::option_if_let_else, clippy::too_many_lines)",
85 );
86 builder.add_import("napi::*");
87 builder.add_import("napi_derive::napi");
88
89 builder.add_import("serde_json");
93
94 for trait_path in generators::collect_trait_imports(api) {
96 builder.add_import(&trait_path);
97 }
98
99 let has_maps = api
101 .types
102 .iter()
103 .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
104 || api
105 .functions
106 .iter()
107 .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
108 if has_maps {
109 builder.add_import("std::collections::HashMap");
110 }
111
112 let has_async =
117 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
118
119 if has_async {
120 builder.add_item(&gen_tokio_runtime());
121 }
122
123 let opaque_types: AHashSet<String> = api
125 .types
126 .iter()
127 .filter(|t| t.is_opaque)
128 .map(|t| t.name.clone())
129 .collect();
130 if !opaque_types.is_empty() {
131 builder.add_import("std::sync::Arc");
132 }
133
134 let exclude_types: ahash::AHashSet<String> = config
135 .node
136 .as_ref()
137 .map(|c| c.exclude_types.iter().cloned().collect())
138 .unwrap_or_default();
139
140 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
142
143 for adapter in &config.adapters {
145 match adapter.pattern {
146 alef_core::config::AdapterPattern::Streaming => {
147 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
148 if let Some(struct_code) = adapter_bodies.get(&key) {
149 builder.add_item(struct_code);
150 }
151 }
152 alef_core::config::AdapterPattern::CallbackBridge => {
153 let struct_key = format!("{}.__bridge_struct__", adapter.name);
154 let impl_key = format!("{}.__bridge_impl__", adapter.name);
155 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
156 builder.add_item(struct_code);
157 }
158 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
159 builder.add_item(impl_code);
160 }
161 }
162 _ => {}
163 }
164 }
165
166 for typ in api
170 .types
171 .iter()
172 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
173 {
174 if typ.is_opaque {
175 builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
176 typ, &cfg, &prefix,
177 ));
178 builder.add_item(&gen_opaque_struct_methods(
179 typ,
180 &mapper,
181 &cfg,
182 &opaque_types,
183 &prefix,
184 &adapter_bodies,
185 ));
186 } else {
187 builder.add_item(&gen_struct(typ, &mapper, &prefix, has_serde));
191 }
192 }
193
194 let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
196
197 for enum_def in &api.enums {
198 builder.add_item(&gen_enum(enum_def, &prefix, has_serde));
199 }
200
201 let exclude_functions: ahash::AHashSet<String> = config
202 .node
203 .as_ref()
204 .map(|c| c.exclude_functions.iter().cloned().collect())
205 .unwrap_or_default();
206
207 for func in &api.functions {
208 if exclude_functions.contains(&func.name) {
209 continue;
210 }
211 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
212 if func.sanitized && bridge_param.is_none() {
217 continue;
218 }
219 if let Some((param_idx, bridge_cfg)) = bridge_param {
220 builder.add_item(&crate::trait_bridge::gen_bridge_function(
221 func,
222 param_idx,
223 bridge_cfg,
224 &mapper,
225 &cfg,
226 &Default::default(),
227 &opaque_types,
228 &core_import,
229 ));
230 } else {
231 builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types, &prefix));
232 }
233 }
234
235 for bridge_cfg in &config.trait_bridges {
237 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
238 let bridge = crate::trait_bridge::gen_trait_bridge(
239 trait_type,
240 bridge_cfg,
241 &core_import,
242 &config.error_type(),
243 &config.error_constructor(),
244 api,
245 );
246 for imp in &bridge.imports {
247 builder.add_import(imp);
248 }
249 builder.add_item(&bridge.code);
250 }
251 }
252
253 let binding_to_core = alef_codegen::conversions::convertible_types(api);
254 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
255 let input_types = alef_codegen::conversions::input_type_names(api);
256 let napi_conv_config = alef_codegen::conversions::ConversionConfig {
257 type_name_prefix: &prefix,
258 cast_large_ints_to_i64: true,
259 cast_f32_to_f64: true,
260 optionalize_defaults: true,
264 option_duration_on_defaults: true,
265 include_cfg_metadata: true,
266 ..Default::default()
267 };
268 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
270 if input_types.contains(&typ.name)
271 && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
272 {
273 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
274 typ,
275 &core_import,
276 &napi_conv_config,
277 ));
278 }
279 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
280 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
281 typ,
282 &core_import,
283 &opaque_types,
284 &napi_conv_config,
285 ));
286 }
287 }
288 for e in &api.enums {
289 let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
290 if is_tagged_data_enum {
291 builder.add_item(&gen_tagged_enum_binding_to_core(
293 e,
294 &core_import,
295 &prefix,
296 &struct_names,
297 ));
298 builder.add_item(&gen_tagged_enum_core_to_binding(
299 e,
300 &core_import,
301 &prefix,
302 &struct_names,
303 ));
304 } else {
305 if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
306 builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
307 e,
308 &core_import,
309 &napi_conv_config,
310 ));
311 }
312 if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
313 builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
314 e,
315 &core_import,
316 &napi_conv_config,
317 ));
318 }
319 }
320 }
321
322 for error in &api.errors {
324 builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
325 builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
326 }
327
328 let content = builder.build();
329
330 let output_dir = resolve_output_dir(
331 config.output.node.as_ref(),
332 &config.crate_config.name,
333 "crates/{name}-node/src/",
334 );
335
336 Ok(vec![GeneratedFile {
337 path: PathBuf::from(&output_dir).join("lib.rs"),
338 content,
339 generated_header: false,
340 }])
341 }
342
343 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
344 let prefix = config.node_type_prefix();
345
346 let mut type_exports = vec![];
348 let mut function_exports = vec![];
349
350 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
352 type_exports.push(format!("{prefix}{}", typ.name));
353 }
354
355 for enum_def in &api.enums {
359 type_exports.push(format!("{prefix}{}", enum_def.name));
360 }
361
362 for func in &api.functions {
367 let js_name = to_node_name(&func.name);
369 function_exports.push(js_name);
370 }
371
372 type_exports.sort();
374 function_exports.sort();
375
376 let mut lines = vec![
379 "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
380 "".to_string(),
381 ];
382
383 if !function_exports.is_empty() {
386 lines.push("export {".to_string());
387 for name in &function_exports {
388 lines.push(format!(" {name},"));
389 }
390 lines.push(format!("}} from '{}';", config.node_package_name()));
391 lines.push("".to_string());
392 }
393 if !type_exports.is_empty() {
394 lines.push("export type {".to_string());
395 for name in &type_exports {
396 lines.push(format!(" {name},"));
397 }
398 lines.push(format!("}} from '{}';", config.node_package_name()));
399 }
400
401 let custom_mods = config.custom_modules.for_language(Language::Node);
403 for module_name in custom_mods {
404 lines.push(format!("export * from './{module_name}';"));
405 }
406
407 let content = lines.join("\n");
408
409 let output_path = PathBuf::from("packages/typescript/src/index.ts");
411
412 Ok(vec![GeneratedFile {
413 path: output_path,
414 content,
415 generated_header: false,
416 }])
417 }
418
419 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
420 let prefix = config.node_type_prefix();
421 let content = gen_dts(api, &prefix);
422
423 let src_dir = resolve_output_dir(
428 config.output.node.as_ref(),
429 &config.crate_config.name,
430 "crates/{name}-node/src/",
431 );
432 let crate_root = {
433 let p = PathBuf::from(&src_dir);
434 match p.file_name().and_then(|n| n.to_str()) {
435 Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
436 _ => p,
437 }
438 };
439
440 Ok(vec![GeneratedFile {
441 path: crate_root.join("index.d.ts"),
442 content,
443 generated_header: false,
444 }])
445 }
446
447 fn build_config(&self) -> Option<BuildConfig> {
448 Some(BuildConfig {
449 tool: "napi",
450 crate_suffix: "-node",
451 build_dep: BuildDependency::None,
452 post_build: vec![PostBuildStep::PatchFile {
453 path: "index.d.ts",
454 find: "export declare const enum",
455 replace: "export declare enum",
456 }],
457 })
458 }
459}
460
461fn gen_struct(typ: &TypeDef, mapper: &NapiMapper, prefix: &str, has_serde: bool) -> String {
463 let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
464 struct_builder.add_attr("napi(object)");
466 struct_builder.add_derive("Clone");
467 struct_builder.add_derive("Default");
471 if has_serde {
475 struct_builder.add_derive("serde::Serialize");
476 struct_builder.add_derive("serde::Deserialize");
477 }
478
479 for field in &typ.fields {
480 let mapped_type = mapper.map_type(&field.ty);
481 let field_type = if (field.optional || typ.has_default) && !matches!(field.ty, TypeRef::Optional(_)) {
485 format!("Option<{}>", mapped_type)
486 } else {
487 mapped_type
488 };
489 let js_name = to_node_name(&field.name);
490 let attrs = if js_name != field.name {
491 vec![format!("napi(js_name = \"{}\")", js_name)]
492 } else {
493 vec![]
494 };
495 struct_builder.add_field(&field.name, &field_type, attrs);
496 }
497
498 struct_builder.build()
499}
500
501fn gen_opaque_struct_methods(
503 typ: &TypeDef,
504 mapper: &NapiMapper,
505 cfg: &RustBindingConfig,
506 opaque_types: &AHashSet<String>,
507 prefix: &str,
508 adapter_bodies: &alef_adapters::AdapterBodies,
509) -> String {
510 let mut impl_builder = ImplBuilder::new(&format!("{prefix}{}", typ.name));
511 impl_builder.add_attr("napi");
512
513 let (instance, statics) = partition_methods(&typ.methods);
514
515 for method in &instance {
516 let adapter_key = format!("{}.{}", typ.name, method.name);
519 if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
520 continue;
521 }
522 impl_builder.add_method(&gen_opaque_instance_method(
523 method,
524 mapper,
525 typ,
526 cfg,
527 opaque_types,
528 prefix,
529 adapter_bodies,
530 ));
531 }
532 for method in &statics {
533 let adapter_key = format!("{}.{}", typ.name, method.name);
535 if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
536 continue;
537 }
538 impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types, prefix));
539 }
540
541 impl_builder.build()
542}
543
544fn gen_opaque_instance_method(
546 method: &MethodDef,
547 mapper: &NapiMapper,
548 typ: &TypeDef,
549 cfg: &RustBindingConfig,
550 opaque_types: &AHashSet<String>,
551 prefix: &str,
552 adapter_bodies: &alef_adapters::AdapterBodies,
553) -> String {
554 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
555 let return_type = mapper.map_type(&method.return_type);
556 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
557
558 let js_name = to_node_name(&method.name);
559 let js_name_attr = if js_name != method.name {
560 format!("(js_name = \"{}\")", js_name)
561 } else {
562 String::new()
563 };
564
565 let async_kw = if method.is_async { "async " } else { "" };
566
567 let type_name = &typ.name;
568 let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
569 let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
570 let call_args = napi_gen_call_args(&method.params, opaque_types);
571
572 let opaque_can_delegate = !method.sanitized
575 && !is_ref_mut_receiver
576 && (!is_owned_receiver || typ.is_clone)
577 && method
578 .params
579 .iter()
580 .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
581 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
582
583 let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
584
585 let async_result_wrap = napi_wrap_return(
586 "result",
587 &method.return_type,
588 type_name,
589 opaque_types,
590 true,
591 method.returns_ref,
592 prefix,
593 );
594
595 let adapter_key = format!("{type_name}.{}", method.name);
596 let body = if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
597 adapter_body.clone()
598 } else if !opaque_can_delegate {
599 if cfg.has_serde
601 && !method.sanitized
602 && generators::has_named_params(&method.params, opaque_types)
603 && method.error_type.is_some()
604 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
605 {
606 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
607 let serde_bindings =
608 generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, " ");
609 let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
610 let core_call = format!("self.inner.{}({serde_call_args})", method.name);
611 if matches!(method.return_type, TypeRef::Unit) {
612 format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
613 } else {
614 let wrap = napi_wrap_return(
615 "result",
616 &method.return_type,
617 type_name,
618 opaque_types,
619 true,
620 method.returns_ref,
621 prefix,
622 );
623 format!("{serde_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
624 }
625 } else {
626 generators::gen_unimplemented_body(
627 &method.return_type,
628 &format!("{type_name}.{}", method.name),
629 method.error_type.is_some(),
630 cfg,
631 &method.params,
632 opaque_types,
633 )
634 }
635 } else if method.is_async {
636 let inner_clone_line = "let inner = self.inner.clone();\n ";
637 let core_call_str = make_async_core_call(&method.name);
638 generators::gen_async_body(
639 &core_call_str,
640 cfg,
641 method.error_type.is_some(),
642 &async_result_wrap,
643 true,
644 inner_clone_line,
645 matches!(method.return_type, TypeRef::Unit),
646 Some(&return_type),
647 )
648 } else {
649 let use_let_bindings = generators::has_named_params(&method.params, opaque_types);
653 let (let_bindings, call_args_for_call) = if use_let_bindings {
654 let bindings = generators::gen_named_let_bindings_pub(&method.params, opaque_types, cfg.core_import);
655 let args = napi_apply_primitive_casts_to_call_args(
656 &generators::gen_call_args_with_let_bindings(&method.params, opaque_types),
657 &method.params,
658 );
659 (bindings, args)
660 } else {
661 (String::new(), napi_gen_call_args(&method.params, opaque_types))
662 };
663 let core_call = if is_owned_receiver {
664 format!("(*self.inner).clone().{}({})", method.name, call_args_for_call)
665 } else {
666 format!("self.inner.{}({})", method.name, call_args_for_call)
667 };
668 if method.error_type.is_some() {
669 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
670 if matches!(method.return_type, TypeRef::Unit) {
671 format!("{let_bindings}{core_call}{err_conv}?;\n Ok(())")
672 } else {
673 let wrap = napi_wrap_return(
674 "result",
675 &method.return_type,
676 type_name,
677 opaque_types,
678 true,
679 method.returns_ref,
680 prefix,
681 );
682 format!("{let_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
683 }
684 } else {
685 format!(
686 "{let_bindings}{}",
687 napi_wrap_return(
688 &core_call,
689 &method.return_type,
690 type_name,
691 opaque_types,
692 true,
693 method.returns_ref,
694 prefix,
695 )
696 )
697 }
698 };
699
700 let mut attrs = String::new();
701 if method.params.len() + 1 > 7 {
703 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
704 }
705 if method.error_type.is_some() {
707 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
708 }
709 if generators::is_trait_method_name(&method.name) {
711 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
712 }
713 format!(
714 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n \
715 {body}\n}}",
716 method.name
717 )
718}
719
720fn gen_static_method(
722 method: &MethodDef,
723 mapper: &NapiMapper,
724 typ: &TypeDef,
725 cfg: &RustBindingConfig,
726 opaque_types: &AHashSet<String>,
727 prefix: &str,
728) -> String {
729 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
730 let return_type = mapper.map_type(&method.return_type);
731 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
732
733 let js_name = to_node_name(&method.name);
734 let js_name_attr = if js_name != method.name {
735 format!("(js_name = \"{}\")", js_name)
736 } else {
737 String::new()
738 };
739
740 let type_name = &typ.name;
741 let core_type_path = typ.rust_path.replace('-', "_");
742 let call_args = napi_gen_call_args(&method.params, opaque_types);
743 let can_delegate_static = can_auto_delegate(method, opaque_types);
744
745 let async_kw = if method.is_async { "async " } else { "" };
746
747 let body = if !can_delegate_static {
748 generators::gen_unimplemented_body(
749 &method.return_type,
750 &format!("{type_name}::{}", method.name),
751 method.error_type.is_some(),
752 cfg,
753 &method.params,
754 opaque_types,
755 )
756 } else if method.is_async {
757 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
758 let return_wrap = napi_wrap_return(
759 "result",
760 &method.return_type,
761 type_name,
762 opaque_types,
763 typ.is_opaque,
764 method.returns_ref,
765 prefix,
766 );
767 generators::gen_async_body(
768 &core_call,
769 cfg,
770 method.error_type.is_some(),
771 &return_wrap,
772 false,
773 "",
774 matches!(method.return_type, TypeRef::Unit),
775 Some(&return_type),
776 )
777 } else {
778 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
779 if method.error_type.is_some() {
780 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
781 let wrapped = napi_wrap_return(
782 "val",
783 &method.return_type,
784 type_name,
785 opaque_types,
786 typ.is_opaque,
787 method.returns_ref,
788 prefix,
789 );
790 if wrapped == "val" {
791 format!("{core_call}{err_conv}")
792 } else {
793 format!("{core_call}.map(|val| {wrapped}){err_conv}")
794 }
795 } else {
796 napi_wrap_return(
797 &core_call,
798 &method.return_type,
799 type_name,
800 opaque_types,
801 typ.is_opaque,
802 method.returns_ref,
803 prefix,
804 )
805 }
806 };
807
808 let mut attrs = String::new();
809 if method.params.len() > 7 {
811 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
812 }
813 if method.error_type.is_some() {
815 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
816 }
817 if generators::is_trait_method_name(&method.name) {
819 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
820 }
821 format!(
822 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
823 {body}\n}}",
824 method.name
825 )
826}
827
828fn gen_enum(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
834 let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
835
836 if is_tagged_data_enum {
837 return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
838 }
839
840 let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
842 "snake_case" => Some("snake_case"),
843 "camelCase" => Some("camelCase"),
844 "kebab-case" => Some("kebab-case"),
845 "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
846 "lowercase" => Some("lowercase"),
847 "UPPERCASE" => Some("UPPERCASE"),
848 "PascalCase" => Some("PascalCase"),
849 _ => None,
850 });
851
852 let string_enum_attr = match napi_case {
853 Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
854 None => "#[napi(string_enum)]".to_string(),
855 };
856
857 let derives = if has_serde {
858 "#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
859 } else {
860 "#[derive(Clone)]".to_string()
861 };
862 let mut lines = vec![
863 string_enum_attr,
864 derives,
865 format!("pub enum {prefix}{} {{", enum_def.name),
866 ];
867
868 for variant in &enum_def.variants {
869 lines.push(format!(" {},", variant.name));
870 }
871
872 lines.push("}".to_string());
873
874 if let Some(first) = enum_def.variants.first() {
876 lines.push(String::new());
877 lines.push("#[allow(clippy::derivable_impls)]".to_string());
878 lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
879 lines.push(format!(" fn default() -> Self {{ Self::{} }}", first.name));
880 lines.push("}".to_string());
881 }
882
883 lines.join("\n")
884}
885
886fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
899 use alef_codegen::type_mapper::TypeMapper;
900 let mapper = NapiMapper::new(prefix.to_string());
901
902 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
903
904 let derive = if has_serde {
905 "#[derive(Clone, serde::Serialize, serde::Deserialize)]"
906 } else {
907 "#[derive(Clone)]"
908 };
909 let mut lines = vec![
910 derive.to_string(),
911 "#[napi(object)]".to_string(),
912 format!("pub struct {prefix}{} {{", enum_def.name),
913 format!(" #[napi(js_name = \"{tag_field}\")]"),
914 format!(" pub {tag_field}_tag: String,"),
915 ];
916
917 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
921
922 let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
924 for variant in &enum_def.variants {
925 for field in &variant.fields {
926 if seen_fields.insert(field.name.clone()) {
927 let field_type = if (field.sanitized || mixed_named_fields.contains(&field.name))
930 && matches!(&field.ty, TypeRef::Named(_))
931 {
932 "String".to_string()
933 } else {
934 mapper.map_type(&field.ty).to_string()
935 };
936 let js_name = alef_codegen::naming::to_node_name(&field.name);
937 if js_name != field.name {
938 lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
939 }
940 lines.push(format!(" pub {}: Option<{field_type}>,", field.name));
941 }
942 }
943 }
944
945 lines.push("}".to_string());
946
947 lines.push(String::new());
949 lines.push("#[allow(clippy::derivable_impls)]".to_string());
950 lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
951 lines.push(format!(
952 " fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
953 seen_fields
954 .iter()
955 .map(|f| format!("{f}: None"))
956 .collect::<Vec<_>>()
957 .join(", ")
958 ));
959 lines.push("}".to_string());
960
961 lines.join("\n")
962}
963
964fn gen_function(
966 func: &FunctionDef,
967 mapper: &NapiMapper,
968 cfg: &RustBindingConfig,
969 opaque_types: &AHashSet<String>,
970 prefix: &str,
971) -> String {
972 let params = function_params(&func.params, &|ty| {
973 if let TypeRef::Named(n) = ty {
976 if opaque_types.contains(n.as_str()) {
977 return format!("&{prefix}{n}");
978 }
979 }
980 mapper.map_type(ty)
981 });
982 let return_type = mapper.map_type(&func.return_type);
983 let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
984
985 let js_name = to_node_name(&func.name);
986 let js_name_attr = if js_name != func.name {
987 format!("(js_name = \"{}\")", js_name)
988 } else {
989 String::new()
990 };
991
992 let core_import = cfg.core_import;
993 let core_fn_path = {
994 let path = func.rust_path.replace('-', "_");
995 if path.starts_with(core_import) {
996 path
997 } else {
998 format!("{core_import}::{}", func.name)
999 }
1000 };
1001
1002 let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
1004 || func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty));
1005 let call_args = if use_let_bindings {
1006 let base_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
1007 napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
1008 } else {
1009 napi_gen_call_args(&func.params, opaque_types)
1010 };
1011
1012 let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
1013
1014 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
1015
1016 let async_kw = if func.is_async { "async " } else { "" };
1017
1018 let body = if !can_delegate_fn {
1019 if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
1022 let serde_bindings =
1023 generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
1024 let vec_str_bindings: String = func.params.iter().filter(|p| {
1026 p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
1027 }).map(|p| {
1028 format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n ", p.name, p.name)
1029 }).collect();
1030 let core_call = format!("{core_fn_path}({call_args})");
1031 let await_kw = if func.is_async { ".await" } else { "" };
1032
1033 if matches!(func.return_type, TypeRef::Unit) {
1034 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n Ok(())")
1035 } else {
1036 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1037 if wrapped == "val" {
1038 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
1039 } else {
1040 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
1041 }
1042 }
1043 } else {
1044 generators::gen_unimplemented_body(
1045 &func.return_type,
1046 &func.name,
1047 func.error_type.is_some(),
1048 cfg,
1049 &func.params,
1050 opaque_types,
1051 )
1052 }
1053 } else if func.is_async {
1054 let mut let_bindings = if use_let_bindings {
1056 generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1057 } else {
1058 String::new()
1059 };
1060 let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1062 let core_call = format!("{core_fn_path}({call_args})");
1063 let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref, prefix);
1064 let return_type = mapper.map_type(&func.return_type);
1065 generators::gen_async_body(
1066 &core_call,
1067 cfg,
1068 func.error_type.is_some(),
1069 &return_wrap,
1070 false,
1071 &let_bindings,
1072 matches!(func.return_type, TypeRef::Unit),
1073 Some(&return_type),
1074 )
1075 } else {
1076 let core_call = format!("{core_fn_path}({call_args})");
1077 let mut let_bindings = if use_let_bindings {
1079 generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1080 } else {
1081 String::new()
1082 };
1083 let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1085
1086 if func.error_type.is_some() {
1087 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1088 if wrapped == "val" {
1089 format!("{let_bindings}{core_call}{err_conv}")
1090 } else {
1091 format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
1092 }
1093 } else {
1094 format!(
1095 "{let_bindings}{}",
1096 napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref, prefix)
1097 )
1098 }
1099 };
1100
1101 let mut attrs = String::new();
1102 if func.params.len() > 7 {
1104 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
1105 }
1106 if func.error_type.is_some() {
1108 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
1109 }
1110 format!(
1111 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
1112 {body}\n}}",
1113 func.name
1114 )
1115}
1116
1117fn napi_apply_primitive_casts_to_call_args(generic_args: &str, params: &[ParamDef]) -> String {
1120 let args_list: Vec<&str> = generic_args.split(',').map(|s| s.trim()).collect();
1122 args_list
1123 .iter()
1124 .zip(params.iter())
1125 .map(|(arg, p)| {
1126 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1128 return format!("&{}_f32", p.name);
1129 }
1130 match &p.ty {
1131 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1132 let core_ty = core_prim_str(prim);
1133 if p.optional {
1134 if arg.contains(".map(") || arg.contains(".as_") {
1136 arg.to_string()
1138 } else {
1139 format!("{}.map(|v| v as {})", arg, core_ty)
1140 }
1141 } else {
1142 format!("{} as {}", arg, core_ty)
1144 }
1145 }
1146 _ => arg.to_string(),
1147 }
1148 })
1149 .collect::<Vec<_>>()
1150 .join(", ")
1151}
1152
1153fn gen_vec_f32_conversion_bindings(params: &[ParamDef]) -> String {
1156 let mut bindings = String::new();
1157 for p in params {
1158 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1159 let conv_name = format!("{}_f32", p.name);
1160 bindings.push_str(&format!(
1161 " let {conv_name}: Vec<f32> = {}.iter().map(|&x| x as f32).collect();\n",
1162 p.name
1163 ));
1164 }
1165 }
1166 bindings
1167}
1168
1169fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
1172 params
1173 .iter()
1174 .map(|p| {
1175 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1177 return format!("&{}_f32", p.name);
1178 }
1179 match &p.ty {
1180 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1181 let core_ty = core_prim_str(prim);
1182 if p.optional {
1183 format!("{}.map(|v| v as {})", p.name, core_ty)
1184 } else {
1185 format!("{} as {}", p.name, core_ty)
1186 }
1187 }
1188 TypeRef::Duration => {
1189 if p.optional {
1190 format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
1191 } else {
1192 format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
1193 }
1194 }
1195 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1196 if p.optional {
1197 format!("{}.as_ref().map(|v| &v.inner)", p.name)
1198 } else {
1199 format!("&{}.inner", p.name)
1200 }
1201 }
1202 TypeRef::Named(_) => {
1203 if p.optional {
1204 if p.is_ref {
1205 format!("{}.as_ref()", p.name)
1206 } else {
1207 format!("{}.map(Into::into)", p.name)
1208 }
1209 } else {
1210 format!("{}.into()", p.name)
1211 }
1212 }
1213 TypeRef::String | TypeRef::Char => {
1214 if p.optional {
1215 if p.is_ref {
1216 format!("{}.as_deref()", p.name)
1217 } else {
1218 p.name.clone()
1219 }
1220 } else if p.is_ref {
1221 format!("&{}", p.name)
1222 } else {
1223 p.name.clone()
1224 }
1225 }
1226 TypeRef::Path => {
1227 if p.optional {
1228 if p.is_ref {
1229 format!("{}.as_deref().map(std::path::Path::new)", p.name)
1230 } else {
1231 format!("{}.map(std::path::PathBuf::from)", p.name)
1232 }
1233 } else if p.is_ref {
1234 format!("std::path::Path::new(&{})", p.name)
1235 } else {
1236 format!("std::path::PathBuf::from({})", p.name)
1237 }
1238 }
1239 TypeRef::Bytes => {
1240 if p.optional {
1241 if p.is_ref {
1242 format!("{}.as_deref()", p.name)
1243 } else {
1244 p.name.clone()
1245 }
1246 } else if p.is_ref {
1247 format!("&{}", p.name)
1248 } else {
1249 p.name.clone()
1250 }
1251 }
1252 TypeRef::Vec(inner) => {
1253 if p.optional {
1254 if p.is_ref {
1255 format!("{}.as_deref()", p.name)
1256 } else {
1257 p.name.clone()
1258 }
1259 } else if p.is_ref && matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) {
1260 format!("&{}_refs", p.name)
1261 } else if p.is_ref {
1262 format!("&{}", p.name)
1263 } else {
1264 p.name.clone()
1265 }
1266 }
1267 TypeRef::Map(_, _) => {
1268 if p.optional {
1269 if p.is_ref {
1270 format!("{}.as_ref()", p.name)
1271 } else {
1272 p.name.clone()
1273 }
1274 } else if p.is_ref {
1275 format!("&{}", p.name)
1276 } else {
1277 p.name.clone()
1278 }
1279 }
1280 _ => p.name.clone(),
1281 }
1282 })
1283 .collect::<Vec<_>>()
1284 .join(", ")
1285}
1286
1287fn napi_wrap_return(
1290 expr: &str,
1291 return_type: &TypeRef,
1292 type_name: &str,
1293 opaque_types: &AHashSet<String>,
1294 self_is_opaque: bool,
1295 returns_ref: bool,
1296 prefix: &str,
1297) -> String {
1298 match return_type {
1299 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1300 format!("{expr} as i64")
1301 }
1302 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1303 TypeRef::Named(n) if n == type_name && self_is_opaque => {
1305 if returns_ref {
1306 format!("Self {{ inner: Arc::new({expr}.clone()) }}")
1307 } else {
1308 format!("Self {{ inner: Arc::new({expr}) }}")
1309 }
1310 }
1311 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1312 if returns_ref {
1313 format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1314 } else {
1315 format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1316 }
1317 }
1318 TypeRef::Named(_) => {
1319 if returns_ref {
1320 format!("{expr}.clone().into()")
1321 } else {
1322 format!("{expr}.into()")
1323 }
1324 }
1325 _ => generators::wrap_return(
1326 expr,
1327 return_type,
1328 type_name,
1329 opaque_types,
1330 self_is_opaque,
1331 returns_ref,
1332 false,
1333 ),
1334 }
1335}
1336
1337fn napi_wrap_return_fn(
1339 expr: &str,
1340 return_type: &TypeRef,
1341 opaque_types: &AHashSet<String>,
1342 returns_ref: bool,
1343 prefix: &str,
1344) -> String {
1345 match return_type {
1346 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1347 format!("{expr} as i64")
1348 }
1349 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1350 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1351 if returns_ref {
1352 format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1353 } else {
1354 format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1355 }
1356 }
1357 TypeRef::Named(_) => {
1358 if returns_ref {
1359 format!("{expr}.clone().into()")
1360 } else {
1361 format!("{expr}.into()")
1362 }
1363 }
1364 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1365 if returns_ref {
1366 format!("{expr}.into()")
1367 } else {
1368 expr.to_string()
1369 }
1370 }
1371 TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1372 TypeRef::Json => format!("{expr}.to_string()"),
1373 TypeRef::Optional(inner) => match inner.as_ref() {
1374 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1375 if returns_ref {
1376 format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
1377 } else {
1378 format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
1379 }
1380 }
1381 TypeRef::Named(_) => {
1382 if returns_ref {
1383 format!("{expr}.map(|v| v.clone().into())")
1384 } else {
1385 format!("{expr}.map(Into::into)")
1386 }
1387 }
1388 TypeRef::Vec(inner) => match inner.as_ref() {
1389 TypeRef::Named(_) => {
1390 if returns_ref {
1391 format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
1392 } else {
1393 format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
1394 }
1395 }
1396 _ => expr.to_string(),
1397 },
1398 TypeRef::Path => {
1399 format!("{expr}.map(Into::into)")
1400 }
1401 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1402 if returns_ref {
1403 format!("{expr}.map(Into::into)")
1404 } else {
1405 expr.to_string()
1406 }
1407 }
1408 _ => expr.to_string(),
1409 },
1410 TypeRef::Vec(inner) => match inner.as_ref() {
1411 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1412 let target_ty = match p {
1414 alef_core::ir::PrimitiveType::F32 => "f64",
1415 _ => "i64", };
1417 format!("{expr}.into_iter().map(|v| v as {target_ty}).collect()")
1418 }
1419 TypeRef::Vec(inner2) => {
1421 if let TypeRef::Primitive(p) = inner2.as_ref() {
1422 if needs_napi_cast(p) {
1423 let target_ty = match p {
1424 alef_core::ir::PrimitiveType::F32 => "f64",
1425 _ => "i64",
1426 };
1427 return format!(
1428 "{expr}.into_iter().map(|row| row.into_iter().map(|x| x as {target_ty}).collect::<Vec<_>>()).collect::<Vec<_>>()"
1429 );
1430 }
1431 }
1432 expr.to_string()
1433 }
1434 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1435 if returns_ref {
1436 format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1437 } else {
1438 format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
1439 }
1440 }
1441 TypeRef::Named(_) => {
1442 if returns_ref {
1443 format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1444 } else {
1445 format!("{expr}.into_iter().map(Into::into).collect()")
1446 }
1447 }
1448 TypeRef::Path => {
1449 format!("{expr}.into_iter().map(Into::into).collect()")
1450 }
1451 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1452 if returns_ref {
1453 format!("{expr}.into_iter().map(Into::into).collect()")
1454 } else {
1455 expr.to_string()
1456 }
1457 }
1458 _ => expr.to_string(),
1459 },
1460 _ => expr.to_string(),
1461 }
1462}
1463
1464fn needs_vec_f32_conversion(ty: &TypeRef) -> bool {
1466 matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::F32)))
1467}
1468
1469fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1470 matches!(
1474 p,
1475 alef_core::ir::PrimitiveType::U64
1476 | alef_core::ir::PrimitiveType::Usize
1477 | alef_core::ir::PrimitiveType::Isize
1478 | alef_core::ir::PrimitiveType::F32
1479 )
1480}
1481
1482fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1483 match p {
1484 alef_core::ir::PrimitiveType::U64 => "u64",
1485 alef_core::ir::PrimitiveType::Usize => "usize",
1486 alef_core::ir::PrimitiveType::Isize => "isize",
1487 alef_core::ir::PrimitiveType::F32 => "f32",
1488 _ => unreachable!(),
1489 }
1490}
1491
1492fn gen_tokio_runtime() -> String {
1494 "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1495 tokio::runtime::Builder::new_multi_thread()
1496 .enable_all()
1497 .build()
1498 .expect(\"Failed to create Tokio runtime\")
1499});"
1500 .to_string()
1501}
1502
1503fn gen_dts(api: &ApiSurface, prefix: &str) -> String {
1513 let header = hash::header(CommentStyle::DoubleSlash);
1514 let mut lines: Vec<String> = header.lines().map(|l| l.to_string()).collect();
1515 lines.push("/* eslint-disable */".to_string());
1516
1517 let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1522 opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1523
1524 let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1526 plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1527
1528 let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1530 sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1531
1532 let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1534 sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1535
1536 enum Decl<'a> {
1539 Class(&'a TypeDef),
1540 Interface(&'a TypeDef),
1541 Enum(&'a EnumDef),
1542 Function(&'a FunctionDef),
1543 }
1544
1545 let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1546 for t in &opaque_types {
1547 all_decls.push((format!("{prefix}{}", t.name), Decl::Class(t)));
1548 }
1549 for t in &plain_types {
1550 all_decls.push((format!("{prefix}{}", t.name), Decl::Interface(t)));
1551 }
1552 for e in &sorted_enums {
1553 all_decls.push((format!("{prefix}{}", e.name), Decl::Enum(e)));
1554 }
1555 for f in &sorted_fns {
1556 all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1557 }
1558 all_decls.sort_by_key(|a| a.0.to_lowercase());
1559
1560 for (_, decl) in &all_decls {
1561 lines.push(String::new());
1562 match decl {
1563 Decl::Class(typ) => {
1564 lines.extend(format_jsdoc(&typ.doc, ""));
1565 lines.push(format!("export declare class {prefix}{} {{", typ.name));
1566 for method in &typ.methods {
1567 let js_name = to_node_name(&method.name);
1568 let params = dts_params(&method.params, prefix);
1569 let ret = dts_return_type(
1570 &method.return_type,
1571 method.error_type.is_some(),
1572 method.is_async,
1573 prefix,
1574 );
1575 lines.extend(format_jsdoc(&method.doc, " "));
1576 if method.is_static {
1577 lines.push(format!(" static {js_name}({params}): {ret}"));
1578 } else {
1579 lines.push(format!(" {js_name}({params}): {ret}"));
1580 }
1581 }
1582 lines.push("}".to_string());
1583 }
1584 Decl::Interface(typ) => {
1585 lines.extend(format_jsdoc(&typ.doc, ""));
1586 lines.push(format!("export interface {prefix}{} {{", typ.name));
1587 for field in &typ.fields {
1588 let js_name = to_node_name(&field.name);
1589 let ts_ty = dts_type(&field.ty, prefix);
1590 lines.extend(format_jsdoc(&field.doc, " "));
1591 if matches!(field.ty, TypeRef::Optional(_)) {
1594 lines.push(format!(" {js_name}?: {ts_ty}"));
1595 } else {
1596 lines.push(format!(" {js_name}: {ts_ty}"));
1597 }
1598 }
1599 lines.push("}".to_string());
1600 }
1601 Decl::Enum(e) => {
1602 let is_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
1603 lines.extend(format_jsdoc(&e.doc, ""));
1604 if is_data_enum {
1605 let tag_field = e.serde_tag.as_deref().unwrap_or("type");
1608 let mut member_lines: Vec<String> = Vec::new();
1609 for variant in &e.variants {
1610 let tag_value = variant
1611 .serde_rename
1612 .as_deref()
1613 .map(|s| s.to_string())
1614 .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1615 let mut obj_fields: Vec<String> = vec![format!("{tag_field}: '{tag_value}'")];
1616 for field in &variant.fields {
1617 let js_name = to_node_name(&field.name);
1618 let ts_ty = dts_type(&field.ty, prefix);
1619 if matches!(field.ty, TypeRef::Optional(_)) {
1620 obj_fields.push(format!("{js_name}?: {ts_ty}"));
1621 } else {
1622 obj_fields.push(format!("{js_name}: {ts_ty}"));
1623 }
1624 }
1625 member_lines.push(format!(" | {{ {} }}", obj_fields.join("; ")));
1626 }
1627 lines.push(format!("export type {prefix}{} =", e.name));
1628 lines.extend(member_lines);
1629 } else {
1630 lines.push(format!("export declare enum {prefix}{} {{", e.name));
1631 for variant in &e.variants {
1632 let value = variant
1635 .serde_rename
1636 .as_deref()
1637 .map(|s| s.to_string())
1638 .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1639 lines.extend(format_jsdoc(&variant.doc, " "));
1640 lines.push(format!(" {} = \"{}\",", variant.name, value));
1641 }
1642 lines.push("}".to_string());
1643 }
1644 }
1645 Decl::Function(func) => {
1646 let js_name = to_node_name(&func.name);
1647 let params = dts_params(&func.params, prefix);
1648 let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async, prefix);
1649 lines.extend(format_jsdoc(&func.doc, ""));
1650 lines.push(format!("export declare function {js_name}({params}): {ret};"));
1651 }
1652 }
1653 }
1654
1655 lines.push(String::new());
1656 lines.join("\n")
1657}
1658
1659fn format_jsdoc(doc: &str, indent: &str) -> Vec<String> {
1665 let doc = doc.trim();
1666 if doc.is_empty() {
1667 return vec![];
1668 }
1669 let lines: Vec<&str> = doc.lines().collect();
1670 if lines.len() == 1 {
1671 vec![format!("{indent}/** {} */", lines[0].trim())]
1672 } else {
1673 let mut out = Vec::with_capacity(lines.len() + 2);
1674 out.push(format!("{indent}/**"));
1675 for line in &lines {
1676 let trimmed = line.trim();
1677 if trimmed.is_empty() {
1678 out.push(format!("{indent} *"));
1679 } else {
1680 out.push(format!("{indent} * {trimmed}"));
1681 }
1682 }
1683 out.push(format!("{indent} */"));
1684 out
1685 }
1686}
1687
1688fn dts_type(ty: &TypeRef, prefix: &str) -> String {
1690 match ty {
1691 TypeRef::Primitive(p) => match p {
1692 alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1693 alef_core::ir::PrimitiveType::U8
1694 | alef_core::ir::PrimitiveType::U16
1695 | alef_core::ir::PrimitiveType::U32
1696 | alef_core::ir::PrimitiveType::I8
1697 | alef_core::ir::PrimitiveType::I16
1698 | alef_core::ir::PrimitiveType::I32
1699 | alef_core::ir::PrimitiveType::F32
1700 | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1701 alef_core::ir::PrimitiveType::U64
1703 | alef_core::ir::PrimitiveType::I64
1704 | alef_core::ir::PrimitiveType::Usize
1705 | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1706 },
1707 TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1708 TypeRef::Bytes => "Uint8Array".to_string(),
1709 TypeRef::Json => "unknown".to_string(),
1710 TypeRef::Duration => "number".to_string(),
1711 TypeRef::Unit => "void".to_string(),
1712 TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner, prefix)),
1713 TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner, prefix)),
1714 TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k, prefix), dts_type(v, prefix)),
1715 TypeRef::Named(name) => format!("{prefix}{name}"),
1716 }
1717}
1718
1719fn dts_params(params: &[ParamDef], prefix: &str) -> String {
1721 let mut required: Vec<&ParamDef> = Vec::new();
1726 let mut optional: Vec<&ParamDef> = Vec::new();
1727 for p in params {
1728 if p.optional {
1729 optional.push(p);
1730 } else {
1731 required.push(p);
1732 }
1733 }
1734 let ordered: Vec<&ParamDef> = if params
1736 .iter()
1737 .zip(required.iter().chain(optional.iter()))
1738 .all(|(a, b)| std::ptr::eq(a as *const ParamDef, *b as *const ParamDef))
1739 {
1740 params.iter().collect()
1741 } else {
1742 required.into_iter().chain(optional).collect()
1743 };
1744 ordered
1745 .iter()
1746 .map(|p| {
1747 let js_name = to_node_name(&p.name);
1748 let ts_ty = dts_type(&p.ty, prefix);
1749 if p.optional {
1750 format!("{js_name}?: {ts_ty} | undefined | null")
1751 } else {
1752 format!("{js_name}: {ts_ty}")
1753 }
1754 })
1755 .collect::<Vec<_>>()
1756 .join(", ")
1757}
1758
1759fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool, prefix: &str) -> String {
1764 let base = match ret {
1765 TypeRef::Unit => "void".to_string(),
1766 other => dts_type(other, prefix),
1767 };
1768 if is_async { format!("Promise<{base}>") } else { base }
1769}
1770
1771fn apply_rename_all(variant_name: &str, rename_all: Option<&str>) -> String {
1776 match rename_all {
1777 Some("snake_case") => {
1778 let mut out = String::with_capacity(variant_name.len() + 4);
1780 for (i, c) in variant_name.chars().enumerate() {
1781 if c.is_uppercase() && i > 0 {
1782 out.push('_');
1783 }
1784 out.extend(c.to_lowercase());
1785 }
1786 out
1787 }
1788 Some("camelCase") => {
1789 let mut chars = variant_name.chars();
1791 match chars.next() {
1792 None => String::new(),
1793 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1794 }
1795 }
1796 Some("kebab-case") => {
1797 let mut out = String::with_capacity(variant_name.len() + 4);
1798 for (i, c) in variant_name.chars().enumerate() {
1799 if c.is_uppercase() && i > 0 {
1800 out.push('-');
1801 }
1802 out.extend(c.to_lowercase());
1803 }
1804 out
1805 }
1806 Some("SCREAMING_SNAKE_CASE") => {
1807 let mut out = String::with_capacity(variant_name.len() + 4);
1808 for (i, c) in variant_name.chars().enumerate() {
1809 if c.is_uppercase() && i > 0 {
1810 out.push('_');
1811 }
1812 out.extend(c.to_uppercase());
1813 }
1814 out
1815 }
1816 Some("lowercase") => variant_name.to_lowercase(),
1817 Some("UPPERCASE") => variant_name.to_uppercase(),
1818 _ => variant_name.to_string(),
1820 }
1821}
1822
1823fn gen_tagged_enum_binding_to_core(
1825 enum_def: &EnumDef,
1826 core_import: &str,
1827 prefix: &str,
1828 struct_names: &ahash::AHashSet<String>,
1829) -> String {
1830 use alef_core::ir::TypeRef;
1831 use std::fmt::Write;
1832 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1833 let binding_name = format!("{prefix}{}", enum_def.name);
1834 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1835
1836 let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1841 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1844
1845 let mut out = String::with_capacity(512);
1846 writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1847 writeln!(out, " fn from(val: {binding_name}) -> Self {{").ok();
1848 writeln!(out, " match val.{tag_field}_tag.as_str() {{").ok();
1849
1850 for variant in &enum_def.variants {
1851 let default_tag = variant.name.to_lowercase();
1852 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1853 if variant.fields.is_empty() {
1854 writeln!(out, " \"{tag_value}\" => Self::{},", variant.name).ok();
1855 } else {
1856 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1857 let field_exprs: Vec<String> = variant
1858 .fields
1859 .iter()
1860 .map(|f| {
1861 let has_binding = fields_with_binding_struct.contains(f.name.as_str());
1862 let is_mixed = mixed_named_fields.contains(&f.name);
1863 if f.optional {
1864 match &f.ty {
1865 TypeRef::Path => {
1866 format!("val.{}.map(std::path::PathBuf::from)", f.name)
1867 }
1868 TypeRef::Named(n) if is_mixed => {
1869 let core_type = format!("{core_import}::{n}");
1871 format!(
1872 "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok())",
1873 f.name
1874 )
1875 }
1876 TypeRef::Named(_) if has_binding => {
1877 format!("val.{}.map(|v| v.into())", f.name)
1878 }
1879 TypeRef::Named(_) => {
1882 format!("val.{}.map(|v| v.into())", f.name)
1883 }
1884 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1885 let core_ty = core_prim_str(p);
1886 format!("val.{}.map(|v| v as {core_ty})", f.name)
1887 }
1888 _ => {
1889 format!("val.{}", f.name)
1890 }
1891 }
1892 } else if f.sanitized {
1893 let expr = "Default::default()".to_string();
1894 if f.is_boxed { format!("Box::new({expr})") } else { expr }
1895 } else {
1896 let expr = match &f.ty {
1897 TypeRef::Named(n) if is_mixed => {
1898 let core_type = format!("{core_import}::{n}");
1900 format!(
1901 "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok()).unwrap_or_default()",
1902 f.name
1903 )
1904 }
1905 TypeRef::Named(_) if has_binding => {
1906 format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1907 }
1908 TypeRef::Named(_) => {
1911 format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1912 }
1913 TypeRef::Path => {
1914 format!("val.{}.map(std::path::PathBuf::from).unwrap_or_default()", f.name)
1915 }
1916 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1917 let core_ty = core_prim_str(p);
1918 format!("val.{}.map(|v| v as {core_ty}).unwrap_or_default()", f.name)
1919 }
1920 _ => {
1921 format!("val.{}.unwrap_or_default()", f.name)
1922 }
1923 };
1924 if f.is_boxed { format!("Box::new({expr})") } else { expr }
1925 }
1926 })
1927 .collect();
1928 if is_tuple {
1929 writeln!(
1930 out,
1931 " \"{tag_value}\" => Self::{}({}),",
1932 variant.name,
1933 field_exprs.join(", ")
1934 )
1935 .ok();
1936 } else {
1937 let field_inits: Vec<String> = variant
1938 .fields
1939 .iter()
1940 .zip(field_exprs.iter())
1941 .map(|(f, expr)| format!("{}: {expr}", f.name))
1942 .collect();
1943 writeln!(
1944 out,
1945 " \"{tag_value}\" => Self::{} {{ {} }},",
1946 variant.name,
1947 field_inits.join(", ")
1948 )
1949 .ok();
1950 }
1951 }
1952 }
1953
1954 if let Some(first) = enum_def.variants.first() {
1956 if first.fields.is_empty() {
1957 writeln!(out, " _ => Self::{},", first.name).ok();
1958 } else {
1959 let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1960 if is_tuple {
1961 let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1962 writeln!(out, " _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1963 } else {
1964 let defaults: Vec<String> = first
1965 .fields
1966 .iter()
1967 .map(|f| format!("{}: Default::default()", f.name))
1968 .collect();
1969 writeln!(
1970 out,
1971 " _ => Self::{} {{ {} }},",
1972 first.name,
1973 defaults.join(", ")
1974 )
1975 .ok();
1976 }
1977 }
1978 }
1979
1980 writeln!(out, " }}").ok();
1981 writeln!(out, " }}").ok();
1982 write!(out, "}}").ok();
1983 out
1984}
1985
1986fn gen_tagged_enum_core_to_binding(
1988 enum_def: &EnumDef,
1989 core_import: &str,
1990 prefix: &str,
1991 struct_names: &ahash::AHashSet<String>,
1992) -> String {
1993 use std::fmt::Write;
1994 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1995 let binding_name = format!("{prefix}{}", enum_def.name);
1996 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1997 let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1998 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
2001
2002 let all_fields: Vec<String> = {
2004 let mut fields = std::collections::BTreeSet::new();
2005 for v in &enum_def.variants {
2006 for f in &v.fields {
2007 fields.insert(f.name.clone());
2008 }
2009 }
2010 fields.into_iter().collect()
2011 };
2012
2013 let mut out = String::with_capacity(512);
2014 writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
2015 writeln!(out, " fn from(val: {core_path}) -> Self {{").ok();
2016 writeln!(out, " match val {{").ok();
2017
2018 for variant in &enum_def.variants {
2019 let default_tag = variant.name.to_lowercase();
2020 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
2021 let _variant_field_names: std::collections::BTreeSet<String> =
2022 variant.fields.iter().map(|f| f.name.clone()).collect();
2023
2024 if variant.fields.is_empty() {
2025 writeln!(
2026 out,
2027 " {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2028 variant.name,
2029 all_fields
2030 .iter()
2031 .map(|f| format!("{f}: None"))
2032 .collect::<Vec<_>>()
2033 .join(", ")
2034 )
2035 .ok();
2036 } else {
2037 use alef_core::ir::TypeRef;
2038 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
2039 let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
2040 variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
2041 let destructured: Vec<String> = variant
2042 .fields
2043 .iter()
2044 .map(|f| {
2045 if f.sanitized {
2046 if is_tuple {
2047 format!("_{}", f.name)
2048 } else {
2049 format!("{}: _{}", f.name, f.name)
2050 }
2051 } else {
2052 f.name.clone()
2053 }
2054 })
2055 .collect();
2056 let field_inits: Vec<String> = all_fields
2057 .iter()
2058 .map(|f| {
2059 if let Some(field) = variant_field_map.get(f.as_str()) {
2060 let has_binding = fields_with_binding_struct.contains(f.as_str());
2061 let is_mixed = mixed_named_fields.contains(f.as_str());
2062 if field.optional {
2063 match &field.ty {
2064 TypeRef::Path => format!("{f}: {f}.map(|p| p.to_string_lossy().to_string())"),
2065 TypeRef::Named(_) if is_mixed => {
2066 format!("{f}: {f}.and_then(|v| serde_json::to_string(&v).ok())")
2068 }
2069 TypeRef::Named(_) if has_binding => {
2070 format!("{f}: {f}.map(|v| v.into())")
2071 }
2072 TypeRef::Named(_) => {
2075 format!("{f}: {f}.map(|v| v.into())")
2076 }
2077 _ => format!("{f}: {f}"),
2078 }
2079 } else if field.sanitized {
2080 format!("{f}: None")
2081 } else {
2082 match &field.ty {
2083 TypeRef::Named(_) if is_mixed => {
2084 format!("{f}: serde_json::to_string(&{f}).ok()")
2086 }
2087 TypeRef::Named(_) if has_binding => format!("{f}: Some({f}.into())"),
2088 TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
2091 TypeRef::Path => format!("{f}: Some({f}.to_string_lossy().to_string())"),
2092 TypeRef::Primitive(p) if needs_napi_cast(p) => {
2093 match p {
2094 alef_core::ir::PrimitiveType::F32 => format!("{f}: Some({f} as f64)"),
2095 alef_core::ir::PrimitiveType::U64
2096 | alef_core::ir::PrimitiveType::Usize
2097 | alef_core::ir::PrimitiveType::Isize => format!("{f}: Some({f} as i64)"),
2098 _ => format!("{f}: Some({f})"),
2100 }
2101 }
2102 _ => format!("{f}: Some({f})"),
2103 }
2104 }
2105 } else {
2106 format!("{f}: None")
2107 }
2108 })
2109 .collect();
2110 if is_tuple {
2111 writeln!(
2112 out,
2113 " {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2114 variant.name,
2115 destructured.join(", "),
2116 field_inits.join(", ")
2117 )
2118 .ok();
2119 } else {
2120 writeln!(
2121 out,
2122 " {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2123 variant.name,
2124 destructured.join(", "),
2125 field_inits.join(", ")
2126 )
2127 .ok();
2128 }
2129 }
2130 }
2131
2132 writeln!(out, " }}").ok();
2133 writeln!(out, " }}").ok();
2134 write!(out, "}}").ok();
2135 out
2136}
2137
2138fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
2142 use alef_core::ir::TypeRef;
2143 let mut field_types: std::collections::HashMap<&str, ahash::AHashSet<&str>> = std::collections::HashMap::new();
2144
2145 for variant in &enum_def.variants {
2146 for field in &variant.fields {
2147 if field.sanitized {
2148 continue;
2149 }
2150 if let TypeRef::Named(n) = &field.ty {
2151 field_types.entry(&field.name).or_default().insert(n.as_str());
2152 }
2153 }
2154 }
2155
2156 field_types
2157 .into_iter()
2158 .filter(|(_, types)| types.len() > 1)
2159 .map(|(name, _)| name.to_string())
2160 .collect()
2161}
2162
2163fn tagged_enum_binding_struct_fields<'a>(
2169 enum_def: &'a EnumDef,
2170 struct_names: &ahash::AHashSet<String>,
2171) -> ahash::AHashSet<&'a str> {
2172 use alef_core::ir::TypeRef;
2173 let mut field_types: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
2174 let mut sanitized_fields: ahash::AHashSet<&str> = ahash::AHashSet::new();
2175
2176 for variant in &enum_def.variants {
2177 for field in &variant.fields {
2178 if field.sanitized {
2179 sanitized_fields.insert(&field.name);
2180 }
2181 if let TypeRef::Named(n) = &field.ty {
2182 field_types.entry(&field.name).or_default().push(n);
2183 }
2184 }
2185 }
2186
2187 let mut result = ahash::AHashSet::new();
2188 for (field_name, types) in &field_types {
2189 if sanitized_fields.contains(field_name) {
2190 continue;
2191 }
2192 if types.iter().all(|t| *t == types[0]) && struct_names.contains(types[0]) {
2194 result.insert(*field_name);
2195 }
2196 }
2197 result
2198}
2199
2200#[cfg(test)]
2201mod tests {
2202 use super::*;
2203 use alef_core::ir::{ParamDef, TypeRef};
2204
2205 fn make_param(name: &str, optional: bool) -> ParamDef {
2206 ParamDef {
2207 name: name.to_string(),
2208 ty: TypeRef::String,
2209 optional,
2210 default: None,
2211 sanitized: false,
2212 typed_default: None,
2213 is_ref: false,
2214 is_mut: false,
2215 newtype_wrapper: None,
2216 original_type: None,
2217 }
2218 }
2219
2220 #[test]
2224 fn dts_params_reorders_required_after_optional() {
2225 let params = vec![
2226 make_param("ctx", false),
2227 make_param("lang", true),
2228 make_param("code", false),
2229 ];
2230 let result = dts_params(¶ms, "Js");
2231 let ctx_pos = result.find("ctx:").expect("ctx not found");
2233 let code_pos = result.find("code:").expect("code not found");
2234 let lang_pos = result.find("lang?:").expect("lang? not found");
2235 assert!(ctx_pos < lang_pos, "ctx should come before lang?: {result}");
2236 assert!(code_pos < lang_pos, "code should come before lang?: {result}");
2237 }
2238
2239 #[test]
2242 fn dts_params_preserves_already_valid_order() {
2243 let params = vec![
2244 make_param("ctx", false),
2245 make_param("code", false),
2246 make_param("lang", true),
2247 ];
2248 let result = dts_params(¶ms, "Js");
2249 assert_eq!(result, "ctx: string, code: string, lang?: string | undefined | null");
2250 }
2251
2252 #[test]
2254 fn dts_params_all_required_preserves_order() {
2255 let params = vec![make_param("a", false), make_param("b", false), make_param("c", false)];
2256 let result = dts_params(¶ms, "Js");
2257 assert_eq!(result, "a: string, b: string, c: string");
2258 }
2259}