This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new a570c2f0d feat(rust): add tuple struct support and improve generic
type handling (#3087)
a570c2f0d is described below
commit a570c2f0d345d6de47f4dfb4c7cb73a285a45825
Author: Damon Zhao <[email protected]>
AuthorDate: Thu Dec 25 12:02:37 2025 +0800
feat(rust): add tuple struct support and improve generic type handling
(#3087)
## Why?
To support serialization of tuple struct and improve generic type in
Rust.
After this PR, we can finally support fory for
[OpenRaft](https://github.com/databendlabs/openraft)
## What does this PR do?
This PR adds tuple struct serialization support and improves generic
type handling in `fory-derive`.
### 1. Tuple Struct Support
Previously, `#[derive(ForyObject)]` only supported named structs. Now it
also supports tuple structs:
```rust
use fory_derive::ForyObject;
// Tuple struct with multiple fields
#[derive(ForyObject, Debug, PartialEq)]
struct Point(f64, f64);
// Single field wrapper
#[derive(ForyObject, Debug, PartialEq)]
struct UserId(u64);
// Complex tuple struct
#[derive(ForyObject, Debug, PartialEq)]
struct Record(i32, String, Vec<u8>);
fn main() {
let mut fory = Fory::default();
fory.register::<Point>(100).unwrap();
let point = Point(3.5, 4.5);
let bytes = fory.serialize(&point).unwrap();
let result: Point = fory.deserialize(&bytes).unwrap();
assert_eq!(result, point);
}
```
### 2. Improved Generic Type Handling
- Fixed field ordering for tuple structs (must preserve original order,
not sort by type)
- Better fingerprint computation for struct versioning
- Improved type parameter detection in generic types
## Related issues
None
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- Yes, adds new public types support: tuple struct
- [ ] Does this PR introduce any binary protocol compatibility change?
- No
## Benchmark
---
rust/fory-derive/src/lib.rs | 24 ++-
rust/fory-derive/src/object/derive_enum.rs | 11 +-
rust/fory-derive/src/object/misc.rs | 100 +++++-----
rust/fory-derive/src/object/read.rs | 131 ++++++++-----
rust/fory-derive/src/object/serializer.rs | 144 ++++++++------
rust/fory-derive/src/object/util.rs | 121 +++++++++++-
rust/fory-derive/src/object/write.rs | 39 ++--
rust/fory-derive/src/util.rs | 16 +-
rust/tests/tests/test_associated_types.rs | 147 ++++++++++++++
rust/tests/tests/test_tuple_struct.rs | 300 +++++++++++++++++++++++++++++
10 files changed, 849 insertions(+), 184 deletions(-)
diff --git a/rust/fory-derive/src/lib.rs b/rust/fory-derive/src/lib.rs
index 69a335ba7..017eb6f12 100644
--- a/rust/fory-derive/src/lib.rs
+++ b/rust/fory-derive/src/lib.rs
@@ -216,12 +216,12 @@ pub fn proc_macro_derive_fory_object(input:
proc_macro::TokenStream) -> TokenStr
// Check if this is being applied to a trait (which is not possible with
derive macros)
// Derive macros can only be applied to structs, enums, and unions
- let (debug_enabled, generate_default) = match
parse_fory_attrs(&input.attrs) {
- Ok(flags) => flags,
+ let attrs = match parse_fory_attrs(&input.attrs) {
+ Ok(attrs) => attrs,
Err(err) => return err.into_compile_error().into(),
};
- object::derive_serializer(&input, debug_enabled, generate_default)
+ object::derive_serializer(&input, attrs)
}
/// Derive macro for row-based serialization.
@@ -249,8 +249,14 @@ pub fn proc_macro_derive_fory_row(input:
proc_macro::TokenStream) -> TokenStream
derive_row(&input)
}
-/// Parse fory attributes and return (debug_enabled, generate_default)
-fn parse_fory_attrs(attrs: &[Attribute]) -> syn::Result<(bool, bool)> {
+/// Parsed fory attributes
+pub(crate) struct ForyAttrs {
+ pub debug_enabled: bool,
+ pub generate_default: bool,
+}
+
+/// Parse fory attributes and return ForyAttrs
+fn parse_fory_attrs(attrs: &[Attribute]) -> syn::Result<ForyAttrs> {
let mut debug_flag: Option<bool> = None;
let mut generate_default_flag: Option<bool> = None;
@@ -297,8 +303,8 @@ fn parse_fory_attrs(attrs: &[Attribute]) ->
syn::Result<(bool, bool)> {
}
}
- Ok((
- debug_flag.unwrap_or(false),
- generate_default_flag.unwrap_or(false),
- ))
+ Ok(ForyAttrs {
+ debug_enabled: debug_flag.unwrap_or(false),
+ generate_default: generate_default_flag.unwrap_or(false),
+ })
}
diff --git a/rust/fory-derive/src/object/derive_enum.rs
b/rust/fory-derive/src/object/derive_enum.rs
index e32fae431..6261c5f9e 100644
--- a/rust/fory-derive/src/object/derive_enum.rs
+++ b/rust/fory-derive/src/object/derive_enum.rs
@@ -488,8 +488,12 @@ fn rust_variant_read_branches(
let read_fields: Vec<TokenStream> = fields_unnamed
.unnamed
.iter()
+ .enumerate()
.zip(field_idents.iter())
- .map(|(f, ident)| gen_read_field(f, ident))
+ .map(|((idx, f), ident)| {
+ let field_name = idx.to_string();
+ gen_read_field(f, ident, &field_name)
+ })
.collect();
quote! {
@@ -511,7 +515,10 @@ fn rust_variant_read_branches(
let read_fields: Vec<_> = sorted_fields
.iter()
.zip(field_idents.iter())
- .map(|(f, ident)| gen_read_field(f, ident))
+ .map(|(f, ident)| {
+ let field_name = ident.to_string();
+ gen_read_field(f, ident, &field_name)
+ })
.collect();
quote! {
diff --git a/rust/fory-derive/src/object/misc.rs
b/rust/fory-derive/src/object/misc.rs
index bcca22605..c0e5a18ba 100644
--- a/rust/fory-derive/src/object/misc.rs
+++ b/rust/fory-derive/src/object/misc.rs
@@ -35,9 +35,9 @@ pub fn allocate_type_id() -> u32 {
#[allow(dead_code)]
fn hash(fields: &[&Field]) -> TokenStream {
- let props = fields.iter().map(|field| {
+ let props = fields.iter().enumerate().map(|(idx, field)| {
let ty = &field.ty;
- let name = format!("{}", field.ident.as_ref().expect("should be field
name"));
+ let name = super::util::get_field_name(field, idx);
quote! {
(#name, <#ty as
fory_core::serializer::Serializer>::fory_get_type_id())
}
@@ -72,61 +72,63 @@ pub fn gen_get_sorted_field_names(fields: &[&Field]) ->
TokenStream {
}
pub fn gen_field_fields_info(fields: &[&Field]) -> TokenStream {
- let field_infos = get_filtered_fields_iter(fields).map(|field| {
- let ty = &field.ty;
- let name = format!("{}", field.ident.as_ref().expect("should be field
name"));
- match classify_trait_object_field(ty) {
- StructField::None => {
- let generic_tree = parse_generic_tree(ty);
- let generic_token = generic_tree_to_tokens(&generic_tree);
- quote! {
- fory_core::meta::FieldInfo::new(#name, #generic_token)
+ let field_infos = get_filtered_fields_iter(fields)
+ .enumerate()
+ .map(|(idx, field)| {
+ let ty = &field.ty;
+ let name = super::util::get_field_name(field, idx);
+ match classify_trait_object_field(ty) {
+ StructField::None => {
+ let generic_tree = parse_generic_tree(ty);
+ let generic_token = generic_tree_to_tokens(&generic_tree);
+ quote! {
+ fory_core::meta::FieldInfo::new(#name, #generic_token)
+ }
}
- }
- StructField::VecBox(_) | StructField::VecRc(_) |
StructField::VecArc(_) => {
- quote! {
- fory_core::meta::FieldInfo::new(#name,
fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::LIST as u32,
- nullable: false,
- generics: vec![fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::UNKNOWN as u32,
+ StructField::VecBox(_) | StructField::VecRc(_) |
StructField::VecArc(_) => {
+ quote! {
+ fory_core::meta::FieldInfo::new(#name,
fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::LIST as u32,
nullable: false,
- generics: Vec::new()
- }]
- })
- }
- }
- StructField::HashMapBox(key_ty, _)
- | StructField::HashMapRc(key_ty, _)
- | StructField::HashMapArc(key_ty, _) => {
- let key_generic_tree = parse_generic_tree(key_ty.as_ref());
- let key_generic_token =
generic_tree_to_tokens(&key_generic_tree);
- quote! {
- fory_core::meta::FieldInfo::new(#name,
fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::MAP as u32,
- nullable: false,
- generics: vec![
- #key_generic_token,
- fory_core::meta::FieldType {
+ generics: vec![fory_core::meta::FieldType {
type_id: fory_core::types::TypeId::UNKNOWN as
u32,
nullable: false,
generics: Vec::new()
- }
- ]
- })
+ }]
+ })
+ }
}
- }
- _ => {
- quote! {
- fory_core::meta::FieldInfo::new(#name,
fory_core::meta::FieldType {
- type_id: fory_core::types::TypeId::UNKNOWN as u32,
- nullable: false,
- generics: Vec::new()
- })
+ StructField::HashMapBox(key_ty, _)
+ | StructField::HashMapRc(key_ty, _)
+ | StructField::HashMapArc(key_ty, _) => {
+ let key_generic_tree = parse_generic_tree(key_ty.as_ref());
+ let key_generic_token =
generic_tree_to_tokens(&key_generic_tree);
+ quote! {
+ fory_core::meta::FieldInfo::new(#name,
fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::MAP as u32,
+ nullable: false,
+ generics: vec![
+ #key_generic_token,
+ fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::UNKNOWN
as u32,
+ nullable: false,
+ generics: Vec::new()
+ }
+ ]
+ })
+ }
+ }
+ _ => {
+ quote! {
+ fory_core::meta::FieldInfo::new(#name,
fory_core::meta::FieldType {
+ type_id: fory_core::types::TypeId::UNKNOWN as u32,
+ nullable: false,
+ generics: Vec::new()
+ })
+ }
}
}
- }
- });
+ });
// Get sorted field names for sorting
let static_field_names = get_sort_fields_ts(fields);
diff --git a/rust/fory-derive/src/object/read.rs
b/rust/fory-derive/src/object/read.rs
index e53a32789..349db4167 100644
--- a/rust/fory-derive/src/object/read.rs
+++ b/rust/fory-derive/src/object/read.rs
@@ -26,8 +26,14 @@ use super::util::{
should_skip_type_info_for_field, skip_ref_flag, StructField,
};
-pub(crate) fn create_private_field_name(field: &Field) -> Ident {
- format_ident!("_{}", field.ident.as_ref().unwrap())
+/// Create a private variable name for a field during deserialization.
+/// For named fields: `_field_name`
+/// For tuple struct fields: `_0`, `_1`, etc.
+pub(crate) fn create_private_field_name(field: &Field, index: usize) -> Ident {
+ match &field.ident {
+ Some(ident) => format_ident!("_{}", ident),
+ None => format_ident!("_{}", index),
+ }
}
fn need_declared_by_option(field: &Field) -> bool {
@@ -38,9 +44,10 @@ fn need_declared_by_option(field: &Field) -> bool {
pub(crate) fn declare_var(fields: &[&Field]) -> Vec<TokenStream> {
fields
.iter()
- .map(|field| {
+ .enumerate()
+ .map(|(idx, field)| {
let ty = &field.ty;
- let var_name = create_private_field_name(field);
+ let var_name = create_private_field_name(field, idx);
match classify_trait_object_field(ty) {
StructField::BoxDyn
| StructField::RcDyn(_)
@@ -70,40 +77,43 @@ pub(crate) fn declare_var(fields: &[&Field]) ->
Vec<TokenStream> {
}
pub(crate) fn assign_value(fields: &[&Field]) -> Vec<TokenStream> {
+ let is_tuple = super::util::is_tuple_struct(fields);
+
fields
.iter()
- .map(|field| {
- let name = &field.ident;
- let var_name = create_private_field_name(field);
- match classify_trait_object_field(&field.ty) {
+ .enumerate()
+ .map(|(idx, field)| {
+ let var_name = create_private_field_name(field, idx);
+ let value_expr = match classify_trait_object_field(&field.ty) {
StructField::BoxDyn | StructField::RcDyn(_) |
StructField::ArcDyn(_) => {
- quote! {
- #name: #var_name
- }
+ quote! { #var_name }
}
StructField::ContainsTraitObject => {
- quote! {
- #name: #var_name.unwrap()
- }
+ quote! { #var_name.unwrap() }
}
_ => {
if need_declared_by_option(field) {
let ty = &field.ty;
- quote! {
- #name: #var_name.unwrap_or_else(|| <#ty as
fory_core::ForyDefault>::fory_default())
- }
+ quote! { #var_name.unwrap_or_else(|| <#ty as
fory_core::ForyDefault>::fory_default()) }
} else {
- quote! {
- #name: #var_name
- }
+ quote! { #var_name }
}
}
+ };
+
+ if is_tuple {
+ // For tuple structs, just return the value
+ value_expr
+ } else {
+ // For named structs, include the field name
+ let name = &field.ident;
+ quote! { #name: #value_expr }
}
})
.collect()
}
-pub fn gen_read_field(field: &Field, private_ident: &Ident) -> TokenStream {
+pub fn gen_read_field(field: &Field, private_ident: &Ident, field_name: &str)
-> TokenStream {
let ty = &field.ty;
if is_skip_field(field) {
return quote! {
@@ -232,8 +242,7 @@ pub fn gen_read_field(field: &Field, private_ident: &Ident)
-> TokenStream {
if is_debug_enabled() {
let struct_name = get_struct_name().expect("struct context not set");
let struct_name_lit = syn::LitStr::new(&struct_name,
proc_macro2::Span::call_site());
- let field_name = field.ident.as_ref().unwrap().to_string();
- let field_name_lit = syn::LitStr::new(&field_name,
proc_macro2::Span::call_site());
+ let field_name_lit = syn::LitStr::new(field_name,
proc_macro2::Span::call_site());
quote! {
fory_core::serializer::struct_::struct_before_read_field(
#struct_name_lit,
@@ -262,9 +271,11 @@ pub fn gen_read_type_info() -> TokenStream {
fn get_fields_loop_ts(fields: &[&Field]) -> TokenStream {
let read_fields_ts: Vec<_> = fields
.iter()
- .map(|field| {
- let private_ident = create_private_field_name(field);
- gen_read_field(field, &private_ident)
+ .enumerate()
+ .map(|(idx, field)| {
+ let private_ident = create_private_field_name(field, idx);
+ let field_name = super::util::get_field_name(field, idx);
+ gen_read_field(field, &private_ident, &field_name)
})
.collect();
quote! {
@@ -282,13 +293,29 @@ pub fn gen_read_data(fields: &[&Field]) -> TokenStream {
#loop_ts
}
};
- let field_idents = fields.iter().map(|field| {
- let private_ident = create_private_field_name(field);
- let original_ident = &field.ident;
- quote! {
- #original_ident: #private_ident
- }
- });
+
+ let is_tuple = super::util::is_tuple_struct(fields);
+ let field_idents: Vec<_> = fields
+ .iter()
+ .enumerate()
+ .map(|(idx, field)| {
+ let private_ident = create_private_field_name(field, idx);
+ if is_tuple {
+ // For tuple structs, just use the variable
+ quote! { #private_ident }
+ } else {
+ // For named structs, include the field name
+ let original_ident = &field.ident;
+ quote! { #original_ident: #private_ident }
+ }
+ })
+ .collect();
+ let self_construction = if is_tuple {
+ quote! { Ok(Self( #(#field_idents),* )) }
+ } else {
+ quote! { Ok(Self { #(#field_idents),* }) }
+ };
+
quote! {
// Read and check version hash when class version checking is enabled
if context.is_check_struct_version() {
@@ -297,13 +324,15 @@ pub fn gen_read_data(fields: &[&Field]) -> TokenStream {
fory_core::meta::TypeMeta::check_struct_version(read_version,
#version_hash, type_name)?;
}
#sorted_read
- Ok(Self {
- #(#field_idents),*
- })
+ #self_construction
}
}
-pub(crate) fn gen_read_compatible_match_arm_body(field: &Field, var_name:
&Ident) -> TokenStream {
+pub(crate) fn gen_read_compatible_match_arm_body(
+ field: &Field,
+ var_name: &Ident,
+ field_name: &str,
+) -> TokenStream {
let ty = &field.ty;
let field_kind = classify_trait_object_field(ty);
let is_skip_flag = is_skip_field(field);
@@ -482,8 +511,7 @@ pub(crate) fn gen_read_compatible_match_arm_body(field:
&Field, var_name: &Ident
if is_debug_enabled() {
let struct_name = get_struct_name().expect("struct context not set");
let struct_name_lit = syn::LitStr::new(&struct_name,
proc_macro2::Span::call_site());
- let field_name = field.ident.as_ref().unwrap().to_string();
- let field_name_lit = syn::LitStr::new(&field_name,
proc_macro2::Span::call_site());
+ let field_name_lit = syn::LitStr::new(field_name,
proc_macro2::Span::call_site());
quote! {
fory_core::serializer::struct_::struct_before_read_field(
#struct_name_lit,
@@ -505,7 +533,10 @@ pub(crate) fn gen_read_compatible_match_arm_body(field:
&Field, var_name: &Ident
}
}
-pub fn gen_read(struct_ident: &Ident) -> TokenStream {
+pub fn gen_read(_struct_ident: &Ident) -> TokenStream {
+ // Note: We use `Self` instead of `#struct_ident` to correctly handle
generic types.
+ // When the struct has generics (e.g., LeaderId<C>), using `Self` ensures
the full
+ // type with generics is used in the impl block.
quote! {
let ref_flag = if read_ref_info {
context.reader.read_i8()?
@@ -520,7 +551,7 @@ pub fn gen_read(struct_ident: &Ident) -> TokenStream {
let rs_type_id = std::any::TypeId::of::<Self>();
context.get_type_info(&rs_type_id)?
};
- <#struct_ident as
fory_core::StructSerializer>::fory_read_compatible(context, type_info)
+ <Self as
fory_core::StructSerializer>::fory_read_compatible(context, type_info)
} else {
if read_type_info {
<Self as
fory_core::Serializer>::fory_read_type_info(context)?;
@@ -535,12 +566,13 @@ pub fn gen_read(struct_ident: &Ident) -> TokenStream {
}
}
-pub fn gen_read_with_type_info(struct_ident: &Ident) -> TokenStream {
+pub fn gen_read_with_type_info() -> TokenStream {
// fn fory_read_with_type_info(
// context: &mut ReadContext,
// read_ref_info: bool,
// type_info: Rc<TypeInfo>,
// ) -> Result<Self, Error>
+ // Note: We use `Self` instead of `#struct_ident` to correctly handle
generic types.
quote! {
let ref_flag = if read_ref_info {
context.reader.read_i8()?
@@ -549,7 +581,7 @@ pub fn gen_read_with_type_info(struct_ident: &Ident) ->
TokenStream {
};
if ref_flag == (fory_core::RefFlag::NotNullValue as i8) || ref_flag ==
(fory_core::RefFlag::RefValue as i8) {
if context.is_compatible() {
- <#struct_ident as
fory_core::StructSerializer>::fory_read_compatible(context, type_info)
+ <Self as
fory_core::StructSerializer>::fory_read_compatible(context, type_info)
} else {
<Self as fory_core::Serializer>::fory_read_data(context)
}
@@ -576,9 +608,10 @@ pub(crate) fn gen_read_compatible_with_construction(
.iter()
.enumerate()
.map(|(i, field)| {
- let var_name = create_private_field_name(field);
+ let var_name = create_private_field_name(field, i);
+ let field_name = super::util::get_field_name(field, i);
let field_id = i as i16;
- let body = gen_read_compatible_match_arm_body(field, &var_name);
+ let body = gen_read_compatible_match_arm_body(field, &var_name,
&field_name);
quote! {
#field_id => {
#body
@@ -627,13 +660,21 @@ pub(crate) fn gen_read_compatible_with_construction(
};
// Generate construction based on whether this is a struct or enum variant
+ let is_tuple = super::util::is_tuple_struct(fields);
let construction = if let Some(variant) = variant_ident {
+ // Enum variants use named syntax (struct variants) or tuple syntax
(tuple variants)
quote! {
Ok(Self::#variant {
#(#assign_ts),*
})
}
+ } else if is_tuple {
+ // Tuple structs use parentheses
+ quote! {
+ Ok(Self( #(#assign_ts),* ))
+ }
} else {
+ // Named structs use braces
quote! {
Ok(Self {
#(#assign_ts),*
diff --git a/rust/fory-derive/src/object/serializer.rs
b/rust/fory-derive/src/object/serializer.rs
index 374745392..e221a0a4d 100644
--- a/rust/fory-derive/src/object/serializer.rs
+++ b/rust/fory-derive/src/object/serializer.rs
@@ -17,6 +17,7 @@
use crate::object::{derive_enum, misc, read, write};
use crate::util::sorted_fields;
+use crate::ForyAttrs;
use proc_macro::TokenStream;
use quote::quote;
use syn::Data;
@@ -36,19 +37,28 @@ fn has_existing_default(ast: &syn::DeriveInput, trait_name:
&str) -> bool {
})
}
-pub fn derive_serializer(
- ast: &syn::DeriveInput,
- debug_enabled: bool,
- generate_default: bool,
-) -> TokenStream {
+pub fn derive_serializer(ast: &syn::DeriveInput, attrs: ForyAttrs) ->
TokenStream {
let name = &ast.ident;
+ let (impl_generics, ty_generics, where_clause) =
ast.generics.split_for_impl();
+
+ // Extract type parameter names from generics (e.g., "C", "T", "E")
+ let type_params: std::collections::HashSet<String> = ast
+ .generics
+ .params
+ .iter()
+ .filter_map(|p| match p {
+ syn::GenericParam::Type(tp) => Some(tp.ident.to_string()),
+ _ => None, // Ignore lifetime and const parameters
+ })
+ .collect();
+
use crate::object::util::{clear_struct_context, set_struct_context};
- set_struct_context(&name.to_string(), debug_enabled);
+ set_struct_context(&name.to_string(), attrs.debug_enabled, type_params);
// Check if ForyDefault is already derived/implemented
let has_existing_default = has_existing_default(ast, "ForyDefault");
let default_impl = if !has_existing_default {
- generate_default_impl(ast, generate_default)
+ generate_default_impl(ast, attrs.generate_default)
} else {
quote! {}
};
@@ -112,7 +122,7 @@ pub fn derive_serializer(
write::gen_write_data(&fields),
write::gen_write_type_info(),
read::gen_read(name),
- read::gen_read_with_type_info(name),
+ read::gen_read_with_type_info(),
read::gen_read_data(&fields),
read::gen_read_type_info(),
write::gen_reserved_space(&fields),
@@ -146,7 +156,7 @@ pub fn derive_serializer(
#default_impl
- impl fory_core::StructSerializer for #name {
+ impl #impl_generics fory_core::StructSerializer for #name #ty_generics
#where_clause {
#[inline(always)]
fn fory_type_index() -> u32 {
#type_idx
@@ -175,7 +185,7 @@ pub fn derive_serializer(
}
}
- impl fory_core::Serializer for #name {
+ impl #impl_generics fory_core::Serializer for #name #ty_generics
#where_clause {
#[inline(always)]
fn fory_get_type_id(type_resolver:
&fory_core::resolver::type_resolver::TypeResolver) -> Result<u32,
fory_core::error::Error> {
type_resolver.get_type_id(&std::any::TypeId::of::<Self>(),
#type_idx)
@@ -251,6 +261,8 @@ fn generate_default_impl(
generate_default: bool,
) -> proc_macro2::TokenStream {
let name = &ast.ident;
+ let (impl_generics, ty_generics, where_clause) =
ast.generics.split_for_impl();
+
// By default, we don't generate Default impl to avoid conflicts.
// Only generate if generate_default is true AND there's no existing
Default.
let should_generate_default = generate_default &&
!has_existing_default(ast, "Default");
@@ -258,63 +270,87 @@ fn generate_default_impl(
match &ast.data {
Data::Struct(s) => {
let fields = sorted_fields(&s.fields);
+ let is_tuple_struct = super::util::is_tuple_struct(&fields);
use super::util::{
classify_trait_object_field, create_wrapper_types_arc,
create_wrapper_types_rc,
StructField,
};
- let field_inits = fields.iter().map(|field| {
- let ident = &field.ident;
- let ty = &field.ty;
-
- match classify_trait_object_field(ty) {
- StructField::RcDyn(trait_name) => {
- let types = create_wrapper_types_rc(&trait_name);
- let wrapper_ty = types.wrapper_ty;
- let trait_ident = types.trait_ident;
- quote! {
- #ident: {
- let wrapper = #wrapper_ty::default();
- std::rc::Rc::<dyn #trait_ident>::from(wrapper)
+ let field_inits: Vec<_> = fields
+ .iter()
+ .map(|field| {
+ let ident = &field.ident;
+ let ty = &field.ty;
+
+ match classify_trait_object_field(ty) {
+ StructField::RcDyn(trait_name) => {
+ let types = create_wrapper_types_rc(&trait_name);
+ let wrapper_ty = types.wrapper_ty;
+ let trait_ident = types.trait_ident;
+ let value = quote! {
+ {
+ let wrapper = #wrapper_ty::default();
+ std::rc::Rc::<dyn
#trait_ident>::from(wrapper)
+ }
+ };
+ if is_tuple_struct {
+ value
+ } else {
+ quote! { #ident: #value }
}
}
- }
- StructField::ArcDyn(trait_name) => {
- let types = create_wrapper_types_arc(&trait_name);
- let wrapper_ty = types.wrapper_ty;
- let trait_ident = types.trait_ident;
- quote! {
- #ident: {
- let wrapper = #wrapper_ty::default();
- std::sync::Arc::<dyn
#trait_ident>::from(wrapper)
+ StructField::ArcDyn(trait_name) => {
+ let types = create_wrapper_types_arc(&trait_name);
+ let wrapper_ty = types.wrapper_ty;
+ let trait_ident = types.trait_ident;
+ let value = quote! {
+ {
+ let wrapper = #wrapper_ty::default();
+ std::sync::Arc::<dyn
#trait_ident>::from(wrapper)
+ }
+ };
+ if is_tuple_struct {
+ value
+ } else {
+ quote! { #ident: #value }
}
}
- }
- StructField::Forward => {
- quote! {
- #ident: <#ty as
fory_core::ForyDefault>::fory_default()
+ StructField::Forward => {
+ let value = quote! { <#ty as
fory_core::ForyDefault>::fory_default() };
+ if is_tuple_struct {
+ value
+ } else {
+ quote! { #ident: #value }
+ }
}
- }
- _ => {
- quote! {
- #ident: <#ty as
fory_core::ForyDefault>::fory_default()
+ _ => {
+ let value = quote! { <#ty as
fory_core::ForyDefault>::fory_default() };
+ if is_tuple_struct {
+ value
+ } else {
+ quote! { #ident: #value }
+ }
}
}
- }
- });
+ })
+ .collect();
+
+ let self_construction = if is_tuple_struct {
+ quote! { Self( #(#field_inits),* ) }
+ } else {
+ quote! { Self { #(#field_inits),* } }
+ };
if should_generate_default {
// User requested Default generation via
#[fory(generate_default)]
quote! {
- impl fory_core::ForyDefault for #name {
+ impl #impl_generics fory_core::ForyDefault for #name
#ty_generics #where_clause {
fn fory_default() -> Self {
- Self {
- #(#field_inits),*
- }
+ #self_construction
}
}
- impl std::default::Default for #name {
+ impl #impl_generics std::default::Default for #name
#ty_generics #where_clause {
fn default() -> Self {
Self::fory_default()
}
@@ -324,11 +360,9 @@ fn generate_default_impl(
// Default case: only generate ForyDefault, not Default
// This avoids conflicts with existing Default implementations
quote! {
- impl fory_core::ForyDefault for #name {
+ impl #impl_generics fory_core::ForyDefault for #name
#ty_generics #where_clause {
fn fory_default() -> Self {
- Self {
- #(#field_inits),*
- }
+ #self_construction
}
}
}
@@ -369,7 +403,7 @@ fn generate_default_impl(
// User has #[derive(Default)] or #[default] attribute
// Only generate ForyDefault that delegates to Default
quote! {
- impl fory_core::ForyDefault for #name {
+ impl #impl_generics fory_core::ForyDefault for #name
#ty_generics #where_clause {
fn fory_default() -> Self {
Self::default()
}
@@ -378,13 +412,13 @@ fn generate_default_impl(
} else if should_generate_default {
// User requested Default generation via
#[fory(generate_default)]
quote! {
- impl fory_core::ForyDefault for #name {
+ impl #impl_generics fory_core::ForyDefault for #name
#ty_generics #where_clause {
fn fory_default() -> Self {
Self::#variant_ident #field_defaults
}
}
- impl std::default::Default for #name {
+ impl #impl_generics std::default::Default for #name
#ty_generics #where_clause {
fn default() -> Self {
Self::#variant_ident #field_defaults
}
@@ -393,7 +427,7 @@ fn generate_default_impl(
} else {
// Default case: only generate ForyDefault, not Default
quote! {
- impl fory_core::ForyDefault for #name {
+ impl #impl_generics fory_core::ForyDefault for #name
#ty_generics #where_clause {
fn fory_default() -> Self {
Self::#variant_ident #field_defaults
}
diff --git a/rust/fory-derive/src/object/util.rs
b/rust/fory-derive/src/object/util.rs
index 1389dfeb9..fbd9e8c60 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -26,7 +26,41 @@ use quote::{format_ident, quote, ToTokens};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
-use syn::{Field, GenericArgument, PathArguments, Type};
+use syn::{Field, GenericArgument, Index, PathArguments, Type};
+
+/// Get field name for a field, handling both named and tuple struct fields.
+/// For named fields, returns the field name.
+/// For tuple struct fields, returns the index as a string (e.g., "0", "1").
+pub(super) fn get_field_name(field: &Field, index: usize) -> String {
+ match &field.ident {
+ Some(ident) => ident.to_string(),
+ None => index.to_string(),
+ }
+}
+
+/// Get the field accessor token for a field.
+/// For named fields: `self.field_name`
+/// For tuple struct fields: `self.0`, `self.1`, etc.
+pub(super) fn get_field_accessor(field: &Field, index: usize, use_self: bool)
-> TokenStream {
+ let prefix = if use_self {
+ quote! { self. }
+ } else {
+ quote! {}
+ };
+
+ match &field.ident {
+ Some(ident) => quote! { #prefix #ident },
+ None => {
+ let idx = Index::from(index);
+ quote! { #prefix #idx }
+ }
+ }
+}
+
+/// Check if this is a tuple struct (all fields are unnamed)
+pub fn is_tuple_struct(fields: &[&Field]) -> bool {
+ !fields.is_empty() && fields[0].ident.is_none()
+}
thread_local! {
static MACRO_CONTEXT: RefCell<Option<MacroContext>> = const
{RefCell::new(None)};
@@ -36,13 +70,24 @@ thread_local! {
struct MacroContext {
struct_name: String,
debug_enabled: bool,
+ /// Type parameter names extracted from the struct/enum generics (e.g.,
"C", "T", "E")
+ type_params: std::collections::HashSet<String>,
}
-pub(super) fn set_struct_context(name: &str, debug_enabled: bool) {
+/// Set the macro context with struct name, debug flag, and type parameters.
+///
+/// `type_params` should contain the names of all type parameters from the
struct/enum
+/// generics (e.g., for `struct Vote<C: RaftTypeConfig>`, pass `{"C"}`).
+pub(super) fn set_struct_context(
+ name: &str,
+ debug_enabled: bool,
+ type_params: std::collections::HashSet<String>,
+) {
MACRO_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = Some(MacroContext {
struct_name: name.to_string(),
debug_enabled,
+ type_params,
});
});
}
@@ -66,6 +111,16 @@ pub(super) fn is_debug_enabled() -> bool {
})
}
+/// Check if a type name is a type parameter of the current struct/enum.
+pub(super) fn is_type_parameter(name: &str) -> bool {
+ MACRO_CONTEXT.with(|ctx| {
+ ctx.borrow()
+ .as_ref()
+ .map(|c| c.type_params.contains(name))
+ .unwrap_or(false)
+ })
+}
+
pub(super) fn contains_trait_object(ty: &Type) -> bool {
match ty {
Type::TraitObject(_) => true,
@@ -233,7 +288,10 @@ pub(super) fn classify_trait_object_field(ty: &Type) ->
StructField {
#[derive(Debug)]
pub(super) struct TypeNode {
+ /// Simple type name, used for type matching (e.g., "LeaderId", "Vec")
pub name: String,
+ /// Full type path string, used for code generation (e.g., "C::LeaderId")
+ pub full_path: String,
pub generics: Vec<TypeNode>,
/// For arrays, store the original type string "[T; N]" to preserve length
info
pub original_type_str: Option<String>,
@@ -294,12 +352,14 @@ impl fmt::Display for TypeNode {
write!(f, "Array")
}
} else if self.generics.is_empty() {
- write!(f, "{}", self.name)
+ // Use full_path to preserve associated type paths like C::LeaderId
+ write!(f, "{}", self.full_path)
} else {
+ // Use full_path for the base type, recursively format generics
write!(
f,
"{}<{}>",
- self.name,
+ self.full_path,
self.generics
.iter()
.map(|g| g.to_string())
@@ -324,6 +384,28 @@ pub(super) fn extract_type_name(ty: &Type) -> String {
}
}
+/// Extracts the full type path string, preserving associated types like
`C::LeaderId`
+pub(super) fn extract_full_type_path(ty: &Type) -> String {
+ if let Type::Path(type_path) = ty {
+ // Build the full path from all segments without generic arguments
+ type_path
+ .path
+ .segments
+ .iter()
+ .map(|seg| seg.ident.to_string())
+ .collect::<Vec<_>>()
+ .join("::")
+ } else if matches!(ty, Type::TraitObject(_)) {
+ "TraitObject".to_string()
+ } else if matches!(ty, Type::Tuple(_)) {
+ "Tuple".to_string()
+ } else if matches!(ty, Type::Array(_)) {
+ "Array".to_string()
+ } else {
+ quote!(#ty).to_string()
+ }
+}
+
#[allow(dead_code)]
pub(super) fn is_option(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
@@ -341,6 +423,7 @@ pub(super) fn parse_generic_tree(ty: &Type) -> TypeNode {
if matches!(ty, Type::TraitObject(_)) {
return TypeNode {
name: "TraitObject".to_string(),
+ full_path: "TraitObject".to_string(),
generics: vec![],
original_type_str: None,
};
@@ -350,6 +433,7 @@ pub(super) fn parse_generic_tree(ty: &Type) -> TypeNode {
if let Type::Tuple(_tuple) = ty {
return TypeNode {
name: "Tuple".to_string(),
+ full_path: "Tuple".to_string(),
generics: vec![],
original_type_str: None,
};
@@ -362,12 +446,14 @@ pub(super) fn parse_generic_tree(ty: &Type) -> TypeNode {
let original_type_str = quote!(#ty).to_string().replace(' ', "");
return TypeNode {
name: "Array".to_string(),
+ full_path: "Array".to_string(),
generics: vec![elem_node],
original_type_str: Some(original_type_str),
};
}
let name = extract_type_name(ty);
+ let full_path = extract_full_type_path(ty);
let generics = if let Type::Path(type_path) = ty {
if let PathArguments::AngleBracketed(args) =
@@ -391,12 +477,27 @@ pub(super) fn parse_generic_tree(ty: &Type) -> TypeNode {
};
TypeNode {
name,
+ full_path,
generics,
original_type_str: None,
}
}
pub(super) fn generic_tree_to_tokens(node: &TypeNode) -> TokenStream {
+ // Special handling for type parameters (e.g., C, T, E)
+ // Type parameters should use UNKNOWN type ID since they are not concrete
types.
+ // This prevents `C::LeaderId` generating code like `<C as
Serializer>::fory_get_type_id()` which
+ // would require the type parameter to implement Serializer.
+ if is_type_parameter(&node.name) {
+ return quote! {
+ fory_core::meta::FieldType::new(
+ fory_core::types::TypeId::UNKNOWN as u32,
+ true,
+ vec![]
+ )
+ };
+ }
+
// Special handling for tuples: always use FieldType { LIST, nullable:
true, generics: vec![UNKNOWN] }
if node.name == "Tuple" {
return quote! {
@@ -817,9 +918,9 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups {
let mut other_fields = Vec::new();
// First handle Forward fields separately to avoid borrow checker issues
- for field in fields {
+ for (idx, field) in fields.iter().enumerate() {
if is_forward_field(&field.ty) {
- let raw_ident = field.ident.as_ref().unwrap().to_string();
+ let raw_ident = get_field_name(field, idx);
let ident = to_snake_case(&raw_ident);
other_fields.push((ident, "Forward".to_string(), TypeId::UNKNOWN
as u32));
}
@@ -844,8 +945,8 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups {
}
};
- for field in fields {
- let raw_ident = field.ident.as_ref().unwrap().to_string();
+ for (idx, field) in fields.iter().enumerate() {
+ let raw_ident = get_field_name(field, idx);
let ident = to_snake_case(&raw_ident);
// Skip if already handled as Forward field
@@ -1013,8 +1114,8 @@ fn to_snake_case(name: &str) -> String {
/// This format is consistent across Go, Java, Rust, and C++ implementations.
pub(crate) fn compute_struct_fingerprint(fields: &[&Field]) -> String {
let mut field_info_map: HashMap<String, (u32, bool)> =
HashMap::with_capacity(fields.len());
- for field in fields {
- let name = field.ident.as_ref().unwrap().to_string();
+ for (idx, field) in fields.iter().enumerate() {
+ let name = get_field_name(field, idx);
let type_id = get_type_id_by_type_ast(&field.ty);
// Match Java's behavior: primitives are non-nullable, everything else
is nullable
// In Java: char nullable = rawType.isPrimitive() ? '0' : '1';
diff --git a/rust/fory-derive/src/object/write.rs
b/rust/fory-derive/src/object/write.rs
index c43a4898c..d7ba72a4a 100644
--- a/rust/fory-derive/src/object/write.rs
+++ b/rust/fory-derive/src/object/write.rs
@@ -17,9 +17,10 @@
use super::util::{
classify_trait_object_field, compute_struct_version_hash,
create_wrapper_types_arc,
- create_wrapper_types_rc, extract_type_name, get_filtered_fields_iter,
- get_primitive_writer_method, get_struct_name, get_type_id_by_type_ast,
is_debug_enabled,
- is_direct_primitive_numeric_type, should_skip_type_info_for_field,
skip_ref_flag, StructField,
+ create_wrapper_types_rc, extract_type_name, get_field_accessor,
get_field_name,
+ get_filtered_fields_iter, get_primitive_writer_method, get_struct_name,
+ get_type_id_by_type_ast, is_debug_enabled,
is_direct_primitive_numeric_type,
+ should_skip_type_info_for_field, skip_ref_flag, StructField,
};
use fory_core::types::TypeId;
use proc_macro2::{Ident, TokenStream};
@@ -114,13 +115,30 @@ pub fn gen_write_type_info() -> TokenStream {
}
}
+/// Generate write code for a field using index-based access (supports tuple
structs)
+pub fn gen_write_field_with_index(field: &Field, index: usize, use_self: bool)
-> TokenStream {
+ let value_ts = get_field_accessor(field, index, use_self);
+ let field_name = get_field_name(field, index);
+ gen_write_field_impl(field, &value_ts, &field_name, use_self)
+}
+
pub fn gen_write_field(field: &Field, ident: &Ident, use_self: bool) ->
TokenStream {
- let ty = &field.ty;
let value_ts = if use_self {
quote! { self.#ident }
} else {
quote! { #ident }
};
+ let field_name = ident.to_string();
+ gen_write_field_impl(field, &value_ts, &field_name, use_self)
+}
+
+fn gen_write_field_impl(
+ field: &Field,
+ value_ts: &TokenStream,
+ field_name: &str,
+ use_self: bool,
+) -> TokenStream {
+ let ty = &field.ty;
let base = match classify_trait_object_field(ty) {
StructField::BoxDyn => {
quote! {
@@ -266,20 +284,19 @@ pub fn gen_write_field(field: &Field, ident: &Ident,
use_self: bool) -> TokenStr
if is_debug_enabled() && use_self {
let struct_name = get_struct_name().expect("struct context not set");
let struct_name_lit = syn::LitStr::new(&struct_name,
proc_macro2::Span::call_site());
- let field_name = field.ident.as_ref().unwrap().to_string();
- let field_name_lit = syn::LitStr::new(&field_name,
proc_macro2::Span::call_site());
+ let field_name_lit = syn::LitStr::new(field_name,
proc_macro2::Span::call_site());
quote! {
fory_core::serializer::struct_::struct_before_write_field(
#struct_name_lit,
#field_name_lit,
- (&self.#ident) as &dyn std::any::Any,
+ (&#value_ts) as &dyn std::any::Any,
context,
);
#base
fory_core::serializer::struct_::struct_after_write_field(
#struct_name_lit,
#field_name_lit,
- (&self.#ident) as &dyn std::any::Any,
+ (&#value_ts) as &dyn std::any::Any,
context,
);
}
@@ -290,10 +307,8 @@ pub fn gen_write_field(field: &Field, ident: &Ident,
use_self: bool) -> TokenStr
pub fn gen_write_data(fields: &[&Field]) -> TokenStream {
let write_fields_ts: Vec<_> = get_filtered_fields_iter(fields)
- .map(|field| {
- let ident = field.ident.as_ref().unwrap();
- gen_write_field(field, ident, true)
- })
+ .enumerate()
+ .map(|(idx, field)| gen_write_field_with_index(field, idx, true))
.collect();
let version_hash = compute_struct_version_hash(fields);
diff --git a/rust/fory-derive/src/util.rs b/rust/fory-derive/src/util.rs
index 5f629e8a1..ffd84d880 100644
--- a/rust/fory-derive/src/util.rs
+++ b/rust/fory-derive/src/util.rs
@@ -23,13 +23,25 @@ pub fn sorted_fields(fields: &Fields) -> Vec<&Field> {
}
pub fn get_sorted_fields<'a>(fields: &[&'a Field]) -> Vec<&'a Field> {
- use crate::object::util::get_sorted_field_names;
+ use crate::object::util::{get_sorted_field_names, is_tuple_struct};
+ // For tuple structs, we must preserve the original field order
+ // because fields are accessed by index (self.0, self.1, etc.)
+ // Sorting would cause type mismatches during
serialization/deserialization.
+ if is_tuple_struct(fields) {
+ return fields.to_vec();
+ }
+
+ // For named structs, sort fields by type for optimal memory layout
let sorted_names = get_sorted_field_names(fields);
let mut sorted_fields = Vec::with_capacity(fields.len());
for name in &sorted_names {
- if let Some(field) = fields.iter().find(|f| *f.ident.as_ref().unwrap()
== name) {
+ // For named structs, field.ident is Some
+ if let Some(field) = fields
+ .iter()
+ .find(|f| f.ident.as_ref().map(|ident| ident ==
name).unwrap_or(false))
+ {
sorted_fields.push(*field);
}
}
diff --git a/rust/tests/tests/test_associated_types.rs
b/rust/tests/tests/test_associated_types.rs
new file mode 100644
index 000000000..de48a4191
--- /dev/null
+++ b/rust/tests/tests/test_associated_types.rs
@@ -0,0 +1,147 @@
+// 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.
+
+//! Tests for structs with associated types (e.g., `C::NodeId`)
+
+use fory_core::fory::Fory;
+use fory_core::{ForyDefault, Serializer};
+use fory_derive::ForyObject;
+
+/// A trait that defines associated types, similar to OpenRaft's RaftTypeConfig
+pub trait TypeConfig: Sized + Send + Sync + 'static {
+ type NodeId: Clone
+ + Default
+ + PartialEq
+ + std::fmt::Debug
+ + Send
+ + Sync
+ + Serializer
+ + ForyDefault
+ + 'static;
+ type Term: Clone
+ + Default
+ + PartialEq
+ + std::fmt::Debug
+ + Send
+ + Sync
+ + Serializer
+ + ForyDefault
+ + 'static;
+}
+
+/// A concrete implementation of TypeConfig
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub struct TestConfig;
+
+impl TypeConfig for TestConfig {
+ type NodeId = u64;
+ type Term = u64;
+}
+
+/// A struct with fields using associated types
+/// The where clause ensures all associated types implement required traits
+#[derive(ForyObject, Debug, Clone, PartialEq)]
+pub struct LeaderId<C>
+where
+ C: TypeConfig,
+ C::NodeId: Serializer + ForyDefault,
+ C::Term: Serializer + ForyDefault,
+{
+ pub term: C::Term,
+ pub node_id: C::NodeId,
+}
+
+#[test]
+fn test_leader_id_with_associated_types() {
+ let mut fory = Fory::default();
+ fory.register::<LeaderId<TestConfig>>(100).unwrap();
+
+ let leader_id: LeaderId<TestConfig> = LeaderId {
+ term: 1,
+ node_id: 42,
+ };
+
+ let bytes = fory.serialize(&leader_id).unwrap();
+ let deserialized: LeaderId<TestConfig> = fory.deserialize(&bytes).unwrap();
+
+ assert_eq!(leader_id, deserialized);
+}
+
+#[test]
+fn test_leader_id_default_values() {
+ let mut fory = Fory::default();
+ fory.register::<LeaderId<TestConfig>>(100).unwrap();
+
+ let leader_id: LeaderId<TestConfig> = LeaderId {
+ term: 0,
+ node_id: 0,
+ };
+
+ let bytes = fory.serialize(&leader_id).unwrap();
+ let deserialized: LeaderId<TestConfig> = fory.deserialize(&bytes).unwrap();
+
+ assert_eq!(leader_id, deserialized);
+}
+
+#[test]
+fn test_vec_of_leader_ids() {
+ let mut fory = Fory::default();
+ fory.register::<LeaderId<TestConfig>>(100).unwrap();
+
+ let leader_ids: Vec<LeaderId<TestConfig>> = vec![
+ LeaderId {
+ term: 1,
+ node_id: 1,
+ },
+ LeaderId {
+ term: 2,
+ node_id: 2,
+ },
+ LeaderId {
+ term: 3,
+ node_id: 3,
+ },
+ ];
+
+ let bytes = fory.serialize(&leader_ids).unwrap();
+ let deserialized: Vec<LeaderId<TestConfig>> =
fory.deserialize(&bytes).unwrap();
+
+ assert_eq!(leader_ids, deserialized);
+}
+
+#[test]
+fn test_option_leader_id() {
+ let mut fory = Fory::default();
+ fory.register::<LeaderId<TestConfig>>(100).unwrap();
+
+ // Test with Some value
+ let some_leader: Option<LeaderId<TestConfig>> = Some(LeaderId {
+ term: 5,
+ node_id: 10,
+ });
+
+ let bytes = fory.serialize(&some_leader).unwrap();
+ let deserialized: Option<LeaderId<TestConfig>> =
fory.deserialize(&bytes).unwrap();
+ assert_eq!(some_leader, deserialized);
+
+ // Test with None value
+ let none_leader: Option<LeaderId<TestConfig>> = None;
+
+ let bytes = fory.serialize(&none_leader).unwrap();
+ let deserialized: Option<LeaderId<TestConfig>> =
fory.deserialize(&bytes).unwrap();
+ assert_eq!(none_leader, deserialized);
+}
diff --git a/rust/tests/tests/test_tuple_struct.rs
b/rust/tests/tests/test_tuple_struct.rs
new file mode 100644
index 000000000..8c6512130
--- /dev/null
+++ b/rust/tests/tests/test_tuple_struct.rs
@@ -0,0 +1,300 @@
+// 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.
+
+//! Tests for tuple struct serialization with #[derive(ForyObject)]
+//!
+//! Tuple structs are structs with unnamed fields, like:
+//! - `struct Point(f64, f64);`
+//! - `struct Wrapper(String);`
+
+use fory_core::fory::Fory;
+use fory_derive::ForyObject;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+// Basic Tuple Structs
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct Point(f64, f64);
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct Wrapper(String);
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct Triple(i32, i64, u32);
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct Single(i32);
+
+#[test]
+fn test_basic_tuple_struct() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+
+ let point = Point(3.15, 2.72);
+ let bytes = fory.serialize(&point).unwrap();
+ let result: Point = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, point);
+}
+
+#[test]
+fn test_single_field_tuple_struct() {
+ let mut fory = Fory::default();
+ fory.register::<Single>(101).unwrap();
+
+ let single = Single(42);
+ let bytes = fory.serialize(&single).unwrap();
+ let result: Single = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, single);
+}
+
+#[test]
+fn test_string_wrapper_tuple_struct() {
+ let mut fory = Fory::default();
+ fory.register::<Wrapper>(102).unwrap();
+
+ let wrapper = Wrapper("hello world".to_string());
+ let bytes = fory.serialize(&wrapper).unwrap();
+ let result: Wrapper = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, wrapper);
+}
+
+#[test]
+fn test_triple_tuple_struct() {
+ let mut fory = Fory::default();
+ fory.register::<Triple>(103).unwrap();
+
+ let triple = Triple(1, 2, 3);
+ let bytes = fory.serialize(&triple).unwrap();
+ let result: Triple = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, triple);
+}
+
+// Tuple Structs with Complex Types
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct WithVec(Vec<i32>, String);
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct WithOption(Option<i32>, Option<String>);
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct WithMap(HashMap<String, i32>);
+
+#[test]
+fn test_tuple_struct_with_vec() {
+ let mut fory = Fory::default();
+ fory.register::<WithVec>(104).unwrap();
+
+ let data = WithVec(vec![1, 2, 3, 4, 5], "test".to_string());
+ let bytes = fory.serialize(&data).unwrap();
+ let result: WithVec = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, data);
+}
+
+#[test]
+fn test_tuple_struct_with_option() {
+ let mut fory = Fory::default();
+ fory.register::<WithOption>(105).unwrap();
+
+ // Test with Some values
+ let data1 = WithOption(Some(42), Some("hello".to_string()));
+ let bytes1 = fory.serialize(&data1).unwrap();
+ let result1: WithOption = fory.deserialize(&bytes1).unwrap();
+ assert_eq!(result1, data1);
+
+ // Test with None values
+ let data2 = WithOption(None, None);
+ let bytes2 = fory.serialize(&data2).unwrap();
+ let result2: WithOption = fory.deserialize(&bytes2).unwrap();
+ assert_eq!(result2, data2);
+
+ // Test with mixed values
+ let data3 = WithOption(Some(100), None);
+ let bytes3 = fory.serialize(&data3).unwrap();
+ let result3: WithOption = fory.deserialize(&bytes3).unwrap();
+ assert_eq!(result3, data3);
+}
+
+#[test]
+fn test_tuple_struct_with_map() {
+ let mut fory = Fory::default();
+ fory.register::<WithMap>(106).unwrap();
+
+ let mut map = HashMap::new();
+ map.insert("one".to_string(), 1);
+ map.insert("two".to_string(), 2);
+ map.insert("three".to_string(), 3);
+
+ let data = WithMap(map);
+ let bytes = fory.serialize(&data).unwrap();
+ let result: WithMap = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, data);
+}
+
+// Nested Tuple Structs
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct Inner(i32, String);
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct Outer(Inner, Vec<Inner>);
+
+#[test]
+fn test_nested_tuple_structs() {
+ let mut fory = Fory::default();
+ fory.register::<Inner>(107).unwrap();
+ fory.register::<Outer>(108).unwrap();
+
+ let inner1 = Inner(1, "first".to_string());
+ let inner2 = Inner(2, "second".to_string());
+ let inner3 = Inner(3, "third".to_string());
+
+ let outer = Outer(inner1.clone(), vec![inner2, inner3]);
+ let bytes = fory.serialize(&outer).unwrap();
+ let result: Outer = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, outer);
+}
+
+// Tuple Struct with Rc (shared reference)
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct WithRc(Rc<String>, Rc<i32>);
+
+#[test]
+fn test_tuple_struct_with_rc() {
+ let mut fory = Fory::default();
+ fory.register::<WithRc>(109).unwrap();
+
+ let data = WithRc(Rc::new("shared".to_string()), Rc::new(42));
+ let bytes = fory.serialize(&data).unwrap();
+ let result: WithRc = fory.deserialize(&bytes).unwrap();
+ assert_eq!(*result.0, "shared");
+ assert_eq!(*result.1, 42);
+}
+
+// Mixed: Tuple Struct inside Named Struct
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct NamedWithTupleStruct {
+ id: i32,
+ point: Point,
+ wrapper: Wrapper,
+}
+
+#[test]
+fn test_named_struct_with_tuple_struct_fields() {
+ let mut fory = Fory::default();
+ fory.register::<Point>(100).unwrap();
+ fory.register::<Wrapper>(102).unwrap();
+ fory.register::<NamedWithTupleStruct>(110).unwrap();
+
+ let data = NamedWithTupleStruct {
+ id: 1,
+ point: Point(1.5, 2.5),
+ wrapper: Wrapper("test".to_string()),
+ };
+
+ let bytes = fory.serialize(&data).unwrap();
+ let result: NamedWithTupleStruct = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, data);
+}
+
+// Tuple Struct with Tuple field
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct TupleStructWithTuple(i32, (String, f64));
+
+#[test]
+fn test_tuple_struct_with_tuple_field() {
+ let mut fory = Fory::default();
+ fory.register::<TupleStructWithTuple>(111).unwrap();
+
+ let data = TupleStructWithTuple(42, ("hello".to_string(), 3.15));
+ let bytes = fory.serialize(&data).unwrap();
+ let result: TupleStructWithTuple = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, data);
+}
+
+// xlang mode tests
+
+#[test]
+fn test_tuple_struct_xlang_mode() {
+ let mut fory = Fory::default().xlang(true);
+ fory.register::<Point>(100).unwrap();
+ fory.register::<Wrapper>(102).unwrap();
+ fory.register::<Triple>(103).unwrap();
+
+ let point = Point(3.15, 2.72);
+ let bytes = fory.serialize(&point).unwrap();
+ let result: Point = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, point);
+
+ let wrapper = Wrapper("xlang test".to_string());
+ let bytes = fory.serialize(&wrapper).unwrap();
+ let result: Wrapper = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, wrapper);
+
+ let triple = Triple(-100, 9999999999i64, 200);
+ let bytes = fory.serialize(&triple).unwrap();
+ let result: Triple = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, triple);
+}
+
+// Edge cases
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct EmptyVecTuple(Vec<i32>);
+
+#[test]
+fn test_tuple_struct_with_empty_vec() {
+ let mut fory = Fory::default();
+ fory.register::<EmptyVecTuple>(112).unwrap();
+
+ let data = EmptyVecTuple(vec![]);
+ let bytes = fory.serialize(&data).unwrap();
+ let result: EmptyVecTuple = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, data);
+}
+
+#[derive(ForyObject, Debug, PartialEq, Clone)]
+struct LargeTupleStruct(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool,
String);
+
+#[test]
+fn test_large_tuple_struct() {
+ let mut fory = Fory::default();
+ fory.register::<LargeTupleStruct>(113).unwrap();
+
+ let data = LargeTupleStruct(
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9.0,
+ 10.0,
+ true,
+ "twelve".to_string(),
+ );
+
+ let bytes = fory.serialize(&data).unwrap();
+ let result: LargeTupleStruct = fory.deserialize(&bytes).unwrap();
+ assert_eq!(result, data);
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]