use super::*;
use crate::core::ir::{ParamDef, ReceiverKind};
use std::collections::HashMap;
fn mapper() -> WasmMapper {
WasmMapper::new(HashMap::new(), "Wasm".to_string())
}
fn param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}
}
fn default_method(return_type_name: &str) -> MethodDef {
MethodDef {
name: "default".to_string(),
is_static: true,
return_type: TypeRef::Named(return_type_name.to_string()),
receiver: None,
..Default::default()
}
}
#[test]
fn gen_method_static_default_uses_full_rust_path_for_nested_type() {
let typ = TypeDef {
name: "RenderOptions".to_string(),
rust_path: "sample_core::config::render::RenderOptions".to_string(),
..Default::default()
};
let method = default_method("RenderOptions");
let out = gen_method(
&method,
&mapper(),
"RenderOptions",
"sample_core",
&AHashSet::default(),
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains("sample_core::config::render::RenderOptions::default()"),
"static default() must call the type's real module path: {out}"
);
assert!(
!out.contains("sample_core::RenderOptions::default()"),
"static default() must not assume the type is re-exported at the crate root: {out}"
);
}
#[test]
fn gen_method_static_default_uses_bare_path_for_root_type() {
let typ = TypeDef {
name: "PlainOptions".to_string(),
rust_path: "PlainOptions".to_string(),
..Default::default()
};
let method = default_method("PlainOptions");
let out = gen_method(
&method,
&mapper(),
"PlainOptions",
"sample_core",
&AHashSet::default(),
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains("sample_core::PlainOptions::default()"),
"a crate-root type must still resolve to `{{core_import}}::{{name}}`: {out}"
);
}
#[test]
fn gen_method_instance_delegate_uses_full_rust_path_for_nested_type() {
let typ = TypeDef {
name: "RenderOptions".to_string(),
rust_path: "sample_core::config::render::RenderOptions".to_string(),
..Default::default()
};
let method = MethodDef {
name: "is_sane".to_string(),
is_static: false,
return_type: TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool),
receiver: Some(ReceiverKind::Ref),
..Default::default()
};
let out = gen_method(
&method,
&mapper(),
"RenderOptions",
"sample_core",
&AHashSet::default(),
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains("sample_core::config::render::RenderOptions::from(self.clone())"),
"instance delegation must call the type's real module path: {out}"
);
assert!(
!out.contains("sample_core::RenderOptions::from(self.clone())"),
"instance delegation must not assume the type is re-exported at the crate root: {out}"
);
}
#[test]
fn gen_method_rewrites_the_source_crate_prefix_when_a_remap_is_configured() {
let typ = TypeDef {
name: "CorsConfig".to_string(),
rust_path: "sample_core::CorsConfig".to_string(),
..Default::default()
};
let method = MethodDef {
name: "is_sane".to_string(),
is_static: false,
return_type: TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool),
receiver: Some(ReceiverKind::Ref),
..Default::default()
};
let out = gen_method(
&method,
&mapper(),
"CorsConfig",
"sample_http",
&AHashSet::default(),
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[("sample_core", "sample_http")],
);
assert!(
out.contains("sample_http::CorsConfig::from(self.clone())"),
"the delegation must name the override crate the binding actually depends on: {out}"
);
assert!(
!out.contains("sample_core::"),
"no reference to the un-depended-on source crate may survive: {out}"
);
}
#[test]
fn lifetime_static_method_deserializes_map_from_jsvalue_once_and_keeps_required_bool() {
let typ = TypeDef {
name: "NodeContext".to_string(),
rust_path: "sample_fixture::NodeContext".to_string(),
has_lifetime_params: true,
..Default::default()
};
let mut parent_tag = param("parent_tag", TypeRef::String);
parent_tag.optional = true;
let mut attributes = param(
"attributes",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
);
attributes.map_is_btree = true;
let method = MethodDef {
name: "with_owned_attributes".to_string(),
is_static: true,
params: vec![
param("node_type", TypeRef::Named("NodeType".to_string())),
param("tag_name", TypeRef::String),
attributes,
param("depth", TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize)),
param(
"index_in_parent",
TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize),
),
parent_tag,
param("is_inline", TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool)),
],
return_type: TypeRef::Named("NodeContext".to_string()),
..Default::default()
};
let out = gen_method(
&method,
&mapper(),
"NodeContext",
"sample_fixture",
&AHashSet::default(),
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains("attributes: JsValue"),
"map must enter through JsValue:\n{out}"
);
assert!(
out.contains("serde_wasm_bindgen::from_value(attributes.clone())"),
"the lifetime map conversion must read the incoming JsValue:\n{out}"
);
assert!(
!out.contains("let attributes: std::collections::HashMap"),
"a generic container conversion must not shadow the incoming JsValue:\n{out}"
);
let normalized = out.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
normalized.contains("parent_tag_converted, is_inline)"),
"WASM keeps required parameters required even when they follow an optional one:\n{out}"
);
assert!(
!out.contains("is_inline.expect"),
"a required bool is not an Option and cannot be unwrapped:\n{out}"
);
}
#[test]
fn lifetime_static_method_preserves_borrowed_map_newtype_binding() {
let typ = TypeDef {
name: "NodeContext".to_string(),
rust_path: "sample_fixture::NodeContext".to_string(),
has_lifetime_params: true,
..Default::default()
};
let mut attributes = param(
"attributes",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
);
attributes.is_ref = true;
attributes.newtype_wrapper = Some(crate::core::ir::NewtypeWrapper::encode_explicit(&[
crate::core::ir::NewtypeWrapperMetadata::transparent_string(
"sample_fixture::AttributeValue",
"from",
"into_inner",
vec![crate::core::ir::NewtypeContainer::MapValue],
),
]));
let method = MethodDef {
name: "with_attributes".to_string(),
is_static: true,
params: vec![attributes],
return_type: TypeRef::Named("NodeContext".to_string()),
..Default::default()
};
let out = gen_method(
&method,
&mapper(),
"NodeContext",
"sample_fixture",
&AHashSet::default(),
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains(
"let attributes: std::collections::HashMap<String, String> = serde_wasm_bindgen::from_value(attributes)"
),
"a wrapped map must still deserialize before rebuilding its values:\n{out}"
);
assert!(
out.contains("let attributes_newtype ="),
"a borrowed wrapper requires a stable local for the core reference:\n{out}"
);
assert!(
out.contains("sample_fixture::NodeContext::with_attributes(&attributes_newtype)"),
"the core call must borrow the rebuilt wrapper local:\n{out}"
);
assert!(
!out.contains("attributes_converted"),
"the lifetime map conversion is superseded by explicit newtype reconstruction:\n{out}"
);
}
#[test]
fn sync_method_opaque_handle_param_is_taken_by_reference() {
let typ = TypeDef {
name: "Session".to_string(),
rust_path: "sample_fixture::Session".to_string(),
..Default::default()
};
let method = MethodDef {
name: "attach".to_string(),
is_static: true,
params: vec![param("engine", TypeRef::Named("CrawlEngineHandle".to_string()))],
return_type: TypeRef::Unit,
..Default::default()
};
let opaque: AHashSet<String> = ["CrawlEngineHandle".to_string()].into_iter().collect();
let out = gen_method(
&method,
&mapper(),
"Session",
"sample_fixture",
&opaque,
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains("engine: &WasmCrawlEngineHandle"),
"opaque handle method param must be by reference so repeated calls do not hit a null pointer:\n{out}"
);
assert!(
!out.contains("engine: WasmCrawlEngineHandle"),
"by-value opaque handle method param leaks into the signature:\n{out}"
);
}
#[test]
fn async_method_opaque_handle_param_is_taken_by_reference() {
let typ = TypeDef {
name: "Session".to_string(),
rust_path: "sample_fixture::Session".to_string(),
..Default::default()
};
let method = MethodDef {
name: "run".to_string(),
is_async: true,
receiver: Some(ReceiverKind::Ref),
params: vec![
param("options", TypeRef::Named("RunOptions".to_string())),
param("engine", TypeRef::Named("CrawlEngineHandle".to_string())),
],
return_type: TypeRef::Unit,
error_type: Some("CrawlError".to_string()),
..Default::default()
};
let opaque: AHashSet<String> = ["CrawlEngineHandle".to_string()].into_iter().collect();
let out = gen_method(
&method,
&mapper(),
"Session",
"sample_fixture",
&opaque,
"Wasm",
&typ,
&AHashSet::default(),
&ahash::AHashMap::default(),
&[],
);
assert!(
out.contains("engine: &WasmCrawlEngineHandle"),
"opaque handle param must be by reference in the async named-reshape fall-through arm:\n{out}"
);
assert!(
!out.contains("engine: WasmCrawlEngineHandle"),
"by-value opaque handle param leaks into the async signature:\n{out}"
);
}
#[test]
fn nonopaque_static_async_fallible_method_has_no_self_and_awaits_core_call() {
let method = MethodDef {
name: "create_async".to_string(),
is_static: true,
is_async: true,
error_type: Some("Error".to_string()),
return_type: TypeRef::String,
..Default::default()
};
let typ = TypeDef {
name: "Factory".to_string(),
rust_path: "sample_fixture::nested::Factory".to_string(),
methods: vec![method.clone()],
..Default::default()
};
let out = gen_method(
&method,
&mapper(),
"Factory",
"sample_fixture",
&AHashSet::new(),
"Wasm",
&typ,
&AHashSet::new(),
&ahash::AHashMap::new(),
&[],
);
assert!(out.contains("pub async fn create_async()"), "{out}");
assert!(
out.contains("sample_fixture::nested::Factory::create_async().await"),
"{out}"
);
assert!(
!out.contains("self.clone()"),
"static async dispatch must not use self: {out}"
);
}