1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
//! NAPI trait-bridge native-array callback args + type-conditional return decoding (#1304).
//!
//! Neutral fixture: a plugin trait `Embedder` with three async methods, all taking a
//! `Vec<String>` param:
//! - `embed` returns `Vec<Vec<f32>>` — arg is native-encodable; `f32` has no
//! `FromNapiValue` impl in napi-rs, so the return decodes via the f64-analog bridge
//! (`Vec<Vec<f64>>` via `FromNapiValue`, then an element-wise `as f32` cast) instead of
//! a JSON round-trip.
//! - `tag` returns `Vec<String>` — both arg and return are napi-native end to end, no
//! bridging needed.
//! - `describe` returns the known serde struct `Doc` — decodes via `Doc`'s own `JsDoc`
//! native DTO (`NapiBridgeGenerator::plan_return_decode`'s `Named` case), not a JSON
//! round-trip; see `named_struct_return_type_decodes_via_native_js_dto_not_json_fallback`
//! for why this is NOT the same claim the test's original name made.
//!
//! Asserts (post-xberg#1636: argument marshalling and return decoding now happen inside each
//! method's `ThreadsafeFunction`, built once in `new()` — see `tsfn_init_source`/the TSFN type
//! alias assertions below — not inline in the trait-impl method body):
//! (a) every method's `texts: Vec<String>` argument is passed as a native JS array via
//! `ToNapiValue`, never `format!("{:?}", texts)`.
//! (b) `tag`'s return decodes via the TSFN's `AlefJsReply<Vec<String>>` Return type, no
//! JSON round-trip.
//! (c) `embed`'s return decodes via the f64 analog + element-wise `as f32` cast, matching
//! its `Vec<Vec<_>>` nesting depth, and does NOT fall back to a JSON round-trip.
//! (d) `describe`'s return (`Doc` struct) decodes via its native `JsDoc` DTO.
use alef::core::config::{BridgeBinding, TraitBridgeConfig};
use alef::core::ir::*;
fn make_field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..Default::default()
}
}
fn make_param(name: &str, ty: TypeRef, is_ref: bool) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
is_ref,
..Default::default()
}
}
fn async_method(name: &str, return_type: TypeRef) -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![make_param("texts", TypeRef::Vec(Box::new(TypeRef::String)), false)],
return_type,
receiver: Some(ReceiverKind::Ref),
cfg: None,
error_type: Some("Error".to_string()),
is_async: true,
..Default::default()
}
}
/// `embed(texts) -> Vec<Vec<f32>>`, `tag(texts) -> Vec<String>`,
/// `describe(texts) -> Doc` (known serde struct).
fn embedder_trait() -> TypeDef {
TypeDef {
name: "Embedder".to_string(),
rust_path: "test_lib::Embedder".to_string(),
is_trait: true,
is_opaque: true,
methods: vec![
async_method(
"embed",
TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32))))),
),
async_method("tag", TypeRef::Vec(Box::new(TypeRef::String))),
async_method("describe", TypeRef::Named("Doc".to_string())),
],
..Default::default()
}
}
fn embedder_api() -> ApiSurface {
let mut doc = TypeDef {
name: "Doc".to_string(),
rust_path: "test_lib::Doc".to_string(),
has_serde: true,
fields: vec![make_field("text", TypeRef::String)],
..Default::default()
};
doc.is_return_type = true;
ApiSurface {
crate_name: "test_lib".to_string(),
version: "0.1.0".to_string(),
types: vec![embedder_trait(), doc],
..Default::default()
}
}
fn embedder_bridge_cfg() -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: "Embedder".to_string(),
register_fn: Some("register_embedder".to_string()),
registry_getter: Some("test_lib::registry::get".to_string()),
super_trait: Some("Plugin".to_string()),
bind_via: BridgeBinding::FunctionParam,
..Default::default()
}
}
/// Extracts the source for a single `async fn <name>(...)` method body from the full
/// bridge `impl` code, up to (but not including) the next `async fn` (or end of string).
fn method_source<'a>(code: &'a str, name: &str) -> &'a str {
let marker = format!("async fn {name}(");
let start = code
.find(&marker)
.unwrap_or_else(|| panic!("method `{name}` not found in:\n{code}"));
let rest = &code[start..];
match rest[marker.len()..].find("async fn ") {
Some(next) => &rest[..marker.len() + next],
None => rest,
}
}
fn gen_bridge_code() -> String {
let trait_def = embedder_trait();
let api = embedder_api();
alef::backends::napi::trait_bridge::gen_trait_bridge(
&trait_def,
&embedder_bridge_cfg(),
"test_lib",
"TestLibError",
"TestLibError::from({msg})",
&api,
)
.expect("gen_trait_bridge must succeed for Embedder")
.code
}
/// Extracts the source for one method's `ThreadsafeFunction` field initializer (built in
/// `new()`) -- where argument marshalling now lives (moved, with #1636's rewrite, out of the
/// trait-impl method body and into the `build_callback` closure `new()` builds eagerly on the
/// JS thread). Scoped to `"<name>_tsfn: "` up to the next such marker.
fn tsfn_init_source<'a>(code: &'a str, name: &str) -> &'a str {
let marker = format!("{name}_tsfn: ");
// `rfind`, not `find`: the SAME marker text also appears earlier as the struct's plain
// field DECLARATION (`embed_tsfn: JsEmbedderBridgeEmbedTsfn,`) before the constructor's
// actual initializer block (`embed_tsfn: { let __f = ...`) -- the initializer is always the
// later occurrence.
let start = code
.rfind(&marker)
.unwrap_or_else(|| panic!("`{name}_tsfn` field initializer not found in:\n{code}"));
let rest = &code[start..];
match rest[marker.len()..].find("_tsfn: ") {
Some(next) => &rest[..marker.len() + next],
None => rest,
}
}
#[test]
fn vec_string_param_is_passed_as_native_js_array_for_every_method() {
let code = gen_bridge_code();
for name in ["embed", "tag", "describe"] {
let m = tsfn_init_source(&code, name);
assert!(
m.contains("napi::bindgen_prelude::ToNapiValue::to_napi_value(ctx.env.raw(), texts.clone())"),
"`{name}`'s `texts: Vec<String>` arg must be passed as a native JS array via the \
threadsafe-function closure:\n{m}"
);
assert!(
!m.contains("format!(\"{:?}\", texts)"),
"`{name}`'s `texts` arg must NOT be Debug-string encoded:\n{m}"
);
}
}
#[test]
fn native_decodable_return_type_decodes_via_from_napi_value() {
let code = gen_bridge_code();
// The native decode now happens inside `AlefJsReply<T>::from_napi_value`/`settle()` (see
// `support::trait_bridge_runtime_def`), keyed purely by the TSFN's `Return` type parameter
// -- there is no per-method `FromNapiValue::from_napi_value(..)` call left in the generated
// code to assert on. What the generator DOES choose per return type is which `T` the TSFN
// decodes into and how the trait-impl method converts it, so assert on those instead.
assert!(
code.contains("type JsEmbedderBridgeTagTsfn = napi::threadsafe_function::ThreadsafeFunction<\n (Vec<String>,),\n AlefJsReply<Vec<String>>,"),
"`tag`'s `Vec<String>` return is fully napi-native: the TSFN must decode straight into \
`AlefJsReply<Vec<String>>`, not a JSON intermediate:\n{code}"
);
let tag = method_source(&code, "tag");
assert!(
tag.contains("let __decoded: Vec<String> ="),
"`tag`'s trait-impl method must bind the settled reply at the native `Vec<String>` \
type with no further conversion:\n{tag}"
);
assert!(
!tag.contains("coerce_to_string()") && !tag.contains("serde_json::from_str"),
"`tag`'s native return must NOT go through the JSON string fallback:\n{tag}"
);
}
#[test]
fn f32_leaved_return_type_decodes_via_f64_bridge_and_elementwise_cast() {
let code = gen_bridge_code();
assert!(
code.contains("type JsEmbedderBridgeEmbedTsfn = napi::threadsafe_function::ThreadsafeFunction<\n (Vec<String>,),\n AlefJsReply<Vec<Vec<f64>>>,"),
"`embed`'s `Vec<Vec<f32>>` return must decode natively via the f64 analog TSFN Return \
type (FromNapiValue has no impl for f32, but does for f64):\n{code}"
);
let embed = method_source(&code, "embed");
assert!(
embed.contains(": Vec<Vec<f64>>"),
"`embed`'s decode must be typed as the f64 analog `Vec<Vec<f64>>`, matching the \
`Vec<Vec<f32>>` return's nesting depth:\n{embed}"
);
assert!(
embed.contains("__decoded.into_iter().map(|v| v.into_iter().map(|v| v as f32).collect()).collect()"),
"`embed` must cast the decoded f64 values back to f32 element-wise at the correct \
Vec<Vec<_>> nesting depth:\n{embed}"
);
assert!(
!embed.contains("coerce_to_string()") && !embed.contains("serde_json::from_str"),
"`embed`'s return must NOT go through the JSON string fallback now that the f64 \
bridge makes it natively decodable:\n{embed}"
);
}
/// Regression guard for the ORIGINAL claim this test made under the old body-generation
/// design (return-type branching must not touch struct returns, which had no `Js{T}` native
/// decode path and always JSON round-tripped). Under the #1636 rewrite, `Doc` — a known,
/// serde-enabled, non-opaque struct — IS one of `NapiBridgeGenerator::plan_return_decode`'s
/// `Named` cases: the TSFN decodes straight into the binding's own `JsDoc` DTO (which has a
/// real `FromNapiValue` via `#[napi(object)]`) and converts through `From<JsDoc> for
/// test_lib::Doc`, no JSON round-trip at all. That's strictly better fidelity than the old
/// `coerce_to_string`/`serde_json::from_str` path this test used to assert on; the test name
/// (and this doc) stay as a marker of what changed and why, rather than silently flipping the
/// assertion with no explanation.
#[test]
fn named_struct_return_type_decodes_via_native_js_dto_not_json_fallback() {
let code = gen_bridge_code();
assert!(
code.contains("type JsEmbedderBridgeDescribeTsfn = napi::threadsafe_function::ThreadsafeFunction<\n (Vec<String>,),\n AlefJsReply<JsDoc>,"),
"`describe`'s `Doc` struct return is a known native-marshalled struct: the TSFN's \
Return must decode into `AlefJsReply<JsDoc>`, not a JSON intermediate:\n{code}"
);
// Bounded to the `describe` method's own body text (not `method_source`, which -- since
// `describe` is the LAST async method in this fixture -- runs to the end of the whole file
// when there's no following `async fn` boundary, sweeping in unrelated later methods like
// the Plugin super-trait's `version_on_js_thread` and its own legitimate
// `coerce_to_string()`/JSON fallback).
let describe_tail = &code[code.find("async fn describe(").expect("describe method present")..];
let describe = &describe_tail[..describe_tail
.find("\n }\n")
.map(|i| i + 6)
.unwrap_or(describe_tail.len())];
assert!(
describe.contains("let __result = test_lib::Doc::from(__decoded);"),
"`describe` must convert the decoded `JsDoc` into `Doc` via `From`, not \
`serde_json::from_value`:\n{describe}"
);
assert!(
!describe.contains("serde_json::from_value") && !describe.contains("coerce_to_string()"),
"`describe` must NOT go through a JSON round-trip now that `Doc` decodes natively via \
its `JsDoc` DTO:\n{describe}"
);
}