blob: 54fb38637ea18601e6f19df3d6fed2691c2461f3 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use chrono::{NaiveDate, NaiveDateTime};
use fory_core::buffer::{Reader, Writer};
use fory_core::error::Error;
use fory_core::meta::murmurhash3_x64_128;
use fory_core::resolver::context::{ReadContext, WriteContext};
use fory_core::serializer::{ForyDefault, Serializer};
use fory_core::TypeResolver;
use fory_core::{read_data, write_data, Fory};
use fory_derive::ForyObject;
use std::collections::{HashMap, HashSet};
use std::{fs, vec};
// RUSTFLAGS="-Awarnings" cargo expand -p tests --test test_cross_language
fn get_data_file() -> String {
std::env::var("DATA_FILE").expect("DATA_FILE not set")
}
#[derive(ForyObject, Debug, PartialEq, Default)]
struct Empty {}
#[derive(ForyObject, Debug, PartialEq, Default)]
enum Color {
#[default]
Green,
Red,
Blue,
White,
}
#[derive(ForyObject, Debug, PartialEq)]
struct Item {
// Use String (not Option<String>) to match Java's non-nullable String field
// xlang mode defaults to nullable=false for non-Optional types
name: String,
}
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct SimpleStruct {
// field_order != sorted_order
f1: HashMap<i32, f64>,
f2: i32,
f3: Item,
// Use String (not Option<String>) to match Java's non-nullable String field
f4: String,
f5: Color,
// Use Vec<String> to match Java's List<String> with non-nullable elements
f6: Vec<String>,
f7: i32,
f8: i32,
last: i32,
}
#[test]
#[ignore]
fn test_buffer() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut reader = Reader::new(bytes.as_slice());
assert_eq!(reader.read_u8().unwrap(), 1);
assert_eq!(reader.read_i8().unwrap(), i8::MAX);
assert_eq!(reader.read_i16().unwrap(), i16::MAX);
assert_eq!(reader.read_i32().unwrap(), i32::MAX);
assert_eq!(reader.read_i64().unwrap(), i64::MAX);
assert_eq!(reader.read_f32().unwrap(), -1.1f32);
assert_eq!(reader.read_f64().unwrap(), -1.1f64);
assert_eq!(reader.read_varuint32().unwrap(), 100);
let bytes_size = reader.read_i32().unwrap() as usize;
let binary = b"ab";
assert_eq!(reader.read_bytes(bytes_size).unwrap(), binary);
let mut buffer = vec![];
let mut writer = Writer::from_buffer(&mut buffer);
writer.write_u8(1);
writer.write_i8(i8::MAX);
writer.write_i16(i16::MAX);
writer.write_i32(i32::MAX);
writer.write_i64(i64::MAX);
writer.write_f32(-1.1);
writer.write_f64(-1.1);
writer.write_varuint32(100);
writer.write_i32(binary.len() as i32);
writer.write_bytes(binary);
fs::write(&data_file_path, writer.dump()).unwrap();
}
#[test]
#[ignore]
fn test_buffer_var() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let varint32_values = vec![
i32::MIN,
i32::MIN + 1,
-1000000,
-1000,
-128,
-1,
0,
1,
127,
128,
16383,
16384,
2097151,
2097152,
268435455,
268435456,
i32::MAX - 1,
i32::MAX,
];
for &expected in &varint32_values {
let value = reader.read_varint32().unwrap();
assert_eq!(expected, value, "varint32 value mismatch");
}
let varuint32_values = vec![
0,
1,
127,
128,
16383,
16384,
2097151,
2097152,
268435455,
268435456,
i32::MAX - 1,
i32::MAX,
];
for &expected in &varuint32_values {
let value = reader.read_varuint32().unwrap();
assert_eq!(expected, value as i32, "varuint32 value mismatch");
}
let varuint64_values = vec![
0u64,
1,
127,
128,
16383,
16384,
2097151,
2097152,
268435455,
268435456,
34359738367,
34359738368,
4398046511103,
4398046511104,
562949953421311,
562949953421312,
72057594037927935,
72057594037927936,
i64::MAX as u64,
];
for &expected in &varuint64_values {
let value = reader.read_varuint64().unwrap();
assert_eq!(expected, value, "varuint64 value mismatch");
}
let varint64_values = vec![
i64::MIN,
i64::MIN + 1,
-1000000000000,
-1000000,
-1000,
-128,
-1,
0,
1,
127,
1000,
1000000,
1000000000000,
i64::MAX - 1,
i64::MAX,
];
for &expected in &varint64_values {
let value = reader.read_varint64().unwrap();
assert_eq!(expected, value, "varint64 value mismatch");
}
let mut buffer = vec![];
let mut writer = Writer::from_buffer(&mut buffer);
for &value in &varint32_values {
writer.write_varint32(value);
}
for &value in &varuint32_values {
writer.write_varuint32(value as u32);
}
for &value in &varuint64_values {
writer.write_varuint64(value);
}
for &value in &varint64_values {
writer.write_varint64(value);
}
fs::write(data_file_path, writer.dump()).unwrap();
}
#[test]
#[ignore]
fn test_murmurhash3() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let (h1, h2) = murmurhash3_x64_128(&[1, 2, 8], 47);
assert_eq!(reader.read_i64().unwrap(), h1 as i64);
assert_eq!(reader.read_i64().unwrap(), h2 as i64);
}
#[test]
#[ignore]
fn test_string_serializer() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let fory = Fory::default()
.compatible(true)
.xlang(true)
.compress_string(false);
let mut reader_compress = Reader::new(bytes.as_slice());
let fory_compress = Fory::default()
.compatible(true)
.xlang(true)
.compress_string(true);
let test_strings: Vec<String> = vec![
// Latin1
"ab".to_string(),
"Rust123".to_string(),
"Çüéâäàåçêëèïî".to_string(),
// UTF16
"こんにちは".to_string(),
"Привет".to_string(),
"𝄞🎵🎶".to_string(),
// UTF8
"Hello, 世界".to_string(),
];
for s in &test_strings {
assert_eq!(*s, fory.deserialize_from::<String>(&mut reader).unwrap());
assert_eq!(
*s,
fory_compress
.deserialize_from::<String>(&mut reader_compress)
.unwrap()
);
}
let fory = Fory::default().compatible(true).xlang(true);
let mut buf = Vec::new();
for s in &test_strings {
fory.serialize_to(&mut buf, s).unwrap();
}
fs::write(&data_file_path, buf).unwrap();
}
macro_rules! assert_de {
($fory:expr, $reader:expr, $ty:ty, $expected:expr) => {{
let v: $ty = $fory.deserialize_from(&mut $reader).unwrap();
assert_eq!(v, $expected);
}};
}
#[test]
#[ignore]
#[allow(deprecated)]
fn test_cross_language_serializer() {
let day = NaiveDate::from_ymd_opt(2021, 11, 23).unwrap();
let instant = NaiveDateTime::from_timestamp(100, 0);
let str_list = vec!["hello".to_string(), "world".to_string()];
let str_set = HashSet::from(["hello".to_string(), "world".to_string()]);
let str_map = HashMap::<String, String>::from([
("hello".to_string(), "world".to_string()),
("foo".to_string(), "bar".to_string()),
]);
let color = Color::White;
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Color>(101).unwrap();
assert_de!(fory, reader, bool, true);
assert_de!(fory, reader, bool, false);
assert_de!(fory, reader, i32, -1);
assert_de!(fory, reader, i8, i8::MAX);
assert_de!(fory, reader, i8, i8::MIN);
assert_de!(fory, reader, i16, i16::MAX);
assert_de!(fory, reader, i16, i16::MIN);
assert_de!(fory, reader, i32, i32::MAX);
assert_de!(fory, reader, i32, i32::MIN);
assert_de!(fory, reader, i64, i64::MAX);
assert_de!(fory, reader, i64, i64::MIN);
assert_de!(fory, reader, f32, -1f32);
assert_de!(fory, reader, f64, -1f64);
assert_de!(fory, reader, String, "str".to_string());
assert_de!(fory, reader, NaiveDate, day);
assert_de!(fory, reader, NaiveDateTime, instant);
assert_de!(fory, reader, Vec<bool>, [true, false]);
assert_de!(fory, reader, Vec<u8>, [1, i8::MAX as u8]);
assert_de!(fory, reader, Vec<i16>, [1, i16::MAX]);
assert_de!(fory, reader, Vec<i32>, [1, i32::MAX]);
assert_de!(fory, reader, Vec<i64>, [1, i64::MAX]);
assert_de!(fory, reader, Vec<f32>, [1f32, 2f32]);
assert_de!(fory, reader, Vec<f64>, [1f64, 2f64]);
assert_de!(fory, reader, Vec<String>, str_list);
assert_de!(fory, reader, HashSet<String>, str_set);
assert_de!(fory, reader, HashMap::<String, String>, str_map);
assert_de!(fory, reader, Color, color);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &true).unwrap();
fory.serialize_to(&mut buf, &false).unwrap();
fory.serialize_to(&mut buf, &-1).unwrap();
fory.serialize_to(&mut buf, &i8::MAX).unwrap();
fory.serialize_to(&mut buf, &i8::MIN).unwrap();
fory.serialize_to(&mut buf, &i16::MAX).unwrap();
fory.serialize_to(&mut buf, &i16::MIN).unwrap();
fory.serialize_to(&mut buf, &i32::MAX).unwrap();
fory.serialize_to(&mut buf, &i32::MIN).unwrap();
fory.serialize_to(&mut buf, &i64::MAX).unwrap();
fory.serialize_to(&mut buf, &i64::MIN).unwrap();
fory.serialize_to(&mut buf, &-1f32).unwrap();
fory.serialize_to(&mut buf, &-1f64).unwrap();
fory.serialize_to(&mut buf, &"str".to_string()).unwrap();
fory.serialize_to(&mut buf, &day).unwrap();
fory.serialize_to(&mut buf, &instant).unwrap();
fory.serialize_to(&mut buf, &vec![true, false]).unwrap();
fory.serialize_to(&mut buf, &vec![1, i8::MAX as u8])
.unwrap();
fory.serialize_to(&mut buf, &vec![1, i16::MAX]).unwrap();
fory.serialize_to(&mut buf, &vec![1, i32::MAX]).unwrap();
fory.serialize_to(&mut buf, &vec![1, i64::MAX]).unwrap();
fory.serialize_to(&mut buf, &vec![1f32, 2f32]).unwrap();
fory.serialize_to(&mut buf, &vec![1f64, 2f64]).unwrap();
fory.serialize_to(&mut buf, &str_list).unwrap();
fory.serialize_to(&mut buf, &str_set).unwrap();
fory.serialize_to(&mut buf, &str_map).unwrap();
fory.serialize_to(&mut buf, &color).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[test]
#[ignore]
fn test_simple_struct() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Color>(101).unwrap();
fory.register::<Item>(102).unwrap();
fory.register::<SimpleStruct>(103).unwrap();
let local_obj = SimpleStruct {
f1: HashMap::from([(1, 1.0f64), (2, 2.0f64)]),
f2: 39,
f3: Item {
name: "item".to_string(),
},
f4: "f4".to_string(),
f5: Color::White,
f6: vec!["f6".to_string()],
f7: 40,
f8: 41,
last: 42,
};
let remote_obj: SimpleStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
let new_local_obj: SimpleStruct = fory.deserialize(&new_bytes).unwrap();
assert_eq!(new_local_obj, local_obj);
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_simple_named_struct() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register_by_namespace::<Color>("demo", "color")
.unwrap();
fory.register_by_namespace::<Item>("demo", "item").unwrap();
fory.register_by_namespace::<SimpleStruct>("demo", "simple_struct")
.unwrap();
let local_obj = SimpleStruct {
f1: HashMap::from([(1, 1.0f64), (2, 2.0f64)]),
f2: 39,
f3: Item {
name: "item".to_string(),
},
f4: "f4".to_string(),
f5: Color::White,
f6: vec!["f6".to_string()],
f7: 40,
f8: 41,
last: 42,
};
let remote_obj: SimpleStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
let new_local_obj: SimpleStruct = fory.deserialize(&new_bytes).unwrap();
assert_eq!(new_local_obj, local_obj);
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_list() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Item>(102).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let str_list = vec![Some("a".to_string()), Some("b".to_string())];
let str_list2 = vec![None, Some("b".to_string())];
let item = Item {
name: "a".to_string(),
};
let item2 = Item {
name: "b".to_string(),
};
let item3 = Item {
name: "c".to_string(),
};
let item_list = vec![Some(item), Some(item2)];
let item_list2 = vec![None, Some(item3)];
let remote_str_list: Vec<Option<String>> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_str_list, str_list);
let remote_str_list2: Vec<Option<String>> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_str_list2, str_list2);
let remote_item_list: Vec<Option<Item>> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item_list, item_list);
let remote_item_list2: Vec<Option<Item>> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item_list2, item_list2);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &remote_str_list).unwrap();
fory.serialize_to(&mut buf, &remote_str_list2).unwrap();
fory.serialize_to(&mut buf, &remote_item_list).unwrap();
fory.serialize_to(&mut buf, &remote_item_list2).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[test]
#[ignore]
fn test_map() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Item>(102).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let str_map = HashMap::from([
(Some("k1".to_string()), Some("v1".to_string())),
(None, Some("v2".to_string())),
(Some("k3".to_string()), None),
(Some("k4".to_string()), Some("v4".to_string())),
]);
let item_map = HashMap::from([
(
Some("k1".to_string()),
Some(Item {
name: "item1".to_string(),
}),
),
(
None,
Some(Item {
name: "item2".to_string(),
}),
),
(Some("k3".to_string()), None),
(
Some("k4".to_string()),
Some(Item {
name: "item3".to_string(),
}),
),
]);
let remote_str_map: HashMap<Option<String>, Option<String>> =
fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_str_map, str_map);
let data_bytes1 = fory.serialize(&remote_str_map).unwrap();
let new_local_str_map: HashMap<Option<String>, Option<String>> =
fory.deserialize(&data_bytes1).unwrap();
assert_eq!(new_local_str_map, str_map);
let remote_item_map: HashMap<Option<String>, Option<Item>> =
fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item_map, item_map);
let data_bytes2 = fory.serialize(&remote_item_map).unwrap();
let new_local_item_map: HashMap<Option<String>, Option<Item>> =
fory.deserialize(&data_bytes2).unwrap();
assert_eq!(new_local_item_map, item_map);
let all_bytes = [data_bytes1.as_slice(), data_bytes2.as_slice()].concat();
fs::write(&data_file_path, all_bytes).unwrap();
}
#[test]
#[ignore]
fn test_integer() {
// In xlang mode with nullable=false default:
// - Java int fields -> Rust i32 (no ref flag)
// - Java Integer fields (with nullable=false) -> Rust i32 (no ref flag)
// All fields use i32 because Java xlang mode defaults to nullable=false for all non-primitives
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct Item2 {
f1: i32,
f2: i32,
f3: i32,
f4: i32,
f5: i32,
f6: i32,
}
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Item2>(101).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let f1 = 1;
let f2 = 2;
let f3 = 3;
let f4 = 4;
let f5 = 0;
let f6 = 0;
let local_item2 = Item2 {
f1,
f2,
f3,
f4,
f5,
f6,
};
let remote_item2: Item2 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item2, local_item2);
// When deserializing standalone values, they're serialized with ref flag
let remote_f1: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_f1, f1);
let remote_f2: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_f2, f2);
let remote_f3: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_f3, f3);
let remote_f4: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_f4, f4);
let remote_f5: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_f5, f5);
let remote_f6: i32 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_f6, f6);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &remote_item2).unwrap();
fory.serialize_to(&mut buf, &remote_f1).unwrap();
fory.serialize_to(&mut buf, &remote_f2).unwrap();
fory.serialize_to(&mut buf, &remote_f3).unwrap();
fory.serialize_to(&mut buf, &remote_f4).unwrap();
fory.serialize_to(&mut buf, &remote_f5).unwrap();
fory.serialize_to(&mut buf, &remote_f6).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[derive(ForyObject, Debug, PartialEq)]
struct MyStruct {
id: i32,
}
#[derive(Debug, PartialEq, Default)]
struct MyExt {
id: i32,
}
impl Serializer for MyExt {
fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), fory_core::error::Error> {
write_data(&self.id, context)
}
fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
Ok(Self {
id: read_data(context)?,
})
}
fn fory_type_id_dyn(
&self,
type_resolver: &TypeResolver,
) -> Result<u32, fory_core::error::Error> {
Self::fory_get_type_id(type_resolver)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ForyDefault for MyExt {
fn fory_default() -> Self {
Self::default()
}
}
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct MyWrapper {
color: Color,
my_struct: MyStruct,
my_ext: MyExt,
}
fn _test_skip_custom(fory1: &Fory, fory2: &Fory) {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
assert_eq!(
fory1.deserialize::<Empty>(&bytes).unwrap(),
Empty::default()
);
let wrapper = MyWrapper {
color: Color::White,
my_struct: MyStruct { id: 42 },
my_ext: MyExt { id: 43 },
};
let bytes = fory2.serialize(&wrapper).unwrap();
fs::write(&data_file_path, bytes).unwrap();
}
#[test]
#[ignore]
fn test_skip_id_custom() {
let mut fory1 = Fory::default().compatible(true).xlang(true);
fory1.register_serializer::<MyExt>(103).unwrap();
fory1.register::<Empty>(104).unwrap();
let mut fory2 = Fory::default().compatible(true).xlang(true);
fory2.register::<Color>(101).unwrap();
fory2.register::<MyStruct>(102).unwrap();
fory2.register_serializer::<MyExt>(103).unwrap();
fory2.register::<MyWrapper>(104).unwrap();
_test_skip_custom(&fory1, &fory2);
}
#[test]
#[ignore]
fn test_skip_name_custom() {
let mut fory1 = Fory::default().compatible(true).xlang(true);
fory1
.register_serializer_by_name::<MyExt>("my_ext")
.unwrap();
fory1.register_by_name::<Empty>("my_wrapper").unwrap();
let mut fory2 = Fory::default().compatible(true).xlang(true);
fory2.register_by_name::<Color>("color").unwrap();
fory2.register_by_name::<MyStruct>("my_struct").unwrap();
fory2
.register_serializer_by_name::<MyExt>("my_ext")
.unwrap();
fory2.register_by_name::<MyWrapper>("my_wrapper").unwrap();
_test_skip_custom(&fory1, &fory2);
}
#[test]
#[ignore]
fn test_consistent_named() {
let mut fory = Fory::default().compatible(false).xlang(true);
fory.register_by_name::<Color>("color").unwrap();
fory.register_by_name::<MyStruct>("my_struct").unwrap();
fory.register_serializer_by_name::<MyExt>("my_ext").unwrap();
let color = Color::White;
let my_struct = MyStruct { id: 42 };
let my_ext = MyExt { id: 43 };
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut reader = Reader::new(bytes.as_slice());
for _ in 0..3 {
assert_eq!(fory.deserialize_from::<Color>(&mut reader).unwrap(), color);
}
for _ in 0..3 {
assert_eq!(
fory.deserialize_from::<MyStruct>(&mut reader).unwrap(),
my_struct
);
}
for _ in 0..3 {
assert_eq!(fory.deserialize_from::<MyExt>(&mut reader).unwrap(), my_ext);
}
let mut buf = Vec::new();
for _ in 0..3 {
fory.serialize_to(&mut buf, &color).unwrap();
}
for _ in 0..3 {
fory.serialize_to(&mut buf, &my_struct).unwrap();
}
for _ in 0..3 {
fory.serialize_to(&mut buf, &my_ext).unwrap();
}
fs::write(&data_file_path, buf).unwrap();
}
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct VersionCheckStruct {
f1: i32,
f2: Option<String>,
f3: f64,
}
#[derive(ForyObject, Debug, PartialEq)]
struct StructWithList {
items: Vec<Option<String>>,
}
#[derive(ForyObject, Debug, PartialEq)]
struct StructWithMap {
data: HashMap<Option<String>, Option<String>>,
}
// ============================================================================
// Schema Evolution Test Types
// ============================================================================
#[derive(ForyObject, Debug, PartialEq)]
struct EmptyStructEvolution {}
// Java f1 has @ForyField(id = -1, nullable = true), so it's nullable
#[derive(ForyObject, Debug, PartialEq)]
struct OneStringFieldStruct {
#[fory(nullable = true)]
f1: Option<String>,
}
// Both f1 and f2 are non-nullable in Java xlang mode
#[derive(ForyObject, Debug, PartialEq)]
struct TwoStringFieldStruct {
f1: String,
f2: String,
}
#[allow(non_camel_case_types)]
#[derive(ForyObject, Debug, PartialEq, Default, Clone)]
enum TestEnum {
#[default]
VALUE_A,
VALUE_B,
VALUE_C,
}
#[derive(ForyObject, Debug, PartialEq)]
struct OneEnumFieldStruct {
f1: TestEnum,
}
// Both f1 and f2 are non-nullable in Java xlang mode
#[derive(ForyObject, Debug, PartialEq)]
struct TwoEnumFieldStruct {
f1: TestEnum,
f2: TestEnum,
}
#[test]
#[ignore]
fn test_struct_version_check() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default()
.compatible(false)
.xlang(true)
.check_struct_version(true);
fory.register::<VersionCheckStruct>(201).unwrap();
let local_obj = VersionCheckStruct {
f1: 10,
f2: Some("test".to_string()),
f3: 3.2,
};
let remote_obj: VersionCheckStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
let new_local_obj: VersionCheckStruct = fory.deserialize(&new_bytes).unwrap();
assert_eq!(new_local_obj, local_obj);
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_item() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Item>(102).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let item1 = Item {
name: "test_item_1".to_string(),
};
let item2 = Item {
name: "test_item_2".to_string(),
};
// With nullable=false (xlang default), Java sends empty string instead of null
let item3 = Item {
name: String::new(),
};
let remote_item1: Item = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item1, item1);
let remote_item2: Item = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item2, item2);
let remote_item3: Item = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_item3, item3);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &remote_item1).unwrap();
fory.serialize_to(&mut buf, &remote_item2).unwrap();
fory.serialize_to(&mut buf, &remote_item3).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[test]
#[ignore]
fn test_color() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Color>(101).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let remote_green: Color = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_green, Color::Green);
let remote_red: Color = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_red, Color::Red);
let remote_blue: Color = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_blue, Color::Blue);
let remote_white: Color = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_white, Color::White);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &Color::Green).unwrap();
fory.serialize_to(&mut buf, &Color::Red).unwrap();
fory.serialize_to(&mut buf, &Color::Blue).unwrap();
fory.serialize_to(&mut buf, &Color::White).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[test]
#[ignore]
fn test_struct_with_list() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<StructWithList>(201).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let struct1 = StructWithList {
items: vec![
Some("a".to_string()),
Some("b".to_string()),
Some("c".to_string()),
],
};
let struct2 = StructWithList {
items: vec![Some("x".to_string()), None, Some("z".to_string())],
};
let remote_struct1: StructWithList = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_struct1, struct1);
let remote_struct2: StructWithList = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_struct2, struct2);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &remote_struct1).unwrap();
fory.serialize_to(&mut buf, &remote_struct2).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[test]
#[ignore]
fn test_struct_with_map() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<StructWithMap>(202).unwrap();
let mut reader = Reader::new(bytes.as_slice());
let struct1 = StructWithMap {
data: HashMap::from([
(Some("key1".to_string()), Some("value1".to_string())),
(Some("key2".to_string()), Some("value2".to_string())),
]),
};
let struct2 = StructWithMap {
data: HashMap::from([
(Some("k1".to_string()), None),
(None, Some("v2".to_string())),
]),
};
let remote_struct1: StructWithMap = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_struct1, struct1);
let remote_struct2: StructWithMap = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(remote_struct2, struct2);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &remote_struct1).unwrap();
fory.serialize_to(&mut buf, &remote_struct2).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
// ============================================================================
// Polymorphic Container Test Types - Using Box<dyn Any> for runtime polymorphism
// ============================================================================
use std::any::Any;
#[derive(ForyObject, Debug, PartialEq, Clone)]
struct Dog {
age: i32,
// Match Java's @ForyField(nullable = true) annotation
name: Option<String>,
}
#[derive(ForyObject, Debug, PartialEq, Clone)]
struct Cat {
age: i32,
lives: i32,
}
#[derive(ForyObject, Debug)]
struct AnimalListHolder {
animals: Vec<Box<dyn Any>>,
}
#[derive(ForyObject, Debug)]
struct AnimalMapHolder {
animal_map: HashMap<String, Box<dyn Any>>,
}
#[test]
#[ignore]
fn test_polymorphic_list() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Dog>(302).unwrap();
fory.register::<Cat>(303).unwrap();
fory.register::<AnimalListHolder>(304).unwrap();
let mut reader = Reader::new(bytes.as_slice());
// Part 1: Read List<Animal> with polymorphic elements (Dog, Cat)
let animals: Vec<Box<dyn Any>> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(animals.len(), 2);
// First element should be Dog
let dog = animals[0]
.downcast_ref::<Dog>()
.expect("First element should be Dog");
assert_eq!(dog.age, 3);
assert_eq!(dog.name, Some("Buddy".to_string()));
// Second element should be Cat
let cat = animals[1]
.downcast_ref::<Cat>()
.expect("Second element should be Cat");
assert_eq!(cat.age, 5);
assert_eq!(cat.lives, 9);
// Part 2: Read AnimalListHolder (List<Animal> as struct field)
let holder: AnimalListHolder = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(holder.animals.len(), 2);
let dog2 = holder.animals[0]
.downcast_ref::<Dog>()
.expect("First holder element should be Dog");
assert_eq!(dog2.name, Some("Rex".to_string()));
let cat2 = holder.animals[1]
.downcast_ref::<Cat>()
.expect("Second holder element should be Cat");
assert_eq!(cat2.lives, 7);
// Write back
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &animals).unwrap();
fory.serialize_to(&mut buf, &holder).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
#[test]
#[ignore]
fn test_polymorphic_map() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<Dog>(302).unwrap();
fory.register::<Cat>(303).unwrap();
fory.register::<AnimalMapHolder>(305).unwrap();
let mut reader = Reader::new(bytes.as_slice());
// Part 1: Read Map<String, Animal> with polymorphic values
let animal_map: HashMap<String, Box<dyn Any>> = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(animal_map.len(), 2);
let dog1 = animal_map
.get("dog1")
.expect("dog1 should exist")
.downcast_ref::<Dog>()
.expect("dog1 should be Dog");
assert_eq!(dog1.name, Some("Rex".to_string()));
assert_eq!(dog1.age, 2);
let cat1 = animal_map
.get("cat1")
.expect("cat1 should exist")
.downcast_ref::<Cat>()
.expect("cat1 should be Cat");
assert_eq!(cat1.lives, 9);
assert_eq!(cat1.age, 4);
// Part 2: Read AnimalMapHolder (Map<String, Animal> as struct field)
let holder: AnimalMapHolder = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(holder.animal_map.len(), 2);
let my_dog = holder
.animal_map
.get("myDog")
.expect("myDog should exist")
.downcast_ref::<Dog>()
.expect("myDog should be Dog");
assert_eq!(my_dog.name, Some("Fido".to_string()));
let my_cat = holder
.animal_map
.get("myCat")
.expect("myCat should exist")
.downcast_ref::<Cat>()
.expect("myCat should be Cat");
assert_eq!(my_cat.lives, 8);
// Write back
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &animal_map).unwrap();
fory.serialize_to(&mut buf, &holder).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
// ============================================================================
// Schema Evolution Tests - String Fields
// ============================================================================
#[test]
#[ignore]
fn test_one_string_field_schema() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(false).xlang(true);
fory.register::<OneStringFieldStruct>(200).unwrap();
let value: OneStringFieldStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(value.f1, Some("hello".to_string()));
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_one_string_field_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<OneStringFieldStruct>(200).unwrap();
let value: OneStringFieldStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(value.f1, Some("hello".to_string()));
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_two_string_field_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<TwoStringFieldStruct>(201).unwrap();
let value: TwoStringFieldStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(value.f1, "first".to_string());
assert_eq!(value.f2, "second".to_string());
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_schema_evolution_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
// Read TwoStringFieldStruct data as EmptyStructEvolution
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<EmptyStructEvolution>(200).unwrap();
let value: EmptyStructEvolution = fory.deserialize(&bytes).unwrap();
// Serialize back as EmptyStructEvolution
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_schema_evolution_compatible_reverse() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
// Read OneStringFieldStruct data as TwoStringFieldStruct (missing f2)
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<TwoStringFieldStruct>(200).unwrap();
let value: TwoStringFieldStruct = fory.deserialize(&bytes).unwrap();
// f1 should be "only_one", f2 should be empty string (default for missing non-nullable String field)
assert_eq!(value.f1, "only_one".to_string());
assert_eq!(value.f2, String::default()); // Empty string for missing non-nullable field
// Serialize back
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
// ============================================================================
// Schema Evolution Tests - Enum Fields
// ============================================================================
#[test]
#[ignore]
fn test_one_enum_field_schema() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(false).xlang(true);
fory.register::<TestEnum>(210).unwrap();
fory.register::<OneEnumFieldStruct>(211).unwrap();
let value: OneEnumFieldStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(value.f1, TestEnum::VALUE_B);
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_one_enum_field_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<TestEnum>(210).unwrap();
fory.register::<OneEnumFieldStruct>(211).unwrap();
let value: OneEnumFieldStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(value.f1, TestEnum::VALUE_A);
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_two_enum_field_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<TestEnum>(210).unwrap();
fory.register::<TwoEnumFieldStruct>(212).unwrap();
let value: TwoEnumFieldStruct = fory.deserialize(&bytes).unwrap();
assert_eq!(value.f1, TestEnum::VALUE_A);
assert_eq!(value.f2, TestEnum::VALUE_C);
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_enum_schema_evolution_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
// Read TwoEnumFieldStruct data as EmptyStructEvolution
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<TestEnum>(210).unwrap();
fory.register::<EmptyStructEvolution>(211).unwrap();
let value: EmptyStructEvolution = fory.deserialize(&bytes).unwrap();
// Serialize back as EmptyStructEvolution
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_enum_schema_evolution_compatible_reverse() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
// Read OneEnumFieldStruct data as TwoEnumFieldStruct (missing f2)
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<TestEnum>(210).unwrap();
fory.register::<TwoEnumFieldStruct>(211).unwrap();
let value: TwoEnumFieldStruct = fory.deserialize(&bytes).unwrap();
// f1 should be ValueC
assert_eq!(value.f1, TestEnum::VALUE_C);
// f2 should be default (VALUE_A) since it's not present in source data and is non-nullable
assert_eq!(value.f2, TestEnum::VALUE_A);
// Serialize back
let new_bytes = fory.serialize(&value).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
// ============================================================================
// Nullable Field Tests - Comprehensive nullable field testing
// ============================================================================
/// Comprehensive struct for testing nullable fields in SCHEMA_CONSISTENT mode (compatible=false).
/// Fields are organized as:
/// - Base non-nullable fields: byte, short, int, long, float, double, bool, string, list, set, map
/// - Nullable fields (first half - boxed numeric types): Integer, Long, Float
/// - Nullable fields (second half - @ForyField): Double, Boolean, String, List, Set, Map
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct NullableComprehensiveSchemaConsistent {
// Base non-nullable primitive fields
byte_field: i8,
short_field: i16,
int_field: i32,
long_field: i64,
float_field: f32,
double_field: f64,
bool_field: bool,
// Base non-nullable reference fields
string_field: String,
list_field: Vec<String>,
set_field: HashSet<String>,
map_field: HashMap<String, String>,
// Nullable fields - first half using boxed types
#[fory(nullable = true)]
nullable_int: Option<i32>,
#[fory(nullable = true)]
nullable_long: Option<i64>,
#[fory(nullable = true)]
nullable_float: Option<f32>,
// Nullable fields - second half using @ForyField annotation
#[fory(nullable = true)]
nullable_double: Option<f64>,
#[fory(nullable = true)]
nullable_bool: Option<bool>,
#[fory(nullable = true)]
nullable_string: Option<String>,
#[fory(nullable = true)]
nullable_list: Option<Vec<String>>,
#[fory(nullable = true)]
nullable_set: Option<HashSet<String>>,
#[fory(nullable = true)]
nullable_map: Option<HashMap<String, String>>,
}
/// Cross-language schema evolution test struct for COMPATIBLE mode.
/// This struct has INVERTED nullability compared to Java:
/// - Group 1: Nullable in Rust (Option), Non-nullable in Java
/// - Group 2: Non-nullable in Rust, Nullable in Java (@ForyField(nullable=true))
///
/// This tests that compatible mode properly handles schema differences across languages.
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct NullableComprehensiveCompatible {
// Group 1: Nullable in Rust, Non-nullable in Java
// Primitive fields
#[fory(nullable = true)]
byte_field: Option<i8>,
#[fory(nullable = true)]
short_field: Option<i16>,
#[fory(nullable = true)]
int_field: Option<i32>,
#[fory(nullable = true)]
long_field: Option<i64>,
#[fory(nullable = true)]
float_field: Option<f32>,
#[fory(nullable = true)]
double_field: Option<f64>,
#[fory(nullable = true)]
bool_field: Option<bool>,
// Boxed fields - also nullable in Rust
#[fory(nullable = true)]
boxed_int: Option<i32>,
#[fory(nullable = true)]
boxed_long: Option<i64>,
#[fory(nullable = true)]
boxed_float: Option<f32>,
#[fory(nullable = true)]
boxed_double: Option<f64>,
#[fory(nullable = true)]
boxed_bool: Option<bool>,
// Reference fields - also nullable in Rust
#[fory(nullable = true)]
string_field: Option<String>,
#[fory(nullable = true)]
list_field: Option<Vec<String>>,
#[fory(nullable = true)]
set_field: Option<HashSet<String>>,
#[fory(nullable = true)]
map_field: Option<HashMap<String, String>>,
// Group 2: Non-nullable in Rust, Nullable in Java (@ForyField(nullable=true))
// Boxed types
nullable_int1: i32,
nullable_long1: i64,
nullable_float1: f32,
nullable_double1: f64,
nullable_bool1: bool,
// Reference types
nullable_string2: String,
nullable_list2: Vec<String>,
nullable_set2: HashSet<String>,
nullable_map2: HashMap<String, String>,
}
#[test]
#[ignore]
fn test_nullable_field_schema_consistent_not_null() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(false).xlang(true);
fory.register::<NullableComprehensiveSchemaConsistent>(401)
.unwrap();
let local_obj = NullableComprehensiveSchemaConsistent {
// Base non-nullable primitive fields
byte_field: 1,
short_field: 2,
int_field: 42,
long_field: 123456789,
float_field: 1.5,
double_field: 2.5,
bool_field: true,
// Base non-nullable reference fields
string_field: "hello".to_string(),
list_field: vec!["a".to_string(), "b".to_string(), "c".to_string()],
set_field: HashSet::from(["x".to_string(), "y".to_string()]),
map_field: HashMap::from([
("key1".to_string(), "value1".to_string()),
("key2".to_string(), "value2".to_string()),
]),
// Nullable fields - all have values (first half - boxed)
nullable_int: Some(100),
nullable_long: Some(200),
nullable_float: Some(1.5),
// Nullable fields - all have values (second half - @ForyField)
nullable_double: Some(2.5),
nullable_bool: Some(false),
nullable_string: Some("nullable_value".to_string()),
nullable_list: Some(vec!["p".to_string(), "q".to_string()]),
nullable_set: Some(HashSet::from(["m".to_string(), "n".to_string()])),
nullable_map: Some(HashMap::from([("nk1".to_string(), "nv1".to_string())])),
};
let remote_obj: NullableComprehensiveSchemaConsistent = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
#[test]
#[ignore]
fn test_nullable_field_schema_consistent_null() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(false).xlang(true);
fory.register::<NullableComprehensiveSchemaConsistent>(401)
.unwrap();
let local_obj = NullableComprehensiveSchemaConsistent {
// Base non-nullable primitive fields - must have values
byte_field: 1,
short_field: 2,
int_field: 42,
long_field: 123456789,
float_field: 1.5,
double_field: 2.5,
bool_field: true,
// Base non-nullable reference fields - must have values
string_field: "hello".to_string(),
list_field: vec!["a".to_string(), "b".to_string(), "c".to_string()],
set_field: HashSet::from(["x".to_string(), "y".to_string()]),
map_field: HashMap::from([
("key1".to_string(), "value1".to_string()),
("key2".to_string(), "value2".to_string()),
]),
// Nullable fields - all null (first half - boxed)
nullable_int: None,
nullable_long: None,
nullable_float: None,
// Nullable fields - all null (second half - @ForyField)
nullable_double: None,
nullable_bool: None,
nullable_string: None,
nullable_list: None,
nullable_set: None,
nullable_map: None,
};
let remote_obj: NullableComprehensiveSchemaConsistent = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
/// Test cross-language schema evolution - all fields have values.
/// Java sends: Group 1 (non-nullable) + Group 2 (nullable with values)
/// Rust reads: Group 1 (nullable/Option) + Group 2 (non-nullable)
#[test]
#[ignore]
fn test_nullable_field_compatible_not_null() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<NullableComprehensiveCompatible>(402)
.unwrap();
let local_obj = NullableComprehensiveCompatible {
// Group 1: Nullable in Rust (read from Java's non-nullable)
byte_field: Some(1),
short_field: Some(2),
int_field: Some(42),
long_field: Some(123456789),
float_field: Some(1.5),
double_field: Some(2.5),
bool_field: Some(true),
boxed_int: Some(10),
boxed_long: Some(20),
boxed_float: Some(1.1),
boxed_double: Some(2.2),
boxed_bool: Some(true),
string_field: Some("hello".to_string()),
list_field: Some(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
set_field: Some(HashSet::from(["x".to_string(), "y".to_string()])),
map_field: Some(HashMap::from([
("key1".to_string(), "value1".to_string()),
("key2".to_string(), "value2".to_string()),
])),
// Group 2: Non-nullable in Rust (read from Java's nullable with values)
nullable_int1: 100,
nullable_long1: 200,
nullable_float1: 1.5,
nullable_double1: 2.5,
nullable_bool1: false,
nullable_string2: "nullable_value".to_string(),
nullable_list2: vec!["p".to_string(), "q".to_string()],
nullable_set2: HashSet::from(["m".to_string(), "n".to_string()]),
nullable_map2: HashMap::from([("nk1".to_string(), "nv1".to_string())]),
};
let remote_obj: NullableComprehensiveCompatible = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
/// Test cross-language schema evolution - nullable fields are null.
/// Java sends: Group 1 (non-nullable with values) + Group 2 (nullable with null)
/// Rust reads: Group 1 (nullable/Option) + Group 2 (non-nullable -> defaults)
///
/// When Java sends null for Group 2 fields, Rust's non-nullable fields receive
/// default values (0 for numbers, false for bool, empty for collections/strings).
#[test]
#[ignore]
fn test_nullable_field_compatible_null() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
fory.register::<NullableComprehensiveCompatible>(402)
.unwrap();
let local_obj = NullableComprehensiveCompatible {
// Group 1: Nullable in Rust (read from Java's non-nullable)
byte_field: Some(1),
short_field: Some(2),
int_field: Some(42),
long_field: Some(123456789),
float_field: Some(1.5),
double_field: Some(2.5),
bool_field: Some(true),
boxed_int: Some(10),
boxed_long: Some(20),
boxed_float: Some(1.1),
boxed_double: Some(2.2),
boxed_bool: Some(true),
string_field: Some("hello".to_string()),
list_field: Some(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
set_field: Some(HashSet::from(["x".to_string(), "y".to_string()])),
map_field: Some(HashMap::from([
("key1".to_string(), "value1".to_string()),
("key2".to_string(), "value2".to_string()),
])),
// Group 2: Non-nullable in Rust (Java sent null -> use defaults)
nullable_int1: 0,
nullable_long1: 0,
nullable_float1: 0.0,
nullable_double1: 0.0,
nullable_bool1: false,
nullable_string2: String::new(),
nullable_list2: Vec::new(),
nullable_set2: HashSet::new(),
nullable_map2: HashMap::new(),
};
let remote_obj: NullableComprehensiveCompatible = fory.deserialize(&bytes).unwrap();
assert_eq!(remote_obj, local_obj);
let new_bytes = fory.serialize(&remote_obj).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
// ============================================================================
// Union Xlang Tests - Rust enum <-> Java Union2
// ============================================================================
/// Rust enum that matches Java Union2<String, Long>
/// Each variant has exactly one field to be Union-compatible
#[derive(ForyObject, Debug, PartialEq)]
enum StringOrLong {
Str(String),
Long(i64),
}
impl Default for StringOrLong {
fn default() -> Self {
StringOrLong::Str(String::default())
}
}
/// Struct containing a Union field, matches Java StructWithUnion2
#[derive(ForyObject, Debug, PartialEq)]
struct StructWithUnion2 {
union: StringOrLong,
}
/// Test cross-language Union serialization between Rust enum and Java Union2.
///
/// Rust enum with single-field variants is Union-compatible and can be deserialized
/// from Java Union2 types. Union fields in xlang mode follow a special format:
/// - Rust writes: ref_flag + union_data (no type_id, since Union fields skip type info)
/// - Java reads: null_flag + union_data (directly calls UnionSerializer.read())
#[test]
#[ignore]
fn test_union_xlang() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true);
// Register both the enum and the struct that contains it
fory.register::<StringOrLong>(300).unwrap();
fory.register::<StructWithUnion2>(301).unwrap();
// Read struct1 with String value (index 0)
let mut reader = Reader::new(bytes.as_slice());
let struct1: StructWithUnion2 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(struct1.union, StringOrLong::Str("hello".to_string()));
// Read struct2 with Long value (index 1)
let struct2: StructWithUnion2 = fory.deserialize_from(&mut reader).unwrap();
assert_eq!(struct2.union, StringOrLong::Long(42));
// Serialize back
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &struct1).unwrap();
fory.serialize_to(&mut buf, &struct2).unwrap();
fs::write(&data_file_path, buf).unwrap();
}
// ============================================================================
// Reference Tracking Tests - Cross-language shared reference tests
// ============================================================================
use fory_core::RcWeak;
use std::rc::Rc;
/// Inner struct for reference tracking test (SCHEMA_CONSISTENT mode)
/// Matches Java RefInnerSchemaConsistent with type ID 501
#[derive(ForyObject, Debug, PartialEq, Clone)]
struct RefInnerSchemaConsistent {
id: i32,
name: String,
}
/// Outer struct for reference tracking test (SCHEMA_CONSISTENT mode)
/// Contains two fields that both point to the same inner object.
/// Matches Java RefOuterSchemaConsistent with type ID 502
/// Uses Option<Rc<T>> for nullable reference-tracked fields - Rc enables reference tracking
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct RefOuterSchemaConsistent {
inner1: Option<Rc<RefInnerSchemaConsistent>>,
inner2: Option<Rc<RefInnerSchemaConsistent>>,
}
/// Inner struct for reference tracking test (COMPATIBLE mode)
/// Matches Java RefInnerCompatible with type ID 503
#[derive(ForyObject, Debug, PartialEq, Clone)]
#[fory(debug)]
struct RefInnerCompatible {
id: i32,
name: String,
}
/// Outer struct for reference tracking test (COMPATIBLE mode)
/// Contains two fields that both point to the same inner object.
/// Matches Java RefOuterCompatible with type ID 504
/// Uses Option<Rc<T>> for nullable reference-tracked fields - Rc enables reference tracking
#[derive(ForyObject, Debug, PartialEq)]
#[fory(debug)]
struct RefOuterCompatible {
inner1: Option<Rc<RefInnerCompatible>>,
inner2: Option<Rc<RefInnerCompatible>>,
}
/// Test cross-language reference tracking in SCHEMA_CONSISTENT mode (compatible=false).
///
/// This test verifies that when Java serializes an object where two fields point to
/// the same instance, Rust can properly deserialize it and both fields will contain
/// equal values. When re-serializing, the reference relationship should be preserved.
#[test]
#[ignore]
fn test_ref_schema_consistent() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default()
.compatible(false)
.xlang(true)
.track_ref(true);
fory.register::<RefInnerSchemaConsistent>(501).unwrap();
fory.register::<RefOuterSchemaConsistent>(502).unwrap();
let outer: RefOuterSchemaConsistent = fory.deserialize(&bytes).unwrap();
// Both inner1 and inner2 should have values
assert!(outer.inner1.is_some(), "inner1 should not be None");
assert!(outer.inner2.is_some(), "inner2 should not be None");
// Both should have the same values (they reference the same object in Java)
let inner1 = outer.inner1.as_ref().unwrap();
let inner2 = outer.inner2.as_ref().unwrap();
assert_eq!(inner1.id, 42);
assert_eq!(inner1.name, "shared_inner");
// Compare the values (Rc contents)
assert_eq!(
inner1.as_ref(),
inner2.as_ref(),
"inner1 and inner2 should have equal values"
);
// With Rc, after deserialization with ref tracking, both fields should point to the same Rc
assert!(
Rc::ptr_eq(inner1, inner2),
"inner1 and inner2 should be the same Rc (reference identity)"
);
// Re-serialize and write back
let new_bytes = fory.serialize(&outer).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
/// Test cross-language reference tracking in COMPATIBLE mode (compatible=true).
///
/// This test verifies reference tracking works correctly with schema evolution support.
/// The inner object is shared between two fields, and this relationship should be
/// preserved through serialization/deserialization.
#[test]
#[ignore]
fn test_ref_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true).track_ref(true);
fory.register::<RefInnerCompatible>(503).unwrap();
fory.register::<RefOuterCompatible>(504).unwrap();
let outer: RefOuterCompatible = fory.deserialize(&bytes).unwrap();
// Both inner1 and inner2 should have values
assert!(outer.inner1.is_some(), "inner1 should not be None");
assert!(outer.inner2.is_some(), "inner2 should not be None");
// Both should have the same values (they reference the same object in Java)
let inner1 = outer.inner1.as_ref().unwrap();
let inner2 = outer.inner2.as_ref().unwrap();
assert_eq!(inner1.id, 99);
assert_eq!(inner1.name, "compatible_shared");
// Compare the values (Rc contents)
assert_eq!(
inner1.as_ref(),
inner2.as_ref(),
"inner1 and inner2 should have equal values"
);
// With Rc, after deserialization with ref tracking, both fields should point to the same Rc
assert!(
Rc::ptr_eq(inner1, inner2),
"inner1 and inner2 should be the same Rc (reference identity)"
);
// Re-serialize and write back
let new_bytes = fory.serialize(&outer).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
// ============================================================================
// Circular Reference Tests - Test self-referencing struct serialization
// ============================================================================
/// Struct for circular reference tests.
/// Contains a self-referencing field and a name field.
/// The 'self_ref' field points back to the same object, creating a circular reference.
/// Uses RcWeak for the self-reference to support forward reference resolution during deserialization.
/// This is the proper Rust pattern for circular references - RcWeak supports callbacks that
/// resolve when the strong Rc becomes available.
/// Matches Java CircularRefStruct with type ID 601 (schema consistent) or 602 (compatible)
#[derive(ForyObject, Debug, Clone)]
struct CircularRefStruct {
name: String,
#[fory(ref = true, nullable = true)]
self_ref: RcWeak<CircularRefStruct>,
}
/// Test circular reference in SCHEMA_CONSISTENT mode (compatible=false).
/// Creates a struct where the 'self_ref' field points back to the same object.
/// Verifies that after serialization/deserialization across languages,
/// the circular reference is preserved.
#[test]
#[ignore]
fn test_circular_ref_schema_consistent() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default()
.compatible(false)
.xlang(true)
.track_ref(true);
fory.register::<CircularRefStruct>(601).unwrap();
// Deserialize as Rc<CircularRefStruct> since the whole struct needs ref tracking
let obj: Rc<CircularRefStruct> = fory.deserialize(&bytes).unwrap();
// Verify the struct has the expected name
assert_eq!(obj.name, "circular_test");
// Verify circular reference is preserved (self_ref points back to the same object)
// RcWeak.upgrade() returns Option<Rc<T>> - should be Some if the strong ref exists
let upgraded = obj.self_ref.upgrade();
assert!(
upgraded.is_some(),
"self_ref.upgrade() should return Some (circular reference)"
);
let self_ref = upgraded.unwrap();
assert_eq!(self_ref.name, "circular_test");
// Verify it's actually pointing to the same object (circular)
assert!(
Rc::ptr_eq(&obj, &self_ref),
"self_ref should point to the same object as obj (circular reference)"
);
// Re-serialize and write back - serialize the Rc, not the dereferenced value
// This ensures the Rc is registered before the RcWeak self-reference tries to reference it
let new_bytes = fory.serialize(&obj).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}
/// Test circular reference in COMPATIBLE mode (compatible=true).
/// Creates a struct where the 'self_ref' field points back to the same object.
/// Verifies that circular references work with schema evolution support.
#[test]
#[ignore]
fn test_circular_ref_compatible() {
let data_file_path = get_data_file();
let bytes = fs::read(&data_file_path).unwrap();
let mut fory = Fory::default().compatible(true).xlang(true).track_ref(true);
fory.register::<CircularRefStruct>(602).unwrap();
// Deserialize as Rc<CircularRefStruct> since the whole struct needs ref tracking
let obj: Rc<CircularRefStruct> = fory.deserialize(&bytes).unwrap();
// Verify the struct has the expected name
assert_eq!(obj.name, "compatible_circular");
// Verify circular reference is preserved (self_ref points back to the same object)
let upgraded = obj.self_ref.upgrade();
assert!(
upgraded.is_some(),
"self_ref.upgrade() should return Some (circular reference)"
);
let self_ref = upgraded.unwrap();
assert_eq!(self_ref.name, "compatible_circular");
// Verify it's actually pointing to the same object (circular)
assert!(
Rc::ptr_eq(&obj, &self_ref),
"self_ref should point to the same object as obj (circular reference)"
);
// Re-serialize and write back - serialize the Rc, not the dereferenced value
// This ensures the Rc is registered before the RcWeak self-reference tries to reference it
let new_bytes = fory.serialize(&obj).unwrap();
fs::write(&data_file_path, new_bytes).unwrap();
}