#![cfg(feature = "typescript")]
pub mod common;
use common::{Choice, Test};
use facet_generate::{
Registry,
generation::{CodeGeneratorConfig, bincode::BincodePlugin, typescript},
};
use std::{fs::File, io::Write, path::PathBuf, process::Command, sync::Arc};
use tempfile::{TempDir, tempdir};
struct TsProject {
dir: TempDir,
source_path: PathBuf,
source: File,
}
impl TsProject {
fn new(registry: &Registry) -> Self {
let dir = tempdir().unwrap();
let mut installer = typescript::Installer::new("main", dir.path());
installer.install_serde_runtime().unwrap();
installer.install_bincode_runtime().unwrap();
let source_path = dir.path().join("test.ts");
let mut source = File::create(&source_path).unwrap();
writeln!(
source,
r#"import {{ assertEquals, assertThrows }} from "https://deno.land/std@0.110.0/testing/asserts.ts";
import {{ BincodeDeserializer, BincodeSerializer }} from "./bincode/index.ts";
"#
)
.unwrap();
let config = CodeGeneratorConfig::new("main".to_string());
let generator = typescript::TypeScriptCodeGenerator::new(&config)
.with_plugins(vec![Arc::new(BincodePlugin)]);
generator.output(&mut source, registry).unwrap();
Self {
dir,
source_path,
source,
}
}
fn write_test(&mut self, body: &str) {
writeln!(self.source, "{body}").unwrap();
}
fn run(self) {
drop(self.source);
let status = Command::new("deno")
.current_dir(self.dir.path())
.arg("test")
.arg("--sloppy-imports")
.arg(&self.source_path)
.status()
.unwrap();
assert!(status.success());
}
}
macro_rules! wire_cases {
($($value:expr),* $(,)?) => {
vec![$((($value).to_string(), bincode::serialize(&$value).unwrap())),*]
};
}
fn scalar_roundtrip_test(test_name: &str, method: &str, cases: &[(String, Vec<u8>)]) -> String {
let rows = cases
.iter()
.map(|(value, bytes)| {
let bytes = bytes
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(", ");
format!(" {{ value: BigInt(\"{value}\"), bytes: new Uint8Array([{bytes}]) }},")
})
.collect::<Vec<_>>()
.join("\n");
format!(
r#"
Deno.test("{test_name}", () => {{
const cases = [
{rows}
];
for (const {{ value, bytes }} of cases) {{
const deserializer = new BincodeDeserializer(bytes);
assertEquals(
deserializer.deserialize{method}(),
value,
`deserialize{method}(${{value}})`,
);
const serializer = new BincodeSerializer();
serializer.serialize{method}(value);
assertEquals(
serializer.getBytes(),
bytes,
`serialize{method}(${{value}})`,
);
}}
}});"#
)
}
#[test]
fn test_typescript_runtime_bincode_uuid_roundtrip() {
let mut project = TsProject::new(&common::get_uuid_registry());
let reference = common::get_uuid_reference_bytes();
project.write_test(&format!(
r#"
Deno.test("UUID bincode roundtrip", () => {{
const expectedBytes = new Uint8Array([{bytes}]);
const deserializer = new BincodeDeserializer(expectedBytes);
const value: UuidData = UuidData.deserialize(deserializer);
assertEquals(value.id, "{id}" as Uuid, "id should match");
assertEquals(value.parent_id, "{parent_id}" as Uuid, "parent_id should match");
const serializer = new BincodeSerializer();
value.serialize(serializer);
const output = serializer.getBytes();
assertEquals(output, expectedBytes, "roundtrip bytes should match");
}});
"#,
bytes = reference
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(", "),
id = common::UUID_ID,
parent_id = common::UUID_PARENT_ID,
));
project.run();
}
#[test]
fn test_typescript_runtime_bincode_serialization() {
let mut project = TsProject::new(&common::get_simple_registry());
let reference = bincode::serialize(&Test {
a: vec![4, 6],
b: (-3, 5),
c: Choice::C { x: 7 },
})
.unwrap();
project.write_test(&format!(
r#"
Deno.test("bincode serialization matches deserialization", () => {{
const expectedBytes = new Uint8Array([{bytes}]);
const deserializer = new BincodeDeserializer(expectedBytes);
const deserializedInstance: Test = Test.deserialize(deserializer);
const expectedChoice: Choice = choiceC(7);
const expectedInstance: Test = new Test(
[4, 6],
[BigInt(-3), BigInt(5)],
expectedChoice,
);
assertEquals(deserializedInstance, expectedInstance, "Object instances should match");
const serializer = new BincodeSerializer();
expectedInstance.serialize(serializer);
const serializedBytes = serializer.getBytes();
assertEquals(serializedBytes, expectedBytes, "bincode bytes should match");
}});
"#,
bytes = reference
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(", "),
));
project.run();
}
#[test]
fn test_typescript_runtime_i64_i128_low_limb_high_bit_roundtrip() {
const LARGE_I64: i64 = 1_785_688_513_662;
const LARGE_I128: i128 = (1 << 64) | 0xF8C4_E09E_F8C4_E09E_u64 as i128;
const NEGATIVE_I128: i128 = i128::MIN + 5;
let mut project = TsProject::new(&common::get_simple_registry());
let reference = bincode::serialize(&Test {
a: vec![1],
b: (LARGE_I64, 9),
c: Choice::A,
})
.unwrap();
project.write_test(&format!(
r#"
Deno.test("i64 with low-half bit 31 set round-trips", () => {{
const expectedBytes = new Uint8Array([{bytes}]);
const deserializer = new BincodeDeserializer(expectedBytes);
const value: Test = Test.deserialize(deserializer);
assertEquals(value.b[0], BigInt("{large}"), "i64 must keep high limb");
assertEquals(value.b[1], BigInt(9));
const serializer = new BincodeSerializer();
value.serialize(serializer);
assertEquals(serializer.getBytes(), expectedBytes, "bytes must round-trip");
}});
"#,
bytes = reference
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(", "),
large = LARGE_I64,
));
let i128_reference = bincode::serialize(&(LARGE_I128, NEGATIVE_I128)).unwrap();
project.write_test(&format!(
r#"
Deno.test("i128 with low-limb bit 63 set round-trips", () => {{
const expectedBytes = new Uint8Array([{bytes}]);
const deserializer = new BincodeDeserializer(expectedBytes);
const large = deserializer.deserializeI128();
const negative = deserializer.deserializeI128();
assertEquals(large, BigInt("{large}"), "i128 must keep high limb");
assertEquals(negative, BigInt("{negative}"), "negative i128 must round-trip");
const serializer = new BincodeSerializer();
serializer.serializeI128(large);
serializer.serializeI128(negative);
assertEquals(serializer.getBytes(), expectedBytes, "bytes must round-trip");
}});
"#,
bytes = i128_reference
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(", "),
large = LARGE_I128,
negative = NEGATIVE_I128,
));
project.run();
}
#[test]
fn test_typescript_runtime_integer_edge_cases_roundtrip() {
let mut project = TsProject::new(&common::get_simple_registry());
let i64_cases = wire_cases![
0_i64,
1_i64,
-1_i64,
i64::MIN,
i64::MAX,
1_i64 << 31,
-(1_i64 << 31),
1_785_688_513_662_i64,
-1_785_688_513_662_i64,
];
project.write_test(&scalar_roundtrip_test(
"i64 edge cases round-trip",
"I64",
&i64_cases,
));
let u64_cases = wire_cases![
0_u64,
1_u64,
u64::MAX,
1_u64 << 31,
(1_u64 << 32) - 1,
1_u64 << 32,
0xFFFF_FFFF_8000_0000_u64,
];
project.write_test(&scalar_roundtrip_test(
"u64 edge cases round-trip",
"U64",
&u64_cases,
));
let i128_cases = wire_cases![
0_i128,
1_i128,
-1_i128,
i128::MIN,
i128::MAX,
i128::MIN + 5,
1_i128 << 63,
-(1_i128 << 64),
];
project.write_test(&scalar_roundtrip_test(
"i128 edge cases round-trip",
"I128",
&i128_cases,
));
let u128_cases = wire_cases![
0_u128,
1_u128,
u128::MAX,
1_u128 << 63,
1_u128 << 64,
u128::from(u64::MAX),
(1_u128 << 64) | 0xF8C4_E09E_F8C4_E09E_u128,
];
project.write_test(&scalar_roundtrip_test(
"u128 edge cases round-trip",
"U128",
&u128_cases,
));
project.run();
}
#[test]
fn test_typescript_runtime_truncated_input_throws() {
let mut project = TsProject::new(&common::get_simple_registry());
project.write_test(
r#"
Deno.test("truncated input throws instead of yielding a short value", () => {
// an i64 needs eight bytes; only three are available
assertThrows(
() => new BincodeDeserializer(new Uint8Array([1, 2, 3])).deserializeI64(),
Error,
"Unexpected end of input",
);
// a length prefix of five, with only two bytes of payload behind it
const truncatedStr = new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 0x68, 0x69]);
assertThrows(
() => new BincodeDeserializer(truncatedStr).deserializeStr(),
Error,
"Unexpected end of input",
);
// nothing at all to read
assertThrows(
() => new BincodeDeserializer(new Uint8Array([])).deserializeBool(),
Error,
"Unexpected end of input",
);
// reading exactly to the end must still succeed
const exact = new BincodeDeserializer(
new Uint8Array([9, 0, 0, 0, 0, 0, 0, 0]),
);
assertEquals(exact.deserializeI64(), BigInt(9));
});
"#,
);
project.run();
}