1use std::collections::HashMap;
8
9use anyhow::Result;
10use camino::Utf8Path;
11use heck::ToUpperCamelCase;
12use serde::{Deserialize, Serialize};
13use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle};
14use weaveffi_core::codegen::Generator;
15use weaveffi_core::model::{BindingModel, FnBinding, ParamBinding, StructBinding};
16use weaveffi_core::utils::{
17 c_abi_struct_name, local_type_name, render_json_prelude, render_prelude, render_trailer,
18 wrapper_name, CommentStyle,
19};
20use weaveffi_ir::ir::{Api, TypeRef};
21
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24#[serde(default)]
25pub struct NodeConfig {
26 pub package_name: Option<String>,
28 pub strip_module_prefix: bool,
31 pub prefix: Option<String>,
35 #[serde(skip)]
37 pub input_basename: Option<String>,
38}
39
40impl NodeConfig {
41 pub fn package_name(&self) -> &str {
42 self.package_name.as_deref().unwrap_or("weaveffi")
43 }
44
45 pub fn prefix(&self) -> &str {
46 self.prefix.as_deref().unwrap_or("weaveffi")
47 }
48
49 pub fn input_basename(&self) -> &str {
50 self.input_basename.as_deref().unwrap_or("weaveffi.yml")
51 }
52}
53
54pub struct NodeGenerator;
55
56impl NodeGenerator {
57 fn generate_impl(
58 &self,
59 api: &Api,
60 out_dir: &Utf8Path,
61 package_name: &str,
62 prefix: &str,
63 strip_module_prefix: bool,
64 input_basename: &str,
65 ) -> Result<()> {
66 let dir = out_dir.join("node");
67 std::fs::create_dir_all(&dir)?;
68 let dbl = CommentStyle::DoubleSlash;
69 std::fs::write(
70 dir.join("index.js"),
71 format!(
72 "{}module.exports = require('./index.node')\n\n{}",
73 render_prelude(dbl, input_basename),
74 render_trailer(dbl, "index.js"),
75 ),
76 )?;
77 std::fs::write(
78 dir.join("types.d.ts"),
79 render_node_dts(api, prefix, strip_module_prefix, input_basename),
80 )?;
81 std::fs::write(
82 dir.join("package.json"),
83 render_package_json(package_name, input_basename),
84 )?;
85 std::fs::write(dir.join("binding.gyp"), render_binding_gyp(input_basename))?;
86 std::fs::write(
87 dir.join("weaveffi_addon.c"),
88 render_addon_c(api, prefix, strip_module_prefix, input_basename),
89 )?;
90 Ok(())
91 }
92}
93
94impl Generator for NodeGenerator {
95 type Config = NodeConfig;
96
97 fn name(&self) -> &'static str {
98 "node"
99 }
100
101 fn generate(&self, api: &Api, out_dir: &Utf8Path, config: &Self::Config) -> Result<()> {
102 self.generate_impl(
103 api,
104 out_dir,
105 config.package_name(),
106 config.prefix(),
107 config.strip_module_prefix,
108 config.input_basename(),
109 )
110 }
111
112 fn output_files(&self, _api: &Api, out_dir: &Utf8Path, _config: &Self::Config) -> Vec<String> {
113 let mut files = vec![
114 out_dir.join("node/binding.gyp").to_string(),
115 out_dir.join("node/index.js").to_string(),
116 out_dir.join("node/package.json").to_string(),
117 out_dir.join("node/types.d.ts").to_string(),
118 out_dir.join("node/weaveffi_addon.c").to_string(),
119 ];
120 files.sort();
121 files
122 }
123}
124
125fn render_package_json(name: &str, input_basename: &str) -> String {
126 let prelude = render_json_prelude(input_basename);
127 format!(
128 "{{\n{prelude} \"name\": \"{name}\",\n \"version\": \"0.1.0\",\n \"main\": \"index.js\",\n \"types\": \"types.d.ts\",\n \"gypfile\": true,\n \"scripts\": {{\n \"install\": \"node-gyp rebuild\"\n }}\n}}\n"
129 )
130}
131
132fn render_binding_gyp(input_basename: &str) -> String {
133 let prelude = render_prelude(CommentStyle::Hash, input_basename);
134 let trailer = render_trailer(CommentStyle::Hash, "binding.gyp");
135 format!(
136 "{prelude}{{\n \"targets\": [\n {{\n \"target_name\": \"weaveffi\",\n \"sources\": [\"weaveffi_addon.c\"],\n \"include_dirs\": [\"../c\"],\n \"libraries\": [\"-lweaveffi\"]\n }}\n ]\n}}\n\n{trailer}"
137 )
138}
139
140fn is_c_ptr_type(ty: &TypeRef) -> bool {
141 matches!(
142 ty,
143 TypeRef::StringUtf8
144 | TypeRef::Bytes
145 | TypeRef::Struct(_)
146 | TypeRef::List(_)
147 | TypeRef::Map(_, _)
148 | TypeRef::Iterator(_)
149 )
150}
151
152fn c_elem_type(ty: &TypeRef, module: &str, prefix: &str) -> String {
153 match ty {
154 TypeRef::I32 => "int32_t".into(),
155 TypeRef::U32 => "uint32_t".into(),
156 TypeRef::I64 => "int64_t".into(),
157 TypeRef::F64 => "double".into(),
158 TypeRef::Bool => "bool".into(),
159 TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
160 TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
161 TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
162 TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
163 TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
164 TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
165 c_elem_type(inner, module, prefix)
166 }
167 TypeRef::Map(_, _) => "void*".into(),
168 }
169}
170
171fn c_ret_type_str(ty: &TypeRef, module: &str, prefix: &str) -> String {
172 match ty {
173 TypeRef::I32 => "int32_t".into(),
174 TypeRef::U32 => "uint32_t".into(),
175 TypeRef::I64 => "int64_t".into(),
176 TypeRef::F64 => "double".into(),
177 TypeRef::Bool => "bool".into(),
178 TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
179 TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
180 TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
181 TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
182 TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
183 TypeRef::Optional(inner) => {
184 if is_c_ptr_type(inner) {
185 c_ret_type_str(inner, module, prefix)
186 } else {
187 format!("{}*", c_elem_type(inner, module, prefix))
188 }
189 }
190 TypeRef::List(inner) => format!("{}*", c_elem_type(inner, module, prefix)),
191 TypeRef::Map(_, _) => "void".into(),
192 TypeRef::Iterator(_) => "void*".into(),
193 }
194}
195
196fn napi_getter(ty: &TypeRef) -> &'static str {
197 match ty {
198 TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32",
199 TypeRef::U32 => "napi_get_value_uint32",
200 TypeRef::I64 | TypeRef::Handle | TypeRef::TypedHandle(_) | TypeRef::Struct(_) => {
201 "napi_get_value_int64"
202 }
203 TypeRef::F64 => "napi_get_value_double",
204 TypeRef::Bool => "napi_get_value_bool",
205 _ => "napi_get_value_int64",
206 }
207}
208
209fn render_addon_c(
210 api: &Api,
211 prefix: &str,
212 strip_module_prefix: bool,
213 input_basename: &str,
214) -> String {
215 let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
216 out.push_str(&format!(
217 "#include <node_api.h>\n#include \"{prefix}.h\"\n#include <stdlib.h>\n#include <string.h>\n\n"
218 ));
219
220 let model = BindingModel::build(api, prefix);
221 let has_async = model.functions().any(|(_, f)| f.is_async);
222 if has_async {
223 out.push_str("typedef struct {\n");
224 out.push_str(" napi_env env;\n");
225 out.push_str(" napi_deferred deferred;\n");
226 out.push_str("} weaveffi_napi_async_ctx;\n\n");
227 }
228
229 let mut all_exports: Vec<(String, String)> = Vec::new();
230 let structs = struct_registry(&model);
231
232 for m in &model.modules {
233 for f in &m.functions {
234 let c_name = &f.c_base;
235 let napi_name = format!("Napi_{c_name}");
236 let js_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
237 all_exports.push((js_name, napi_name.clone()));
238
239 if f.is_async {
240 render_async_callback(&mut out, f, c_name, &m.path, prefix, &structs);
241 }
242
243 out.push_str(&format!(
244 "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n"
245 ));
246 if f.is_async {
247 render_async_napi_body(&mut out, f, c_name, &m.path, prefix);
248 } else {
249 render_napi_body(&mut out, f, c_name, &m.path, prefix, &structs);
250 }
251 out.push_str("}\n\n");
252 }
253 }
254
255 out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n");
256 if !all_exports.is_empty() {
257 out.push_str(" napi_property_descriptor props[] = {\n");
258 for (js_name, napi_fn) in &all_exports {
259 out.push_str(&format!(
260 " {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n"
261 ));
262 }
263 out.push_str(" };\n");
264 out.push_str(&format!(
265 " napi_define_properties(env, exports, {}, props);\n",
266 all_exports.len()
267 ));
268 }
269 out.push_str(" return exports;\n");
270 out.push_str("}\n\n");
271 out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n\n");
272 out.push_str(&render_trailer(
273 CommentStyle::DoubleSlash,
274 "weaveffi_addon.c",
275 ));
276 out
277}
278
279fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str, prefix: &str) -> String {
280 match ret {
281 None => String::new(),
282 Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => ", const char* result".into(),
283 Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
284 ", const uint8_t* result, size_t result_len".into()
285 }
286 Some(TypeRef::List(inner)) => {
287 let et = c_elem_type(inner, module, prefix);
288 format!(", {et}* result, size_t result_len")
289 }
290 Some(TypeRef::Map(k, v)) => {
291 let kt = c_elem_type(k, module, prefix);
292 let vt = c_elem_type(v, module, prefix);
293 format!(", {kt}* result_keys, {vt}* result_values, size_t result_len")
294 }
295 Some(t) => format!(", {} result", c_ret_type_str(t, module, prefix)),
296 }
297}
298
299fn emit_async_resolve_value(
300 out: &mut String,
301 ret: Option<&TypeRef>,
302 module: &str,
303 prefix: &str,
304 structs: &HashMap<String, StructBinding>,
305) {
306 out.push_str(" napi_value val;\n");
307 match ret {
308 None => out.push_str(" napi_get_undefined(ctx->env, &val);\n"),
309 Some(TypeRef::I32) => out.push_str(" napi_create_int32(ctx->env, result, &val);\n"),
310 Some(TypeRef::U32) => out.push_str(" napi_create_uint32(ctx->env, result, &val);\n"),
311 Some(TypeRef::I64) => out.push_str(" napi_create_int64(ctx->env, result, &val);\n"),
312 Some(TypeRef::F64) => out.push_str(" napi_create_double(ctx->env, result, &val);\n"),
313 Some(TypeRef::Bool) => out.push_str(" napi_get_boolean(ctx->env, result, &val);\n"),
314 Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
315 out.push_str(
316 " napi_create_string_utf8(ctx->env, result, NAPI_AUTO_LENGTH, &val);\n",
317 );
318 }
319 Some(TypeRef::TypedHandle(_) | TypeRef::Handle) => {
320 out.push_str(" napi_create_int64(ctx->env, (int64_t)result, &val);\n");
321 }
322 Some(TypeRef::Struct(name)) => {
323 emit_struct_to_object(
324 out, "ctx->env", name, "result", "val", module, prefix, structs, " ", true,
325 );
326 }
327 Some(TypeRef::Enum(_)) => {
328 out.push_str(" napi_create_int32(ctx->env, (int32_t)result, &val);\n");
329 }
330 Some(TypeRef::Iterator(_)) => {
331 out.push_str(" napi_create_int64(ctx->env, (int64_t)(intptr_t)result, &val);\n");
332 }
333 _ => out.push_str(" napi_get_undefined(ctx->env, &val);\n"),
334 }
335 out.push_str(" napi_resolve_deferred(ctx->env, ctx->deferred, val);\n");
336}
337
338fn render_async_callback(
339 out: &mut String,
340 f: &FnBinding,
341 c_name: &str,
342 module: &str,
343 prefix: &str,
344 structs: &HashMap<String, StructBinding>,
345) {
346 let cb_name = format!("{c_name}_napi_cb");
347 let cb_result = async_cb_result_params_node(f.ret.as_ref(), module, prefix);
348
349 out.push_str(&format!(
350 "static void {cb_name}(void* context, weaveffi_error* err{cb_result}) {{\n"
351 ));
352 out.push_str(" weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)context;\n");
353 out.push_str(" if (err != NULL && err->code != 0) {\n");
354 out.push_str(" napi_value err_msg;\n");
355 out.push_str(
356 " napi_create_string_utf8(ctx->env, err->message, NAPI_AUTO_LENGTH, &err_msg);\n",
357 );
358 out.push_str(" napi_reject_deferred(ctx->env, ctx->deferred, err_msg);\n");
359 out.push_str(" } else {\n");
360 emit_async_resolve_value(out, f.ret.as_ref(), module, prefix, structs);
361 out.push_str(" }\n");
362 out.push_str(" free(ctx);\n");
363 out.push_str("}\n\n");
364}
365
366fn render_async_napi_body(
367 out: &mut String,
368 f: &FnBinding,
369 c_name: &str,
370 module: &str,
371 prefix: &str,
372) {
373 let n = f.params.len();
374 if n > 0 {
375 out.push_str(&format!(" size_t argc = {n};\n"));
376 out.push_str(&format!(" napi_value args[{n}];\n"));
377 out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
378 } else {
379 out.push_str(" size_t argc = 0;\n");
380 out.push_str(" napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
381 }
382
383 let mut c_args: Vec<String> = Vec::new();
384 let mut cleanups: Vec<String> = Vec::new();
385 for (i, p) in f.params.iter().enumerate() {
386 emit_param(
387 out,
388 &mut c_args,
389 &mut cleanups,
390 &p.ty,
391 &p.name,
392 i,
393 module,
394 prefix,
395 );
396 }
397
398 out.push_str(
399 " weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)malloc(sizeof(weaveffi_napi_async_ctx));\n",
400 );
401 out.push_str(" ctx->env = env;\n");
402 out.push_str(" napi_value promise;\n");
403 out.push_str(" napi_create_promise(env, &ctx->deferred, &promise);\n");
404
405 if f.cancellable {
406 c_args.push("NULL".into());
407 }
408
409 let cb_name = format!("{c_name}_napi_cb");
410 c_args.push(cb_name);
411 c_args.push("ctx".into());
412 let args_str = c_args.join(", ");
413 out.push_str(&format!(" {c_name}_async({args_str});\n"));
414
415 for cleanup in &cleanups {
416 out.push_str(cleanup);
417 }
418
419 out.push_str(" return promise;\n");
420}
421
422fn render_napi_body(
423 out: &mut String,
424 f: &FnBinding,
425 c_name: &str,
426 module: &str,
427 prefix: &str,
428 structs: &HashMap<String, StructBinding>,
429) {
430 let n = f.params.len();
431 if n > 0 {
432 out.push_str(&format!(" size_t argc = {n};\n"));
433 out.push_str(&format!(" napi_value args[{n}];\n"));
434 out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
435 } else {
436 out.push_str(" size_t argc = 0;\n");
437 out.push_str(" napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
438 }
439
440 let mut c_args: Vec<String> = Vec::new();
441 let mut cleanups: Vec<String> = Vec::new();
442 for (i, p) in f.params.iter().enumerate() {
443 emit_param(
444 out,
445 &mut c_args,
446 &mut cleanups,
447 &p.ty,
448 &p.name,
449 i,
450 module,
451 prefix,
452 );
453 }
454
455 out.push_str(" weaveffi_error err = {0};\n");
456
457 if let Some(ret) = &f.ret {
458 emit_ret_out_params(out, &mut c_args, ret, module, prefix);
459 }
460 c_args.push("&err".to_string());
461
462 let args_str = c_args.join(", ");
463 let ret_type = f.ret.as_ref().map(|r| c_ret_type_str(r, module, prefix));
464 match &ret_type {
465 Some(rt) if rt != "void" => {
466 out.push_str(&format!(" {rt} result = {c_name}({args_str});\n"));
467 }
468 _ => {
469 out.push_str(&format!(" {c_name}({args_str});\n"));
470 }
471 }
472
473 for cleanup in &cleanups {
474 out.push_str(cleanup);
475 }
476
477 out.push_str(" if (err.code != 0) {\n");
478 out.push_str(" napi_throw_error(env, NULL, err.message);\n");
479 out.push_str(" weaveffi_error_clear(&err);\n");
480 out.push_str(" return NULL;\n");
481 out.push_str(" }\n");
482
483 match &f.ret {
484 Some(ret) => emit_ret_to_napi(out, ret, module, prefix, &f.name, structs),
485 None => {
486 out.push_str(" napi_value ret;\n");
487 out.push_str(" napi_get_undefined(env, &ret);\n");
488 out.push_str(" return ret;\n");
489 }
490 }
491}
492
493#[allow(clippy::too_many_arguments)]
494fn emit_param(
495 out: &mut String,
496 c_args: &mut Vec<String>,
497 cleanups: &mut Vec<String>,
498 ty: &TypeRef,
499 name: &str,
500 idx: usize,
501 module: &str,
502 prefix: &str,
503) {
504 match ty {
505 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
506 let ct = c_elem_type(ty, module, prefix);
507 let getter = napi_getter(ty);
508 out.push_str(&format!(" {ct} {name};\n"));
509 out.push_str(&format!(" {getter}(env, args[{idx}], &{name});\n"));
510 c_args.push(name.into());
511 }
512 TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
513 out.push_str(&format!(" size_t {name}_len;\n"));
514 out.push_str(&format!(
515 " napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
516 ));
517 out.push_str(&format!(
518 " char* {name} = (char*)malloc({name}_len + 1);\n"
519 ));
520 out.push_str(&format!(
521 " napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
522 ));
523 c_args.push(name.into());
524 cleanups.push(format!(" free({name});\n"));
525 }
526 TypeRef::TypedHandle(_) | TypeRef::Handle => {
527 out.push_str(&format!(" int64_t {name}_raw;\n"));
528 out.push_str(&format!(
529 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
530 ));
531 c_args.push(format!("(weaveffi_handle_t){name}_raw"));
532 }
533 TypeRef::Enum(e) => {
534 out.push_str(&format!(" int32_t {name};\n"));
535 out.push_str(&format!(
536 " napi_get_value_int32(env, args[{idx}], &{name});\n"
537 ));
538 c_args.push(format!("({prefix}_{module}_{e}){name}"));
539 }
540 TypeRef::Struct(s) => {
541 let abi = c_abi_struct_name(s, module, prefix);
542 out.push_str(&format!(" int64_t {name}_raw;\n"));
543 out.push_str(&format!(
544 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
545 ));
546 c_args.push(format!("(const {abi}*)(intptr_t){name}_raw"));
547 }
548 TypeRef::Optional(inner) => {
549 out.push_str(&format!(" napi_valuetype {name}_type;\n"));
550 out.push_str(&format!(" napi_typeof(env, args[{idx}], &{name}_type);\n"));
551 emit_optional_param(out, c_args, cleanups, inner, name, idx, module, prefix);
552 }
553 TypeRef::List(inner) => {
554 emit_list_param(out, c_args, cleanups, inner, name, idx, module, prefix);
555 }
556 TypeRef::Bytes | TypeRef::BorrowedBytes => {
557 out.push_str(&format!(" void* {name}_raw;\n"));
558 out.push_str(&format!(" size_t {name}_len;\n"));
559 out.push_str(&format!(
560 " napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n"
561 ));
562 c_args.push(format!("(const uint8_t*){name}_raw"));
563 c_args.push(format!("{name}_len"));
564 }
565 TypeRef::Map(k, v) => {
566 emit_map_param(out, c_args, cleanups, k, v, name, idx, module, prefix);
567 }
568 TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
569 }
570}
571
572fn emit_opt_val(
573 out: &mut String,
574 c_args: &mut Vec<String>,
575 c_type: &str,
576 napi_fn: &str,
577 name: &str,
578 idx: usize,
579) {
580 out.push_str(&format!(" {c_type} {name}_val;\n"));
581 out.push_str(&format!(" const {c_type}* {name}_ptr = NULL;\n"));
582 out.push_str(&format!(
583 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
584 ));
585 out.push_str(&format!(" {napi_fn}(env, args[{idx}], &{name}_val);\n"));
586 out.push_str(&format!(" {name}_ptr = &{name}_val;\n"));
587 out.push_str(" }\n");
588 c_args.push(format!("{name}_ptr"));
589}
590
591#[allow(clippy::too_many_arguments)]
592fn emit_optional_param(
593 out: &mut String,
594 c_args: &mut Vec<String>,
595 cleanups: &mut Vec<String>,
596 inner: &TypeRef,
597 name: &str,
598 idx: usize,
599 module: &str,
600 prefix: &str,
601) {
602 match inner {
603 TypeRef::I32 => {
604 emit_opt_val(out, c_args, "int32_t", "napi_get_value_int32", name, idx);
605 }
606 TypeRef::U32 => {
607 emit_opt_val(out, c_args, "uint32_t", "napi_get_value_uint32", name, idx);
608 }
609 TypeRef::I64 => {
610 emit_opt_val(out, c_args, "int64_t", "napi_get_value_int64", name, idx);
611 }
612 TypeRef::F64 => {
613 emit_opt_val(out, c_args, "double", "napi_get_value_double", name, idx);
614 }
615 TypeRef::Bool => {
616 emit_opt_val(out, c_args, "bool", "napi_get_value_bool", name, idx);
617 }
618 TypeRef::TypedHandle(_) | TypeRef::Handle => {
619 out.push_str(&format!(" int64_t {name}_raw = 0;\n"));
620 out.push_str(&format!(" weaveffi_handle_t {name}_val;\n"));
621 out.push_str(&format!(" const weaveffi_handle_t* {name}_ptr = NULL;\n"));
622 out.push_str(&format!(
623 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
624 ));
625 out.push_str(&format!(
626 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
627 ));
628 out.push_str(&format!(
629 " {name}_val = (weaveffi_handle_t){name}_raw;\n"
630 ));
631 out.push_str(&format!(" {name}_ptr = &{name}_val;\n"));
632 out.push_str(" }\n");
633 c_args.push(format!("{name}_ptr"));
634 }
635 TypeRef::Enum(e) => {
636 let etype = format!("{prefix}_{module}_{e}");
637 out.push_str(&format!(" int32_t {name}_raw;\n"));
638 out.push_str(&format!(" {etype} {name}_val;\n"));
639 out.push_str(&format!(" const {etype}* {name}_ptr = NULL;\n"));
640 out.push_str(&format!(
641 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
642 ));
643 out.push_str(&format!(
644 " napi_get_value_int32(env, args[{idx}], &{name}_raw);\n"
645 ));
646 out.push_str(&format!(" {name}_val = ({etype}){name}_raw;\n"));
647 out.push_str(&format!(" {name}_ptr = &{name}_val;\n"));
648 out.push_str(" }\n");
649 c_args.push(format!("{name}_ptr"));
650 }
651 TypeRef::StringUtf8 => {
652 out.push_str(&format!(" char* {name} = NULL;\n"));
653 out.push_str(&format!(
654 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
655 ));
656 out.push_str(&format!(" size_t {name}_len;\n"));
657 out.push_str(&format!(
658 " napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
659 ));
660 out.push_str(&format!(" {name} = (char*)malloc({name}_len + 1);\n"));
661 out.push_str(&format!(
662 " napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
663 ));
664 out.push_str(" }\n");
665 c_args.push(name.into());
666 cleanups.push(format!(" free({name});\n"));
667 }
668 TypeRef::Struct(s) => {
669 let abi = c_abi_struct_name(s, module, prefix);
670 out.push_str(&format!(" int64_t {name}_raw = 0;\n"));
671 out.push_str(&format!(
672 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
673 ));
674 out.push_str(&format!(
675 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
676 ));
677 out.push_str(" }\n");
678 c_args.push(format!(
679 "{name}_raw ? (const {abi}*)(intptr_t){name}_raw : NULL"
680 ));
681 }
682 _ => {
683 emit_param(out, c_args, cleanups, inner, name, idx, module, prefix);
684 }
685 }
686}
687
688#[allow(clippy::too_many_arguments)]
689fn emit_list_param(
690 out: &mut String,
691 c_args: &mut Vec<String>,
692 cleanups: &mut Vec<String>,
693 inner: &TypeRef,
694 name: &str,
695 idx: usize,
696 module: &str,
697 prefix: &str,
698) {
699 let et = c_elem_type(inner, module, prefix);
700 out.push_str(&format!(" uint32_t {name}_count;\n"));
701 out.push_str(&format!(
702 " napi_get_array_length(env, args[{idx}], &{name}_count);\n"
703 ));
704 out.push_str(&format!(
705 " {et}* {name}_arr = ({et}*)malloc(sizeof({et}) * ({name}_count + 1));\n"
706 ));
707 out.push_str(&format!(
708 " for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
709 ));
710 out.push_str(&format!(" napi_value {name}_el;\n"));
711 out.push_str(&format!(
712 " napi_get_element(env, args[{idx}], {name}_i, &{name}_el);\n"
713 ));
714
715 match inner {
716 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
717 let getter = napi_getter(inner);
718 out.push_str(&format!(
719 " {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
720 ));
721 }
722 TypeRef::TypedHandle(_) | TypeRef::Handle => {
723 out.push_str(&format!(" int64_t {name}_h;\n"));
724 out.push_str(&format!(
725 " napi_get_value_int64(env, {name}_el, &{name}_h);\n"
726 ));
727 out.push_str(&format!(
728 " {name}_arr[{name}_i] = (weaveffi_handle_t){name}_h;\n"
729 ));
730 }
731 TypeRef::Enum(_) => {
732 out.push_str(&format!(" int32_t {name}_ev;\n"));
733 out.push_str(&format!(
734 " napi_get_value_int32(env, {name}_el, &{name}_ev);\n"
735 ));
736 out.push_str(&format!(" {name}_arr[{name}_i] = ({et}){name}_ev;\n"));
737 }
738 TypeRef::StringUtf8 => {
739 out.push_str(&format!(" size_t {name}_sl;\n"));
740 out.push_str(&format!(
741 " napi_get_value_string_utf8(env, {name}_el, NULL, 0, &{name}_sl);\n"
742 ));
743 out.push_str(&format!(
744 " char* {name}_s = (char*)malloc({name}_sl + 1);\n"
745 ));
746 out.push_str(&format!(
747 " napi_get_value_string_utf8(env, {name}_el, {name}_s, {name}_sl + 1, &{name}_sl);\n"
748 ));
749 out.push_str(&format!(" {name}_arr[{name}_i] = {name}_s;\n"));
750 }
751 TypeRef::Struct(_) => {
752 out.push_str(&format!(" int64_t {name}_sp;\n"));
753 out.push_str(&format!(
754 " napi_get_value_int64(env, {name}_el, &{name}_sp);\n"
755 ));
756 out.push_str(&format!(
757 " {name}_arr[{name}_i] = ({et})(intptr_t){name}_sp;\n"
758 ));
759 }
760 _ => {
761 let getter = napi_getter(inner);
762 out.push_str(&format!(
763 " {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
764 ));
765 }
766 }
767
768 out.push_str(" }\n");
769 c_args.push(format!("{name}_arr"));
770 c_args.push(format!("(size_t){name}_count"));
771
772 if matches!(inner, TypeRef::StringUtf8) {
773 cleanups.push(format!(
774 " for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_arr[{name}_j]);\n"
775 ));
776 }
777 cleanups.push(format!(" free({name}_arr);\n"));
778}
779
780#[allow(clippy::too_many_arguments)]
781fn emit_map_param(
782 out: &mut String,
783 c_args: &mut Vec<String>,
784 cleanups: &mut Vec<String>,
785 k: &TypeRef,
786 v: &TypeRef,
787 name: &str,
788 idx: usize,
789 module: &str,
790 prefix: &str,
791) {
792 let kt = c_elem_type(k, module, prefix);
793 let vt = c_elem_type(v, module, prefix);
794 out.push_str(&format!(" napi_value {name}_keys_napi;\n"));
795 out.push_str(&format!(
796 " napi_get_property_names(env, args[{idx}], &{name}_keys_napi);\n"
797 ));
798 out.push_str(&format!(" uint32_t {name}_count;\n"));
799 out.push_str(&format!(
800 " napi_get_array_length(env, {name}_keys_napi, &{name}_count);\n"
801 ));
802 out.push_str(&format!(
803 " {kt}* {name}_keys = ({kt}*)malloc(sizeof({kt}) * ({name}_count + 1));\n"
804 ));
805 out.push_str(&format!(
806 " {vt}* {name}_values = ({vt}*)malloc(sizeof({vt}) * ({name}_count + 1));\n"
807 ));
808 out.push_str(&format!(
809 " for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
810 ));
811 out.push_str(&format!(" napi_value {name}_k;\n"));
812 out.push_str(&format!(
813 " napi_get_element(env, {name}_keys_napi, {name}_i, &{name}_k);\n"
814 ));
815
816 if matches!(k, TypeRef::StringUtf8) {
817 out.push_str(&format!(" size_t {name}_kl;\n"));
818 out.push_str(&format!(
819 " napi_get_value_string_utf8(env, {name}_k, NULL, 0, &{name}_kl);\n"
820 ));
821 out.push_str(&format!(
822 " char* {name}_ks = (char*)malloc({name}_kl + 1);\n"
823 ));
824 out.push_str(&format!(
825 " napi_get_value_string_utf8(env, {name}_k, {name}_ks, {name}_kl + 1, &{name}_kl);\n"
826 ));
827 out.push_str(&format!(" {name}_keys[{name}_i] = {name}_ks;\n"));
828 } else {
829 out.push_str(&format!(" napi_value {name}_kn;\n"));
830 out.push_str(&format!(
831 " napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
832 ));
833 let kgetter = napi_getter(k);
834 out.push_str(&format!(
835 " {kgetter}(env, {name}_kn, &{name}_keys[{name}_i]);\n"
836 ));
837 }
838
839 out.push_str(&format!(" napi_value {name}_v;\n"));
840 out.push_str(&format!(
841 " napi_get_property(env, args[{idx}], {name}_k, &{name}_v);\n"
842 ));
843
844 if matches!(v, TypeRef::StringUtf8) {
845 out.push_str(&format!(" size_t {name}_vl;\n"));
846 out.push_str(&format!(
847 " napi_get_value_string_utf8(env, {name}_v, NULL, 0, &{name}_vl);\n"
848 ));
849 out.push_str(&format!(
850 " char* {name}_vs = (char*)malloc({name}_vl + 1);\n"
851 ));
852 out.push_str(&format!(
853 " napi_get_value_string_utf8(env, {name}_v, {name}_vs, {name}_vl + 1, &{name}_vl);\n"
854 ));
855 out.push_str(&format!(" {name}_values[{name}_i] = {name}_vs;\n"));
856 } else {
857 let vgetter = napi_getter(v);
858 out.push_str(&format!(
859 " {vgetter}(env, {name}_v, &{name}_values[{name}_i]);\n"
860 ));
861 }
862
863 out.push_str(" }\n");
864 c_args.push(format!("{name}_keys"));
865 c_args.push(format!("{name}_values"));
866 c_args.push(format!("(size_t){name}_count"));
867
868 if matches!(k, TypeRef::StringUtf8) {
869 cleanups.push(format!(
870 " for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_keys[{name}_j]);\n"
871 ));
872 }
873 cleanups.push(format!(" free({name}_keys);\n"));
874 if matches!(v, TypeRef::StringUtf8) {
875 cleanups.push(format!(
876 " for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_values[{name}_j]);\n"
877 ));
878 }
879 cleanups.push(format!(" free({name}_values);\n"));
880}
881
882fn emit_ret_out_params(
883 out: &mut String,
884 c_args: &mut Vec<String>,
885 ty: &TypeRef,
886 module: &str,
887 prefix: &str,
888) {
889 match ty {
890 TypeRef::Bytes | TypeRef::List(_) => {
891 out.push_str(" size_t out_len;\n");
892 c_args.push("&out_len".into());
893 }
894 TypeRef::Map(k, v) => {
895 let kt = c_elem_type(k, module, prefix);
896 let vt = c_elem_type(v, module, prefix);
897 out.push_str(&format!(" {kt}* out_keys = NULL;\n"));
898 out.push_str(&format!(" {vt}* out_values = NULL;\n"));
899 out.push_str(" size_t out_len = 0;\n");
900 c_args.push("out_keys".into());
901 c_args.push("out_values".into());
902 c_args.push("&out_len".into());
903 }
904 TypeRef::Optional(inner) if is_c_ptr_type(inner) => {
905 emit_ret_out_params(out, c_args, inner, module, prefix);
906 }
907 _ => {}
908 }
909}
910
911fn struct_registry(model: &BindingModel) -> HashMap<String, StructBinding> {
915 model
916 .modules
917 .iter()
918 .flat_map(|m| m.structs.iter())
919 .map(|s| (s.name.clone(), s.clone()))
920 .collect()
921}
922
923#[allow(clippy::too_many_arguments)]
928fn emit_struct_to_object(
929 out: &mut String,
930 env: &str,
931 struct_name: &str,
932 ptr_expr: &str,
933 obj_var: &str,
934 module: &str,
935 prefix: &str,
936 structs: &HashMap<String, StructBinding>,
937 indent: &str,
938 destroy: bool,
939) {
940 let Some(def) = structs.get(local_type_name(struct_name)).cloned() else {
941 out.push_str(&format!(
943 "{indent}napi_create_int64({env}, (int64_t)(intptr_t){ptr_expr}, &{obj_var});\n"
944 ));
945 return;
946 };
947 let abi = &def.c_tag;
948 let p = format!("{obj_var}_p");
949 out.push_str(&format!("{indent}{{\n"));
950 out.push_str(&format!("{indent} {abi}* {p} = ({abi}*){ptr_expr};\n"));
951 out.push_str(&format!(
952 "{indent} napi_create_object({env}, &{obj_var});\n"
953 ));
954 for field in &def.fields {
955 let getter = &field.getter_symbol;
956 let fv = format!("{obj_var}_{}", field.name);
957 out.push_str(&format!("{indent} napi_value {fv};\n"));
958 emit_struct_field_to_napi(
959 out,
960 env,
961 &field.ty,
962 getter,
963 &p,
964 &fv,
965 module,
966 prefix,
967 structs,
968 &format!("{indent} "),
969 );
970 out.push_str(&format!(
971 "{indent} napi_set_named_property({env}, {obj_var}, \"{}\", {fv});\n",
972 field.name
973 ));
974 }
975 if destroy {
976 out.push_str(&format!("{indent} {}({p});\n", def.destroy_symbol));
977 }
978 out.push_str(&format!("{indent}}}\n"));
979}
980
981#[allow(clippy::too_many_arguments)]
986fn emit_struct_field_to_napi(
987 out: &mut String,
988 env: &str,
989 ty: &TypeRef,
990 getter: &str,
991 pv: &str,
992 fv: &str,
993 module: &str,
994 prefix: &str,
995 structs: &HashMap<String, StructBinding>,
996 indent: &str,
997) {
998 match ty {
999 TypeRef::I32 => out.push_str(&format!(
1000 "{indent}napi_create_int32({env}, {getter}({pv}), &{fv});\n"
1001 )),
1002 TypeRef::U32 => out.push_str(&format!(
1003 "{indent}napi_create_uint32({env}, {getter}({pv}), &{fv});\n"
1004 )),
1005 TypeRef::I64 => out.push_str(&format!(
1006 "{indent}napi_create_int64({env}, {getter}({pv}), &{fv});\n"
1007 )),
1008 TypeRef::F64 => out.push_str(&format!(
1009 "{indent}napi_create_double({env}, {getter}({pv}), &{fv});\n"
1010 )),
1011 TypeRef::Bool => out.push_str(&format!(
1012 "{indent}napi_get_boolean({env}, {getter}({pv}), &{fv});\n"
1013 )),
1014 TypeRef::Enum(_) => out.push_str(&format!(
1015 "{indent}napi_create_int32({env}, (int32_t){getter}({pv}), &{fv});\n"
1016 )),
1017 TypeRef::Handle | TypeRef::TypedHandle(_) => out.push_str(&format!(
1018 "{indent}napi_create_int64({env}, (int64_t){getter}({pv}), &{fv});\n"
1019 )),
1020 TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1021 let owned = matches!(ty, TypeRef::StringUtf8);
1022 out.push_str(&format!("{indent}{{\n"));
1023 out.push_str(&format!(
1024 "{indent} char* {fv}_s = (char*){getter}({pv});\n"
1025 ));
1026 out.push_str(&format!(
1027 "{indent} napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});\n"
1028 ));
1029 if owned {
1030 out.push_str(&format!("{indent} weaveffi_free_string({fv}_s);\n"));
1031 }
1032 out.push_str(&format!("{indent}}}\n"));
1033 }
1034 TypeRef::Struct(name) => {
1035 emit_struct_to_object(
1036 out,
1037 env,
1038 name,
1039 &format!("{getter}({pv})"),
1040 fv,
1041 module,
1042 prefix,
1043 structs,
1044 indent,
1045 true,
1046 );
1047 }
1048 TypeRef::Optional(inner)
1049 if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
1050 {
1051 let owned = matches!(inner.as_ref(), TypeRef::StringUtf8);
1052 out.push_str(&format!("{indent}{{\n"));
1053 out.push_str(&format!(
1054 "{indent} char* {fv}_s = (char*){getter}({pv});\n"
1055 ));
1056 out.push_str(&format!(
1057 "{indent} if ({fv}_s == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1058 ));
1059 out.push_str(&format!(
1060 "{indent} else {{ napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});"
1061 ));
1062 if owned {
1063 out.push_str(&format!(" weaveffi_free_string({fv}_s);"));
1064 }
1065 out.push_str(" }\n");
1066 out.push_str(&format!("{indent}}}\n"));
1067 }
1068 TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Struct(_)) => {
1069 let TypeRef::Struct(name) = inner.as_ref() else {
1070 unreachable!()
1071 };
1072 let abi = c_abi_struct_name(name, module, prefix);
1073 out.push_str(&format!("{indent}{{\n"));
1074 out.push_str(&format!("{indent} {abi}* {fv}_sp = {getter}({pv});\n"));
1075 out.push_str(&format!(
1076 "{indent} if ({fv}_sp == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1077 ));
1078 out.push_str(&format!("{indent} else {{\n"));
1079 emit_struct_to_object(
1080 out,
1081 env,
1082 name,
1083 &format!("{fv}_sp"),
1084 fv,
1085 module,
1086 prefix,
1087 structs,
1088 &format!("{indent} "),
1089 true,
1090 );
1091 out.push_str(&format!("{indent} }}\n"));
1092 out.push_str(&format!("{indent}}}\n"));
1093 }
1094 _ => out.push_str(&format!("{indent}napi_get_null({env}, &{fv});\n")),
1095 }
1096}
1097
1098fn emit_ret_to_napi(
1099 out: &mut String,
1100 ty: &TypeRef,
1101 module: &str,
1102 prefix: &str,
1103 fn_name: &str,
1104 structs: &HashMap<String, StructBinding>,
1105) {
1106 out.push_str(" napi_value ret;\n");
1107 match ty {
1108 TypeRef::I32 => out.push_str(" napi_create_int32(env, result, &ret);\n"),
1109 TypeRef::U32 => out.push_str(" napi_create_uint32(env, result, &ret);\n"),
1110 TypeRef::I64 => out.push_str(" napi_create_int64(env, result, &ret);\n"),
1111 TypeRef::F64 => out.push_str(" napi_create_double(env, result, &ret);\n"),
1112 TypeRef::Bool => out.push_str(" napi_get_boolean(env, result, &ret);\n"),
1113 TypeRef::StringUtf8 => {
1114 out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1115 out.push_str(" weaveffi_free_string(result);\n");
1116 }
1117 TypeRef::BorrowedStr => {
1118 out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1119 }
1120 TypeRef::TypedHandle(_) | TypeRef::Handle => {
1121 out.push_str(" napi_create_int64(env, (int64_t)result, &ret);\n");
1122 }
1123 TypeRef::Struct(name) => {
1124 emit_struct_to_object(
1125 out, "env", name, "result", "ret", module, prefix, structs, " ", true,
1126 );
1127 }
1128 TypeRef::Enum(_) => {
1129 out.push_str(" napi_create_int32(env, (int32_t)result, &ret);\n");
1130 }
1131 TypeRef::Bytes => {
1132 out.push_str(" napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
1133 out.push_str(" weaveffi_free_bytes((uint8_t*)result, out_len);\n");
1134 }
1135 TypeRef::BorrowedBytes => {
1136 out.push_str(" napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
1137 }
1138 TypeRef::Optional(inner) => {
1139 out.push_str(" if (result == NULL) {\n");
1140 out.push_str(" napi_get_null(env, &ret);\n");
1141 out.push_str(" } else {\n");
1142 emit_optional_ret_inner(out, inner, module, prefix, structs);
1143 out.push_str(" }\n");
1144 }
1145 TypeRef::List(inner) => emit_list_ret(out, inner, module, prefix, " ", structs),
1146 TypeRef::Map(_, _) => {
1147 out.push_str(" napi_create_object(env, &ret);\n");
1148 }
1149 TypeRef::Iterator(inner) => {
1150 let fn_pascal = fn_name.to_upper_camel_case();
1151 let iter_type = format!("{prefix}_{module}_{fn_pascal}Iterator");
1152 let et = c_elem_type(inner, module, prefix);
1153 out.push_str(" napi_create_array(env, &ret);\n");
1154 out.push_str(" uint32_t iter_idx = 0;\n");
1155 out.push_str(&format!(" {et} iter_item;\n"));
1156 out.push_str(&format!(
1157 " while ({iter_type}_next(result, &iter_item)) {{\n"
1158 ));
1159 out.push_str(" napi_value elem;\n");
1160 match inner.as_ref() {
1161 TypeRef::I32 => {
1162 out.push_str(" napi_create_int32(env, iter_item, &elem);\n");
1163 }
1164 TypeRef::U32 => {
1165 out.push_str(" napi_create_uint32(env, iter_item, &elem);\n");
1166 }
1167 TypeRef::I64 => {
1168 out.push_str(" napi_create_int64(env, iter_item, &elem);\n");
1169 }
1170 TypeRef::F64 => {
1171 out.push_str(" napi_create_double(env, iter_item, &elem);\n");
1172 }
1173 TypeRef::Bool => {
1174 out.push_str(" napi_get_boolean(env, iter_item, &elem);\n");
1175 }
1176 TypeRef::TypedHandle(_) | TypeRef::Handle => {
1177 out.push_str(" napi_create_int64(env, (int64_t)iter_item, &elem);\n");
1178 }
1179 TypeRef::StringUtf8 => {
1180 out.push_str(
1181 " napi_create_string_utf8(env, iter_item, NAPI_AUTO_LENGTH, &elem);\n",
1182 );
1183 out.push_str(" weaveffi_free_string(iter_item);\n");
1184 }
1185 TypeRef::Struct(_) | TypeRef::Enum(_) => {
1186 out.push_str(
1187 " napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
1188 );
1189 }
1190 _ => {
1191 out.push_str(" napi_create_int64(env, (int64_t)iter_item, &elem);\n");
1192 }
1193 }
1194 out.push_str(" napi_set_element(env, ret, iter_idx++, elem);\n");
1195 out.push_str(" }\n");
1196 out.push_str(&format!(" {iter_type}_destroy(result);\n"));
1197 }
1198 }
1199 out.push_str(" return ret;\n");
1200}
1201
1202fn emit_optional_ret_inner(
1203 out: &mut String,
1204 inner: &TypeRef,
1205 module: &str,
1206 prefix: &str,
1207 structs: &HashMap<String, StructBinding>,
1208) {
1209 match inner {
1210 TypeRef::I32 => {
1211 out.push_str(" napi_create_int32(env, *result, &ret);\n");
1212 out.push_str(" free(result);\n");
1213 }
1214 TypeRef::U32 => {
1215 out.push_str(" napi_create_uint32(env, *result, &ret);\n");
1216 out.push_str(" free(result);\n");
1217 }
1218 TypeRef::I64 => {
1219 out.push_str(" napi_create_int64(env, *result, &ret);\n");
1220 out.push_str(" free(result);\n");
1221 }
1222 TypeRef::F64 => {
1223 out.push_str(" napi_create_double(env, *result, &ret);\n");
1224 out.push_str(" free(result);\n");
1225 }
1226 TypeRef::Bool => {
1227 out.push_str(" napi_get_boolean(env, *result, &ret);\n");
1228 out.push_str(" free(result);\n");
1229 }
1230 TypeRef::TypedHandle(_) | TypeRef::Handle => {
1231 out.push_str(" napi_create_int64(env, (int64_t)*result, &ret);\n");
1232 out.push_str(" free(result);\n");
1233 }
1234 TypeRef::Enum(_) => {
1235 out.push_str(" napi_create_int32(env, (int32_t)*result, &ret);\n");
1236 out.push_str(" free(result);\n");
1237 }
1238 TypeRef::StringUtf8 => {
1239 out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1240 out.push_str(" weaveffi_free_string(result);\n");
1241 }
1242 TypeRef::Struct(name) => {
1243 emit_struct_to_object(
1244 out, "env", name, "result", "ret", module, prefix, structs, " ", true,
1245 );
1246 }
1247 TypeRef::List(li) => emit_list_ret(out, li, module, prefix, " ", structs),
1248 _ => out.push_str(" napi_get_null(env, &ret);\n"),
1249 }
1250}
1251
1252fn emit_list_ret(
1253 out: &mut String,
1254 inner: &TypeRef,
1255 module: &str,
1256 prefix: &str,
1257 ind: &str,
1258 structs: &HashMap<String, StructBinding>,
1259) {
1260 out.push_str(&format!(
1261 "{ind}napi_create_array_with_length(env, out_len, &ret);\n"
1262 ));
1263 out.push_str(&format!(
1264 "{ind}for (size_t ret_i = 0; ret_i < out_len; ret_i++) {{\n"
1265 ));
1266 out.push_str(&format!("{ind} napi_value elem;\n"));
1267 match inner {
1268 TypeRef::I32 => out.push_str(&format!(
1269 "{ind} napi_create_int32(env, result[ret_i], &elem);\n"
1270 )),
1271 TypeRef::U32 => out.push_str(&format!(
1272 "{ind} napi_create_uint32(env, result[ret_i], &elem);\n"
1273 )),
1274 TypeRef::I64 => out.push_str(&format!(
1275 "{ind} napi_create_int64(env, result[ret_i], &elem);\n"
1276 )),
1277 TypeRef::F64 => out.push_str(&format!(
1278 "{ind} napi_create_double(env, result[ret_i], &elem);\n"
1279 )),
1280 TypeRef::Bool => out.push_str(&format!(
1281 "{ind} napi_get_boolean(env, result[ret_i], &elem);\n"
1282 )),
1283 TypeRef::TypedHandle(_) | TypeRef::Handle => out.push_str(&format!(
1284 "{ind} napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1285 )),
1286 TypeRef::StringUtf8 => {
1287 out.push_str(&format!(
1288 "{ind} napi_create_string_utf8(env, result[ret_i], NAPI_AUTO_LENGTH, &elem);\n"
1289 ));
1290 out.push_str(&format!("{ind} weaveffi_free_string(result[ret_i]);\n"));
1291 }
1292 TypeRef::Enum(_) => out.push_str(&format!(
1293 "{ind} napi_create_int32(env, (int32_t)result[ret_i], &elem);\n"
1294 )),
1295 TypeRef::Struct(name) => {
1296 let elem_indent = format!("{ind} ");
1297 emit_struct_to_object(
1298 out,
1299 "env",
1300 name,
1301 "result[ret_i]",
1302 "elem",
1303 module,
1304 prefix,
1305 structs,
1306 &elem_indent,
1307 true,
1308 );
1309 }
1310 _ => out.push_str(&format!(
1311 "{ind} napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1312 )),
1313 }
1314 out.push_str(&format!(
1315 "{ind} napi_set_element(env, ret, (uint32_t)ret_i, elem);\n"
1316 ));
1317 out.push_str(&format!("{ind}}}\n"));
1318 out.push_str(&format!("{ind}free(result);\n"));
1319}
1320
1321fn ts_type_for(ty: &TypeRef) -> String {
1322 match ty {
1323 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 => "number".into(),
1324 TypeRef::Bool => "boolean".into(),
1325 TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(),
1326 TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(),
1327 TypeRef::Handle => "bigint".into(),
1328 TypeRef::TypedHandle(name) => name.clone(),
1329 TypeRef::Struct(name) => local_type_name(name).to_string(),
1330 TypeRef::Enum(name) => name.clone(),
1331 TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
1332 TypeRef::List(inner) => {
1333 let inner_ts = ts_type_for(inner);
1334 if matches!(inner.as_ref(), TypeRef::Optional(_)) {
1335 format!("({inner_ts})[]")
1336 } else {
1337 format!("{inner_ts}[]")
1338 }
1339 }
1340 TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)),
1341 TypeRef::Iterator(inner) => {
1342 let t = ts_type_for(inner);
1343 format!("{t}[]")
1344 }
1345 }
1346}
1347
1348fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str) {
1351 common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc);
1352}
1353
1354fn emit_fn_doc(
1357 out: &mut String,
1358 doc: &Option<String>,
1359 params: &[ParamBinding],
1360 indent: &str,
1361 extra_tags: &[String],
1362) {
1363 let has_param_docs = params.iter().any(|p| p.doc.is_some());
1364 let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty());
1365 if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() {
1366 return;
1367 }
1368 out.push_str(indent);
1369 out.push_str("/**\n");
1370 if let Some(d) = trimmed_doc {
1371 for line in d.lines() {
1372 out.push_str(indent);
1373 if line.is_empty() {
1374 out.push_str(" *\n");
1375 } else {
1376 out.push_str(" * ");
1377 out.push_str(line);
1378 out.push('\n');
1379 }
1380 }
1381 }
1382 for p in params {
1383 if let Some(pdoc) = &p.doc {
1384 let pdoc = pdoc.trim();
1385 if pdoc.is_empty() {
1386 continue;
1387 }
1388 let mut lines = pdoc.lines();
1389 if let Some(first) = lines.next() {
1390 out.push_str(indent);
1391 out.push_str(&format!(" * @param {} {}\n", p.name, first));
1392 }
1393 for line in lines {
1394 out.push_str(indent);
1395 if line.is_empty() {
1396 out.push_str(" *\n");
1397 } else {
1398 out.push_str(" * ");
1399 out.push_str(line);
1400 out.push('\n');
1401 }
1402 }
1403 }
1404 }
1405 for tag in extra_tags {
1406 out.push_str(indent);
1407 out.push_str(" * ");
1408 out.push_str(tag);
1409 out.push('\n');
1410 }
1411 out.push_str(indent);
1412 out.push_str(" */\n");
1413}
1414
1415fn render_struct_builder_dts(out: &mut String, s: &StructBinding) {
1416 let name = &s.name;
1417 emit_doc(out, &s.doc, "");
1418 out.push_str(&format!("export interface {}Builder {{\n", s.name));
1419 for field in &s.fields {
1420 let method = format!("with{}", field.name.to_upper_camel_case());
1421 let ts = ts_type_for(&field.ty);
1422 emit_doc(out, &field.doc, " ");
1423 out.push_str(&format!(" {method}(value: {ts}): {name}Builder;\n"));
1424 }
1425 out.push_str(&format!(" build(): {name};\n"));
1426 out.push_str("}\n");
1427}
1428
1429fn render_node_dts(
1430 api: &Api,
1431 prefix: &str,
1432 strip_module_prefix: bool,
1433 input_basename: &str,
1434) -> String {
1435 let model = BindingModel::build(api, prefix);
1436 let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
1437 out.push_str("// Generated types for WeaveFFI functions\n");
1438 for m in &model.modules {
1439 for s in &m.structs {
1440 emit_doc(&mut out, &s.doc, "");
1441 out.push_str(&format!("export interface {} {{\n", s.name));
1442 for field in &s.fields {
1443 emit_doc(&mut out, &field.doc, " ");
1444 out.push_str(&format!(" {}: {};\n", field.name, ts_type_for(&field.ty)));
1445 }
1446 out.push_str("}\n");
1447 if s.builder.is_some() {
1448 render_struct_builder_dts(&mut out, s);
1449 }
1450 }
1451 for e in &m.enums {
1452 emit_doc(&mut out, &e.doc, "");
1453 out.push_str(&format!("export enum {} {{\n", e.name));
1454 for v in &e.variants {
1455 emit_doc(&mut out, &v.doc, " ");
1456 out.push_str(&format!(" {} = {},\n", v.name, v.value));
1457 }
1458 out.push_str("}\n");
1459 }
1460 out.push_str(&format!("// module {}\n", m.path));
1461 for f in &m.functions {
1462 let params: Vec<String> = f
1463 .params
1464 .iter()
1465 .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
1466 .collect();
1467 let base_ret = match &f.ret {
1468 Some(ty) => ts_type_for(ty),
1469 None => "void".into(),
1470 };
1471 let ret = if f.is_async {
1472 format!("Promise<{base_ret}>")
1473 } else {
1474 base_ret
1475 };
1476 let ts_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
1477 let mut tags = vec![format!("Maps to C function: {}", f.c_base)];
1478 if let Some(msg) = &f.deprecated {
1479 tags.push(format!("@deprecated {}", msg));
1480 }
1481 emit_fn_doc(&mut out, &f.doc, &f.params, "", &tags);
1482 out.push_str(&format!(
1483 "export function {}({}): {}\n",
1484 ts_name,
1485 params.join(", "),
1486 ret
1487 ));
1488 }
1489 }
1490 out.push('\n');
1491 out.push_str(&render_trailer(CommentStyle::DoubleSlash, "types.d.ts"));
1492 out
1493}
1494
1495#[cfg(test)]
1496mod tests {
1497 use super::*;
1498 use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
1499
1500 fn make_api(modules: Vec<Module>) -> Api {
1501 Api {
1502 version: "0.1.0".into(),
1503 modules,
1504 generators: None,
1505 }
1506 }
1507
1508 fn make_module(name: &str) -> Module {
1509 Module {
1510 name: name.into(),
1511 functions: vec![],
1512 structs: vec![],
1513 enums: vec![],
1514 callbacks: vec![],
1515 listeners: vec![],
1516 errors: None,
1517 modules: vec![],
1518 }
1519 }
1520
1521 #[test]
1522 fn ts_type_for_primitives() {
1523 assert_eq!(ts_type_for(&TypeRef::I32), "number");
1524 assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
1525 assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
1526 assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
1527 assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
1528 }
1529
1530 #[test]
1531 fn ts_type_for_struct_and_enum() {
1532 assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
1533 assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
1534 }
1535
1536 #[test]
1537 fn ts_type_for_optional() {
1538 let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
1539 assert_eq!(ts_type_for(&ty), "string | null");
1540 }
1541
1542 #[test]
1543 fn ts_type_for_list() {
1544 let ty = TypeRef::List(Box::new(TypeRef::I32));
1545 assert_eq!(ts_type_for(&ty), "number[]");
1546 }
1547
1548 #[test]
1549 fn ts_type_for_list_of_optional() {
1550 let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
1551 assert_eq!(ts_type_for(&ty), "(number | null)[]");
1552 }
1553
1554 #[test]
1555 fn ts_type_for_map() {
1556 let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1557 assert_eq!(ts_type_for(&ty), "Record<string, number>");
1558 }
1559
1560 #[test]
1561 fn ts_type_for_optional_list() {
1562 let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
1563 assert_eq!(ts_type_for(&ty), "number[] | null");
1564 }
1565
1566 #[test]
1567 fn generate_node_dts_with_structs() {
1568 let mut m = make_module("contacts");
1569 m.structs.push(StructDef {
1570 name: "Contact".into(),
1571 doc: None,
1572 fields: vec![
1573 StructField {
1574 name: "name".into(),
1575 ty: TypeRef::StringUtf8,
1576 doc: None,
1577 default: None,
1578 },
1579 StructField {
1580 name: "age".into(),
1581 ty: TypeRef::I32,
1582 doc: None,
1583 default: None,
1584 },
1585 StructField {
1586 name: "active".into(),
1587 ty: TypeRef::Bool,
1588 doc: None,
1589 default: None,
1590 },
1591 ],
1592 builder: false,
1593 });
1594 m.enums.push(EnumDef {
1595 name: "Color".into(),
1596 doc: None,
1597 variants: vec![
1598 EnumVariant {
1599 name: "Red".into(),
1600 value: 0,
1601 doc: None,
1602 },
1603 EnumVariant {
1604 name: "Green".into(),
1605 value: 1,
1606 doc: None,
1607 },
1608 EnumVariant {
1609 name: "Blue".into(),
1610 value: 2,
1611 doc: None,
1612 },
1613 ],
1614 });
1615 m.functions.push(Function {
1616 name: "get_contact".into(),
1617 params: vec![Param {
1618 name: "id".into(),
1619 ty: TypeRef::I32,
1620 mutable: false,
1621 doc: None,
1622 }],
1623 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1624 "Contact".into(),
1625 )))),
1626 doc: None,
1627 r#async: false,
1628 cancellable: false,
1629 deprecated: None,
1630 since: None,
1631 });
1632 m.functions.push(Function {
1633 name: "list_contacts".into(),
1634 params: vec![],
1635 returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1636 doc: None,
1637 r#async: false,
1638 cancellable: false,
1639 deprecated: None,
1640 since: None,
1641 });
1642
1643 let dts = render_node_dts(&make_api(vec![m]), "weaveffi", true, "weaveffi.yml");
1644
1645 assert!(dts.contains("export interface Contact {"));
1646 assert!(dts.contains(" name: string;"));
1647 assert!(dts.contains(" age: number;"));
1648 assert!(dts.contains(" active: boolean;"));
1649 assert!(dts.contains("export enum Color {"));
1650 assert!(dts.contains(" Red = 0,"));
1651 assert!(dts.contains(" Green = 1,"));
1652 assert!(dts.contains(" Blue = 2,"));
1653 assert!(dts.contains("export function get_contact(id: number): Contact | null"));
1654 assert!(dts.contains("export function list_contacts(): Contact[]"));
1655
1656 let iface_pos = dts.find("export interface Contact").unwrap();
1657 let enum_pos = dts.find("export enum Color").unwrap();
1658 let fn_pos = dts.find("export function get_contact").unwrap();
1659 assert!(
1660 iface_pos < fn_pos,
1661 "interface should appear before functions"
1662 );
1663 assert!(enum_pos < fn_pos, "enum should appear before functions");
1664 }
1665
1666 #[test]
1667 fn node_generates_binding_gyp() {
1668 let api = make_api(vec![{
1669 let mut m = make_module("math");
1670 m.functions.push(Function {
1671 name: "add".into(),
1672 params: vec![
1673 Param {
1674 name: "a".into(),
1675 ty: TypeRef::I32,
1676 mutable: false,
1677 doc: None,
1678 },
1679 Param {
1680 name: "b".into(),
1681 ty: TypeRef::I32,
1682 mutable: false,
1683 doc: None,
1684 },
1685 ],
1686 returns: Some(TypeRef::I32),
1687 doc: None,
1688 r#async: false,
1689 cancellable: false,
1690 deprecated: None,
1691 since: None,
1692 });
1693 m
1694 }]);
1695
1696 let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp");
1697 let _ = std::fs::remove_dir_all(&tmp);
1698 std::fs::create_dir_all(&tmp).unwrap();
1699 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1700
1701 NodeGenerator
1702 .generate(&api, out_dir, &NodeConfig::default())
1703 .unwrap();
1704
1705 let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap();
1706 assert!(
1707 gyp.contains("\"target_name\": \"weaveffi\""),
1708 "missing target_name: {gyp}"
1709 );
1710 assert!(
1711 gyp.contains("weaveffi_addon.c"),
1712 "missing source file: {gyp}"
1713 );
1714
1715 let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap();
1716 assert!(
1717 addon.contains("napi_value Init("),
1718 "missing Init function: {addon}"
1719 );
1720 assert!(
1721 addon.contains("weaveffi_math_add"),
1722 "missing C ABI call: {addon}"
1723 );
1724 assert!(
1725 addon.contains("napi_get_cb_info"),
1726 "missing napi_get_cb_info call: {addon}"
1727 );
1728
1729 let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1730 assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}");
1731 assert!(
1732 pkg.contains("node-gyp rebuild"),
1733 "missing install script: {pkg}"
1734 );
1735
1736 let _ = std::fs::remove_dir_all(&tmp);
1737 }
1738
1739 #[test]
1740 fn generate_node_dts_with_structs_and_enums() {
1741 let api = make_api(vec![Module {
1742 name: "contacts".to_string(),
1743 functions: vec![
1744 Function {
1745 name: "get_contact".to_string(),
1746 params: vec![Param {
1747 name: "id".to_string(),
1748 ty: TypeRef::I32,
1749 mutable: false,
1750 doc: None,
1751 }],
1752 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1753 "Contact".into(),
1754 )))),
1755 doc: None,
1756 r#async: false,
1757 cancellable: false,
1758 deprecated: None,
1759 since: None,
1760 },
1761 Function {
1762 name: "list_contacts".to_string(),
1763 params: vec![],
1764 returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1765 doc: None,
1766 r#async: false,
1767 cancellable: false,
1768 deprecated: None,
1769 since: None,
1770 },
1771 Function {
1772 name: "set_favorite_color".to_string(),
1773 params: vec![
1774 Param {
1775 name: "contact_id".to_string(),
1776 ty: TypeRef::I32,
1777 mutable: false,
1778 doc: None,
1779 },
1780 Param {
1781 name: "color".to_string(),
1782 ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
1783 mutable: false,
1784 doc: None,
1785 },
1786 ],
1787 returns: None,
1788 doc: None,
1789 r#async: false,
1790 cancellable: false,
1791 deprecated: None,
1792 since: None,
1793 },
1794 Function {
1795 name: "get_tags".to_string(),
1796 params: vec![Param {
1797 name: "contact_id".to_string(),
1798 ty: TypeRef::I32,
1799 mutable: false,
1800 doc: None,
1801 }],
1802 returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
1803 doc: None,
1804 r#async: false,
1805 cancellable: false,
1806 deprecated: None,
1807 since: None,
1808 },
1809 ],
1810 structs: vec![StructDef {
1811 name: "Contact".to_string(),
1812 doc: None,
1813 fields: vec![
1814 StructField {
1815 name: "name".to_string(),
1816 ty: TypeRef::StringUtf8,
1817 doc: None,
1818 default: None,
1819 },
1820 StructField {
1821 name: "email".to_string(),
1822 ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1823 doc: None,
1824 default: None,
1825 },
1826 StructField {
1827 name: "tags".to_string(),
1828 ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
1829 doc: None,
1830 default: None,
1831 },
1832 ],
1833 builder: false,
1834 }],
1835 enums: vec![EnumDef {
1836 name: "Color".to_string(),
1837 doc: None,
1838 variants: vec![
1839 EnumVariant {
1840 name: "Red".to_string(),
1841 value: 0,
1842 doc: None,
1843 },
1844 EnumVariant {
1845 name: "Green".to_string(),
1846 value: 1,
1847 doc: None,
1848 },
1849 EnumVariant {
1850 name: "Blue".to_string(),
1851 value: 2,
1852 doc: None,
1853 },
1854 ],
1855 }],
1856 callbacks: vec![],
1857 listeners: vec![],
1858 errors: None,
1859 modules: vec![],
1860 }]);
1861
1862 let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
1863 let _ = std::fs::remove_dir_all(&tmp);
1864 std::fs::create_dir_all(&tmp).unwrap();
1865 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1866
1867 NodeGenerator
1868 .generate(
1869 &api,
1870 out_dir,
1871 &NodeConfig {
1872 strip_module_prefix: true,
1873 ..NodeConfig::default()
1874 },
1875 )
1876 .unwrap();
1877
1878 let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
1879
1880 assert!(
1881 dts.contains("export interface Contact {"),
1882 "missing Contact interface: {dts}"
1883 );
1884 assert!(dts.contains(" name: string;"), "missing name field: {dts}");
1885 assert!(
1886 dts.contains(" email: string | null;"),
1887 "missing optional email field: {dts}"
1888 );
1889 assert!(
1890 dts.contains(" tags: string[];"),
1891 "missing list tags field: {dts}"
1892 );
1893
1894 assert!(
1895 dts.contains("export enum Color {"),
1896 "missing Color enum: {dts}"
1897 );
1898 assert!(dts.contains(" Red = 0,"), "missing Red variant: {dts}");
1899 assert!(dts.contains(" Green = 1,"), "missing Green variant: {dts}");
1900 assert!(dts.contains(" Blue = 2,"), "missing Blue variant: {dts}");
1901
1902 assert!(
1903 dts.contains("export function get_contact(id: number): Contact | null"),
1904 "missing get_contact with optional return: {dts}"
1905 );
1906 assert!(
1907 dts.contains("export function list_contacts(): Contact[]"),
1908 "missing list_contacts with list return: {dts}"
1909 );
1910 assert!(
1911 dts.contains(
1912 "export function set_favorite_color(contact_id: number, color: Color | null): void"
1913 ),
1914 "missing set_favorite_color with optional enum param: {dts}"
1915 );
1916 assert!(
1917 dts.contains("export function get_tags(contact_id: number): string[]"),
1918 "missing get_tags with list return: {dts}"
1919 );
1920
1921 let iface_pos = dts.find("export interface Contact").unwrap();
1922 let enum_pos = dts.find("export enum Color").unwrap();
1923 let fn_pos = dts.find("export function get_contact").unwrap();
1924 assert!(
1925 iface_pos < fn_pos,
1926 "interface should appear before functions"
1927 );
1928 assert!(enum_pos < fn_pos, "enum should appear before functions");
1929
1930 let _ = std::fs::remove_dir_all(&tmp);
1931 }
1932
1933 #[test]
1934 fn node_custom_package_name() {
1935 let api = make_api(vec![make_module("math")]);
1936
1937 let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg");
1938 let _ = std::fs::remove_dir_all(&tmp);
1939 std::fs::create_dir_all(&tmp).unwrap();
1940 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1941
1942 let config = NodeConfig {
1943 package_name: Some("@myorg/cool-lib".into()),
1944 ..NodeConfig::default()
1945 };
1946 NodeGenerator.generate(&api, out_dir, &config).unwrap();
1947
1948 let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1949 assert!(
1950 pkg.contains("\"name\": \"@myorg/cool-lib\""),
1951 "package.json should use custom name: {pkg}"
1952 );
1953 assert!(
1954 !pkg.contains("\"name\": \"weaveffi\""),
1955 "package.json should not contain default name: {pkg}"
1956 );
1957
1958 let _ = std::fs::remove_dir_all(&tmp);
1959 }
1960
1961 #[test]
1962 fn node_dts_has_jsdoc() {
1963 let api = make_api(vec![{
1964 let mut m = make_module("math");
1965 m.functions.push(Function {
1966 name: "add".into(),
1967 params: vec![
1968 Param {
1969 name: "a".into(),
1970 ty: TypeRef::I32,
1971 mutable: false,
1972 doc: None,
1973 },
1974 Param {
1975 name: "b".into(),
1976 ty: TypeRef::I32,
1977 mutable: false,
1978 doc: None,
1979 },
1980 ],
1981 returns: Some(TypeRef::I32),
1982 doc: None,
1983 r#async: false,
1984 cancellable: false,
1985 deprecated: None,
1986 since: None,
1987 });
1988 m.functions.push(Function {
1989 name: "subtract".into(),
1990 params: vec![
1991 Param {
1992 name: "a".into(),
1993 ty: TypeRef::I32,
1994 mutable: false,
1995 doc: None,
1996 },
1997 Param {
1998 name: "b".into(),
1999 ty: TypeRef::I32,
2000 mutable: false,
2001 doc: None,
2002 },
2003 ],
2004 returns: Some(TypeRef::I32),
2005 doc: None,
2006 r#async: false,
2007 cancellable: false,
2008 deprecated: None,
2009 since: None,
2010 });
2011 m
2012 }]);
2013
2014 let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2015
2016 assert!(
2017 dts.contains("Maps to C function: weaveffi_math_add"),
2018 "missing JSDoc for add: {dts}"
2019 );
2020 assert!(
2021 dts.contains("Maps to C function: weaveffi_math_subtract"),
2022 "missing JSDoc for subtract: {dts}"
2023 );
2024 }
2025
2026 #[test]
2027 fn node_addon_has_no_todo() {
2028 let api = make_api(vec![{
2029 let mut m = make_module("math");
2030 m.functions.push(Function {
2031 name: "add".into(),
2032 params: vec![
2033 Param {
2034 name: "a".into(),
2035 ty: TypeRef::I32,
2036 mutable: false,
2037 doc: None,
2038 },
2039 Param {
2040 name: "b".into(),
2041 ty: TypeRef::I32,
2042 mutable: false,
2043 doc: None,
2044 },
2045 ],
2046 returns: Some(TypeRef::I32),
2047 doc: None,
2048 r#async: false,
2049 cancellable: false,
2050 deprecated: None,
2051 since: None,
2052 });
2053 m
2054 }]);
2055 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2056 assert!(
2057 !addon.contains("// TODO: implement"),
2058 "generated addon.c should not contain TODO comments: {addon}"
2059 );
2060 }
2061
2062 #[test]
2063 fn node_addon_extracts_args() {
2064 let api = make_api(vec![{
2065 let mut m = make_module("math");
2066 m.functions.push(Function {
2067 name: "add".into(),
2068 params: vec![
2069 Param {
2070 name: "a".into(),
2071 ty: TypeRef::I32,
2072 mutable: false,
2073 doc: None,
2074 },
2075 Param {
2076 name: "b".into(),
2077 ty: TypeRef::I32,
2078 mutable: false,
2079 doc: None,
2080 },
2081 ],
2082 returns: Some(TypeRef::I32),
2083 doc: None,
2084 r#async: false,
2085 cancellable: false,
2086 deprecated: None,
2087 since: None,
2088 });
2089 m
2090 }]);
2091 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2092 assert!(
2093 addon.contains("napi_get_cb_info"),
2094 "generated addon.c should call napi_get_cb_info: {addon}"
2095 );
2096 }
2097
2098 #[test]
2099 fn node_addon_frees_strings() {
2100 let api = make_api(vec![{
2101 let mut m = make_module("greet");
2102 m.functions.push(Function {
2103 name: "hello".into(),
2104 params: vec![Param {
2105 name: "name".into(),
2106 ty: TypeRef::StringUtf8,
2107 mutable: false,
2108 doc: None,
2109 }],
2110 returns: Some(TypeRef::StringUtf8),
2111 doc: None,
2112 r#async: false,
2113 cancellable: false,
2114 deprecated: None,
2115 since: None,
2116 });
2117 m
2118 }]);
2119 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2120 assert!(
2121 addon.contains("weaveffi_free_string(result)"),
2122 "generated addon should free returned strings: {addon}"
2123 );
2124 assert!(
2125 addon.contains("#include <string.h>"),
2126 "generated addon should include string.h: {addon}"
2127 );
2128 assert!(
2129 addon.contains("#include <stdlib.h>"),
2130 "generated addon should include stdlib.h: {addon}"
2131 );
2132 assert!(
2133 addon.contains("weaveffi_error_clear(&err)"),
2134 "generated addon should clear errors: {addon}"
2135 );
2136 }
2137
2138 #[test]
2139 fn node_custom_prefix_threads_to_user_symbols() {
2140 let api = make_api(vec![{
2141 let mut m = make_module("greet");
2142 m.functions.push(Function {
2143 name: "hello".into(),
2144 params: vec![Param {
2145 name: "name".into(),
2146 ty: TypeRef::StringUtf8,
2147 mutable: false,
2148 doc: None,
2149 }],
2150 returns: Some(TypeRef::StringUtf8),
2151 doc: None,
2152 r#async: false,
2153 cancellable: false,
2154 deprecated: None,
2155 since: None,
2156 });
2157 m
2158 }]);
2159
2160 let config = NodeConfig {
2161 prefix: Some("myffi".into()),
2162 ..NodeConfig::default()
2163 };
2164
2165 let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_prefix");
2166 let _ = std::fs::remove_dir_all(&tmp);
2167 std::fs::create_dir_all(&tmp).unwrap();
2168 let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2169
2170 NodeGenerator.generate(&api, out_dir, &config).unwrap();
2171
2172 let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
2175
2176 assert!(
2178 addon.contains("myffi_greet_hello"),
2179 "addon should call the prefixed user symbol myffi_greet_hello: {addon}"
2180 );
2181 assert!(
2182 !addon.contains("weaveffi_greet_hello"),
2183 "addon must not emit the hard-coded weaveffi_ user symbol: {addon}"
2184 );
2185 assert!(
2186 addon.contains("#include \"myffi.h\""),
2187 "addon should include the prefixed header myffi.h: {addon}"
2188 );
2189
2190 assert!(
2192 addon.contains("weaveffi_error"),
2193 "runtime weaveffi_error must remain literal: {addon}"
2194 );
2195 assert!(
2196 addon.contains("weaveffi_free_string"),
2197 "runtime weaveffi_free_string must remain literal: {addon}"
2198 );
2199
2200 let _ = std::fs::remove_dir_all(&tmp);
2201 }
2202
2203 #[test]
2204 fn node_addon_checks_error() {
2205 let api = make_api(vec![{
2206 let mut m = make_module("math");
2207 m.functions.push(Function {
2208 name: "add".into(),
2209 params: vec![
2210 Param {
2211 name: "a".into(),
2212 ty: TypeRef::I32,
2213 mutable: false,
2214 doc: None,
2215 },
2216 Param {
2217 name: "b".into(),
2218 ty: TypeRef::I32,
2219 mutable: false,
2220 doc: None,
2221 },
2222 ],
2223 returns: Some(TypeRef::I32),
2224 doc: None,
2225 r#async: false,
2226 cancellable: false,
2227 deprecated: None,
2228 since: None,
2229 });
2230 m
2231 }]);
2232 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2233 assert!(
2234 addon.contains("err.code"),
2235 "generated addon.c should check err.code: {addon}"
2236 );
2237 }
2238
2239 #[test]
2240 fn node_strip_module_prefix() {
2241 let api = make_api(vec![{
2242 let mut m = make_module("contacts");
2243 m.functions.push(Function {
2244 name: "create_contact".into(),
2245 params: vec![Param {
2246 name: "name".into(),
2247 ty: TypeRef::StringUtf8,
2248 mutable: false,
2249 doc: None,
2250 }],
2251 returns: Some(TypeRef::I32),
2252 doc: None,
2253 r#async: false,
2254 cancellable: false,
2255 deprecated: None,
2256 since: None,
2257 });
2258 m
2259 }]);
2260
2261 let config = NodeConfig {
2262 strip_module_prefix: true,
2263 ..NodeConfig::default()
2264 };
2265
2266 let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix");
2267 let _ = std::fs::remove_dir_all(&tmp);
2268 std::fs::create_dir_all(&tmp).unwrap();
2269 let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2270
2271 NodeGenerator.generate(&api, out_dir, &config).unwrap();
2272
2273 let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap();
2274 assert!(
2275 dts.contains("export function create_contact("),
2276 "stripped name should be create_contact: {dts}"
2277 );
2278 assert!(
2279 !dts.contains("export function contacts_create_contact("),
2280 "should not contain module-prefixed name: {dts}"
2281 );
2282
2283 let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
2284 assert!(
2285 addon.contains("\"create_contact\""),
2286 "JS export name should be stripped: {addon}"
2287 );
2288 assert!(
2289 addon.contains("weaveffi_contacts_create_contact"),
2290 "C ABI call should still use full name: {addon}"
2291 );
2292
2293 let no_strip = NodeConfig::default();
2294 let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix");
2295 let _ = std::fs::remove_dir_all(&tmp2);
2296 std::fs::create_dir_all(&tmp2).unwrap();
2297 let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
2298
2299 NodeGenerator.generate(&api, out_dir2, &no_strip).unwrap();
2300
2301 let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap();
2302 assert!(
2303 dts2.contains("export function contacts_create_contact("),
2304 "default should use module-prefixed name: {dts2}"
2305 );
2306
2307 let _ = std::fs::remove_dir_all(&tmp);
2308 let _ = std::fs::remove_dir_all(&tmp2);
2309 }
2310
2311 #[test]
2312 fn node_typed_handle_type() {
2313 let api = make_api(vec![{
2314 let mut m = make_module("contacts");
2315 m.structs.push(StructDef {
2316 name: "Contact".into(),
2317 doc: None,
2318 fields: vec![StructField {
2319 name: "name".into(),
2320 ty: TypeRef::StringUtf8,
2321 doc: None,
2322 default: None,
2323 }],
2324 builder: false,
2325 });
2326 m.functions.push(Function {
2327 name: "get_info".into(),
2328 params: vec![Param {
2329 name: "contact".into(),
2330 ty: TypeRef::TypedHandle("Contact".into()),
2331 mutable: false,
2332 doc: None,
2333 }],
2334 returns: None,
2335 doc: None,
2336 r#async: false,
2337 cancellable: false,
2338 deprecated: None,
2339 since: None,
2340 });
2341 m
2342 }]);
2343 let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2344 assert!(
2345 dts.contains("contact: Contact"),
2346 "TypedHandle should use class type not bigint: {dts}"
2347 );
2348 }
2349
2350 #[test]
2351 fn node_deeply_nested_optional() {
2352 let api = make_api(vec![Module {
2353 name: "edge".into(),
2354 functions: vec![Function {
2355 name: "process".into(),
2356 params: vec![Param {
2357 name: "data".into(),
2358 ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2359 Box::new(TypeRef::Struct("Contact".into())),
2360 ))))),
2361 mutable: false,
2362 doc: None,
2363 }],
2364 returns: None,
2365 doc: None,
2366 r#async: false,
2367 cancellable: false,
2368 deprecated: None,
2369 since: None,
2370 }],
2371 structs: vec![StructDef {
2372 name: "Contact".into(),
2373 doc: None,
2374 fields: vec![StructField {
2375 name: "name".into(),
2376 ty: TypeRef::StringUtf8,
2377 doc: None,
2378 default: None,
2379 }],
2380 builder: false,
2381 }],
2382 enums: vec![],
2383 callbacks: vec![],
2384 listeners: vec![],
2385 errors: None,
2386 modules: vec![],
2387 }]);
2388 let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2389 assert!(
2390 dts.contains("(Contact | null)[] | null"),
2391 "should contain deeply nested optional type: {dts}"
2392 );
2393 }
2394
2395 #[test]
2396 fn node_map_of_lists() {
2397 let api = make_api(vec![Module {
2398 name: "edge".into(),
2399 functions: vec![Function {
2400 name: "process".into(),
2401 params: vec![Param {
2402 name: "scores".into(),
2403 ty: TypeRef::Map(
2404 Box::new(TypeRef::StringUtf8),
2405 Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2406 ),
2407 mutable: false,
2408 doc: None,
2409 }],
2410 returns: None,
2411 doc: None,
2412 r#async: false,
2413 cancellable: false,
2414 deprecated: None,
2415 since: None,
2416 }],
2417 structs: vec![],
2418 enums: vec![],
2419 callbacks: vec![],
2420 listeners: vec![],
2421 errors: None,
2422 modules: vec![],
2423 }]);
2424 let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2425 assert!(
2426 dts.contains("Record<string, number[]>"),
2427 "should contain map of lists type: {dts}"
2428 );
2429 }
2430
2431 #[test]
2432 fn node_enum_keyed_map() {
2433 let api = make_api(vec![Module {
2434 name: "edge".into(),
2435 functions: vec![Function {
2436 name: "process".into(),
2437 params: vec![Param {
2438 name: "contacts".into(),
2439 ty: TypeRef::Map(
2440 Box::new(TypeRef::Enum("Color".into())),
2441 Box::new(TypeRef::Struct("Contact".into())),
2442 ),
2443 mutable: false,
2444 doc: None,
2445 }],
2446 returns: None,
2447 doc: None,
2448 r#async: false,
2449 cancellable: false,
2450 deprecated: None,
2451 since: None,
2452 }],
2453 structs: vec![StructDef {
2454 name: "Contact".into(),
2455 doc: None,
2456 fields: vec![StructField {
2457 name: "name".into(),
2458 ty: TypeRef::StringUtf8,
2459 doc: None,
2460 default: None,
2461 }],
2462 builder: false,
2463 }],
2464 enums: vec![EnumDef {
2465 name: "Color".into(),
2466 doc: None,
2467 variants: vec![
2468 EnumVariant {
2469 name: "Red".into(),
2470 value: 0,
2471 doc: None,
2472 },
2473 EnumVariant {
2474 name: "Green".into(),
2475 value: 1,
2476 doc: None,
2477 },
2478 EnumVariant {
2479 name: "Blue".into(),
2480 value: 2,
2481 doc: None,
2482 },
2483 ],
2484 }],
2485 callbacks: vec![],
2486 listeners: vec![],
2487 errors: None,
2488 modules: vec![],
2489 }]);
2490 let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2491 assert!(
2492 dts.contains("Record<Color, Contact>"),
2493 "should contain enum-keyed map type: {dts}"
2494 );
2495 }
2496
2497 #[test]
2498 fn node_no_double_free_on_error() {
2499 let api = make_api(vec![{
2500 let mut m = make_module("contacts");
2501 m.structs.push(StructDef {
2502 name: "Contact".into(),
2503 doc: None,
2504 fields: vec![StructField {
2505 name: "name".into(),
2506 ty: TypeRef::StringUtf8,
2507 doc: None,
2508 default: None,
2509 }],
2510 builder: false,
2511 });
2512 m.functions.push(Function {
2513 name: "find_contact".into(),
2514 params: vec![Param {
2515 name: "name".into(),
2516 ty: TypeRef::StringUtf8,
2517 mutable: false,
2518 doc: None,
2519 }],
2520 returns: Some(TypeRef::Struct("Contact".into())),
2521 doc: None,
2522 r#async: false,
2523 cancellable: false,
2524 deprecated: None,
2525 since: None,
2526 });
2527 m
2528 }]);
2529 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2530 assert!(
2531 addon.contains("free(name)"),
2532 "malloc'd JS string copy should be freed after the C call: {addon}"
2533 );
2534 assert!(
2535 !addon.contains("weaveffi_free_string(name)"),
2536 "input string param must not use weaveffi_free_string: {addon}"
2537 );
2538 let free_pos = addon
2539 .find("free(name)")
2540 .expect("free(name) should be present");
2541 let err_pos = addon
2542 .find("if (err.code != 0)")
2543 .expect("err.code check should be present");
2544 assert!(
2545 free_pos < err_pos,
2546 "cleanup should run before error check: free at {free_pos}, err at {err_pos}"
2547 );
2548 let err_block_start = addon
2549 .find(" if (err.code != 0) {\n")
2550 .expect("error if block should be present");
2551 let after_err = &addon[err_block_start..];
2552 let err_block_end_rel = after_err
2553 .find(" }\n napi_value ret;")
2554 .expect("napi_value ret should follow error block");
2555 let err_block = &addon[err_block_start..err_block_start + err_block_end_rel];
2556 assert!(
2557 !err_block.contains("result"),
2558 "error path should not touch result before return NULL: {err_block}"
2559 );
2560 }
2561
2562 #[test]
2563 fn node_null_check_on_optional_return() {
2564 let api = make_api(vec![{
2565 let mut m = make_module("contacts");
2566 m.structs.push(StructDef {
2567 name: "Contact".into(),
2568 doc: None,
2569 fields: vec![StructField {
2570 name: "name".into(),
2571 ty: TypeRef::StringUtf8,
2572 doc: None,
2573 default: None,
2574 }],
2575 builder: false,
2576 });
2577 m.functions.push(Function {
2578 name: "find_contact".into(),
2579 params: vec![Param {
2580 name: "id".into(),
2581 ty: TypeRef::I32,
2582 mutable: false,
2583 doc: None,
2584 }],
2585 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2586 "Contact".into(),
2587 )))),
2588 doc: None,
2589 r#async: false,
2590 cancellable: false,
2591 deprecated: None,
2592 since: None,
2593 });
2594 m
2595 }]);
2596 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2597 assert!(
2598 addon.contains("if (result == NULL)"),
2599 "optional struct return should null-check before wrapping: {addon}"
2600 );
2601 assert!(
2602 addon.contains("napi_get_null"),
2603 "optional absent should return JS null via napi_get_null: {addon}"
2604 );
2605 }
2606
2607 #[test]
2608 fn node_async_returns_promise() {
2609 let api = make_api(vec![{
2610 let mut m = make_module("tasks");
2611 m.functions.push(Function {
2612 name: "run".into(),
2613 params: vec![Param {
2614 name: "id".into(),
2615 ty: TypeRef::I32,
2616 mutable: false,
2617 doc: None,
2618 }],
2619 returns: Some(TypeRef::StringUtf8),
2620 doc: None,
2621 r#async: true,
2622 cancellable: false,
2623 deprecated: None,
2624 since: None,
2625 });
2626 m.functions.push(Function {
2627 name: "fire_and_forget".into(),
2628 params: vec![],
2629 returns: None,
2630 doc: None,
2631 r#async: true,
2632 cancellable: false,
2633 deprecated: None,
2634 since: None,
2635 });
2636 m
2637 }]);
2638 let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2639 assert!(
2640 dts.contains("Promise<"),
2641 "async function should return Promise in .d.ts: {dts}"
2642 );
2643 assert!(
2644 dts.contains("): Promise<string>"),
2645 "async string return should be Promise<string>: {dts}"
2646 );
2647 assert!(
2648 dts.contains("): Promise<void>"),
2649 "async void return should be Promise<void>: {dts}"
2650 );
2651 }
2652
2653 #[test]
2654 fn node_addon_creates_promise() {
2655 let api = make_api(vec![{
2656 let mut m = make_module("tasks");
2657 m.functions.push(Function {
2658 name: "run".into(),
2659 params: vec![Param {
2660 name: "id".into(),
2661 ty: TypeRef::I32,
2662 mutable: false,
2663 doc: None,
2664 }],
2665 returns: Some(TypeRef::I32),
2666 doc: None,
2667 r#async: true,
2668 cancellable: false,
2669 deprecated: None,
2670 since: None,
2671 });
2672 m
2673 }]);
2674 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2675 assert!(
2676 addon.contains("napi_create_promise"),
2677 "async addon should call napi_create_promise: {addon}"
2678 );
2679 assert!(
2680 addon.contains("napi_resolve_deferred"),
2681 "async callback should call napi_resolve_deferred: {addon}"
2682 );
2683 assert!(
2684 addon.contains("napi_reject_deferred"),
2685 "async callback should call napi_reject_deferred: {addon}"
2686 );
2687 assert!(
2688 addon.contains("weaveffi_napi_async_ctx"),
2689 "async addon should define async context struct: {addon}"
2690 );
2691 assert!(
2692 addon.contains("weaveffi_tasks_run_async("),
2693 "async addon should call the _async C function: {addon}"
2694 );
2695 assert!(
2696 addon.contains("weaveffi_tasks_run_napi_cb"),
2697 "async addon should define the callback: {addon}"
2698 );
2699 }
2700
2701 #[test]
2707 fn node_async_pins_callback_for_lifetime() {
2708 let api = make_api(vec![{
2709 let mut m = make_module("tasks");
2710 m.functions.push(Function {
2711 name: "run".into(),
2712 params: vec![Param {
2713 name: "id".into(),
2714 ty: TypeRef::I32,
2715 mutable: false,
2716 doc: None,
2717 }],
2718 returns: Some(TypeRef::I32),
2719 doc: None,
2720 r#async: true,
2721 cancellable: false,
2722 deprecated: None,
2723 since: None,
2724 });
2725 m
2726 }]);
2727 let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2728 let create_count = addon.matches("napi_create_promise").count();
2729 let resolve_count = addon.matches("napi_resolve_deferred").count();
2730 let reject_count = addon.matches("napi_reject_deferred").count();
2731 let malloc_count = addon
2732 .matches("malloc(sizeof(weaveffi_napi_async_ctx))")
2733 .count();
2734 let free_count = addon.matches("free(ctx);").count();
2735 assert_eq!(
2736 create_count, 1,
2737 "expected one napi_create_promise per async fn, got {create_count}: {addon}"
2738 );
2739 assert_eq!(
2740 resolve_count, 1,
2741 "expected one napi_resolve_deferred per async fn, got {resolve_count}: {addon}"
2742 );
2743 assert_eq!(
2744 reject_count, 1,
2745 "expected one napi_reject_deferred per async fn, got {reject_count}: {addon}"
2746 );
2747 assert_eq!(
2748 malloc_count, free_count,
2749 "ctx malloc / free must balance per async fn: malloc={malloc_count} free={free_count}: {addon}"
2750 );
2751 }
2752
2753 fn doc_module() -> Module {
2754 Module {
2755 name: "docs".into(),
2756 functions: vec![Function {
2757 name: "do_thing".into(),
2758 params: vec![Param {
2759 name: "x".into(),
2760 ty: TypeRef::I32,
2761 mutable: false,
2762 doc: Some("the input value".into()),
2763 }],
2764 returns: Some(TypeRef::I32),
2765 doc: Some("Performs a thing.".into()),
2766 r#async: false,
2767 cancellable: false,
2768 deprecated: None,
2769 since: None,
2770 }],
2771 structs: vec![StructDef {
2772 name: "Item".into(),
2773 doc: Some("An item we track.".into()),
2774 fields: vec![StructField {
2775 name: "id".into(),
2776 ty: TypeRef::I64,
2777 doc: Some("Stable id".into()),
2778 default: None,
2779 }],
2780 builder: false,
2781 }],
2782 enums: vec![EnumDef {
2783 name: "Kind".into(),
2784 doc: Some("Kind of item.".into()),
2785 variants: vec![EnumVariant {
2786 name: "Small".into(),
2787 value: 0,
2788 doc: Some("A small one".into()),
2789 }],
2790 }],
2791 callbacks: vec![],
2792 listeners: vec![],
2793 errors: None,
2794 modules: vec![],
2795 }
2796 }
2797
2798 #[test]
2799 fn node_emits_doc_on_function() {
2800 let dts = render_node_dts(
2801 &make_api(vec![doc_module()]),
2802 "weaveffi",
2803 true,
2804 "weaveffi.yml",
2805 );
2806 assert!(dts.contains("Performs a thing."), "{dts}");
2807 }
2808
2809 #[test]
2810 fn node_emits_doc_on_struct() {
2811 let dts = render_node_dts(
2812 &make_api(vec![doc_module()]),
2813 "weaveffi",
2814 true,
2815 "weaveffi.yml",
2816 );
2817 assert!(dts.contains("/** An item we track. */"), "{dts}");
2818 }
2819
2820 #[test]
2821 fn node_emits_doc_on_enum_variant() {
2822 let dts = render_node_dts(
2823 &make_api(vec![doc_module()]),
2824 "weaveffi",
2825 true,
2826 "weaveffi.yml",
2827 );
2828 assert!(dts.contains("/** Kind of item. */"), "{dts}");
2829 assert!(dts.contains("/** A small one */"), "{dts}");
2830 }
2831
2832 #[test]
2833 fn node_emits_doc_on_field() {
2834 let dts = render_node_dts(
2835 &make_api(vec![doc_module()]),
2836 "weaveffi",
2837 true,
2838 "weaveffi.yml",
2839 );
2840 assert!(dts.contains("/** Stable id */"), "{dts}");
2841 }
2842
2843 #[test]
2844 fn node_emits_doc_on_param() {
2845 let dts = render_node_dts(
2846 &make_api(vec![doc_module()]),
2847 "weaveffi",
2848 true,
2849 "weaveffi.yml",
2850 );
2851 assert!(dts.contains("@param x the input value"), "{dts}");
2852 }
2853}