| // 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 crate::util::{ |
| detect_collection_with_trait_object, is_arc_dyn_trait, is_box_dyn_trait, is_rc_dyn_trait, |
| CollectionTraitInfo, |
| }; |
| use fory_core::types::{TypeId, PRIMITIVE_ARRAY_TYPE_MAP}; |
| use fory_core::util::to_snake_case; |
| use proc_macro2::{Ident, TokenStream}; |
| use quote::{format_ident, quote, ToTokens}; |
| use std::cell::RefCell; |
| use std::fmt; |
| 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)}; |
| } |
| |
| #[derive(Clone)] |
| 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>, |
| } |
| |
| /// 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, |
| }); |
| }); |
| } |
| |
| pub(super) fn clear_struct_context() { |
| MACRO_CONTEXT.with(|ctx| { |
| *ctx.borrow_mut() = None; |
| }); |
| } |
| |
| pub(super) fn get_struct_name() -> Option<String> { |
| MACRO_CONTEXT.with(|ctx| ctx.borrow().as_ref().map(|c| c.struct_name.clone())) |
| } |
| |
| pub(super) fn is_debug_enabled() -> bool { |
| MACRO_CONTEXT.with(|ctx| { |
| ctx.borrow() |
| .as_ref() |
| .map(|c| c.debug_enabled) |
| .unwrap_or(false) |
| }) |
| } |
| |
| /// 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, |
| Type::Path(type_path) => { |
| if is_box_dyn_trait(ty).is_some() |
| || is_rc_dyn_trait(ty).is_some() |
| || is_arc_dyn_trait(ty).is_some() |
| { |
| return true; |
| } |
| |
| if let Some(seg) = type_path.path.segments.last() { |
| if let PathArguments::AngleBracketed(args) = &seg.arguments { |
| return args.args.iter().any(|arg| { |
| if let GenericArgument::Type(inner_ty) = arg { |
| contains_trait_object(inner_ty) |
| } else { |
| false |
| } |
| }); |
| } |
| } |
| false |
| } |
| _ => false, |
| } |
| } |
| |
| pub(super) struct WrapperTypes { |
| pub wrapper_ty: Ident, |
| pub trait_ident: Ident, |
| } |
| |
| pub(super) fn create_wrapper_types_rc(trait_name: &str) -> WrapperTypes { |
| WrapperTypes { |
| wrapper_ty: format_ident!("{}Rc", trait_name), |
| trait_ident: format_ident!("{}", trait_name), |
| } |
| } |
| |
| pub(super) fn create_wrapper_types_arc(trait_name: &str) -> WrapperTypes { |
| WrapperTypes { |
| wrapper_ty: format_ident!("{}Arc", trait_name), |
| trait_ident: format_ident!("{}", trait_name), |
| } |
| } |
| |
| #[allow(dead_code)] |
| pub(super) enum StructField { |
| BoxDyn, |
| RcDyn(String), |
| ArcDyn(String), |
| VecBox(String), |
| VecRc(String), |
| VecArc(String), |
| HashMapBox(Box<Type>, String), |
| HashMapRc(Box<Type>, String), |
| HashMapArc(Box<Type>, String), |
| ContainsTraitObject, |
| Forward, |
| None, |
| } |
| |
| fn is_forward_field(ty: &Type) -> bool { |
| let struct_name = match get_struct_name() { |
| Some(name) => name, |
| None => return false, |
| }; |
| is_forward_field_internal(ty, &struct_name) |
| } |
| |
| fn is_forward_field_internal(ty: &Type, struct_name: &str) -> bool { |
| match ty { |
| Type::TraitObject(_) => true, |
| |
| Type::Path(type_path) => { |
| if let Some(seg) = type_path.path.segments.last() { |
| // Direct match: type is the struct itself |
| if seg.ident == struct_name { |
| return true; |
| } |
| |
| // Special cases for weak pointers |
| if seg.ident == "RcWeak" || seg.ident == "ArcWeak" { |
| return true; |
| } |
| |
| // Check smart pointers: Rc<T> / Arc<T> |
| // Only return true if: |
| // 1. Inner type is Rc<dyn Any> (polymorphic) |
| // 2. Inner type references the containing struct (forward reference) |
| if seg.ident == "Rc" || seg.ident == "Arc" { |
| if let PathArguments::AngleBracketed(args) = &seg.arguments { |
| if let Some(GenericArgument::Type(inner_ty)) = args.args.first() { |
| match inner_ty { |
| // Inner type is trait object |
| Type::TraitObject(trait_obj) => { |
| if trait_obj |
| .bounds |
| .iter() |
| .any(|b| b.to_token_stream().to_string() == "Any") |
| { |
| // Rc<dyn Any> → return true |
| return true; |
| } else { |
| // Rc<dyn SomethingElse> → return false |
| return false; |
| } |
| } |
| // Inner type is not a trait object - recursively check |
| // if it references the containing struct |
| _ => { |
| return is_forward_field_internal(inner_ty, struct_name); |
| } |
| } |
| } |
| } |
| } |
| |
| // Recursively check other generic args |
| if let PathArguments::AngleBracketed(args) = &seg.arguments { |
| for arg in &args.args { |
| if let GenericArgument::Type(inner_ty) = arg { |
| if is_forward_field_internal(inner_ty, struct_name) { |
| return true; |
| } |
| } |
| } |
| } |
| } |
| |
| false |
| } |
| |
| _ => false, |
| } |
| } |
| |
| pub(super) fn classify_trait_object_field(ty: &Type) -> StructField { |
| // Check collections FIRST - they should be handled as collections, not forward refs |
| // This is important because is_forward_field would match Vec<Box<dyn Any>> as forward |
| if let Some(collection_info) = detect_collection_with_trait_object(ty) { |
| return match collection_info { |
| CollectionTraitInfo::VecBox(t) => StructField::VecBox(t), |
| CollectionTraitInfo::VecRc(t) => StructField::VecRc(t), |
| CollectionTraitInfo::VecArc(t) => StructField::VecArc(t), |
| CollectionTraitInfo::HashMapBox(k, t) => StructField::HashMapBox(k, t), |
| CollectionTraitInfo::HashMapRc(k, t) => StructField::HashMapRc(k, t), |
| CollectionTraitInfo::HashMapArc(k, t) => StructField::HashMapArc(k, t), |
| }; |
| } |
| if is_forward_field(ty) { |
| return StructField::Forward; |
| } |
| if let Some((_, _)) = is_box_dyn_trait(ty) { |
| return StructField::BoxDyn; |
| } |
| if let Some((_, trait_name)) = is_rc_dyn_trait(ty) { |
| return StructField::RcDyn(trait_name); |
| } |
| if let Some((_, trait_name)) = is_arc_dyn_trait(ty) { |
| return StructField::ArcDyn(trait_name); |
| } |
| if contains_trait_object(ty) { |
| return StructField::ContainsTraitObject; |
| } |
| StructField::None |
| } |
| |
| #[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>, |
| } |
| |
| pub(super) fn try_primitive_vec_type(node: &TypeNode) -> Option<TokenStream> { |
| if node.name != "Vec" { |
| return None; |
| } |
| let child = node.generics.first()?; |
| for (ty_name, type_id, _) in PRIMITIVE_ARRAY_TYPE_MAP { |
| if child.name == *ty_name { |
| return Some(quote! { #type_id }); |
| } |
| } |
| None |
| } |
| |
| pub(super) fn try_vec_of_option_primitive(node: &TypeNode) -> Option<TokenStream> { |
| if node.name != "Vec" { |
| return None; |
| } |
| let child = node.generics.first()?; |
| if child.name != "Option" { |
| return None; |
| } |
| let grandchild = child.generics.first()?; |
| for (ty_name, _, _) in PRIMITIVE_ARRAY_TYPE_MAP { |
| if grandchild.name == *ty_name { |
| return Some(quote! { |
| compile_error!("Vec<Option<primitive>> is not allowed!"); |
| }); |
| } |
| } |
| |
| None |
| } |
| |
| impl fmt::Display for TypeNode { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| if self.name == "Tuple" { |
| // Format as Rust tuple syntax: (T1, T2, T3) |
| write!( |
| f, |
| "({})", |
| self.generics |
| .iter() |
| .map(|g| g.to_string()) |
| .collect::<Vec<_>>() |
| .join(", ") |
| ) |
| } else if self.name == "Array" { |
| // For arrays, use the original type string if available (e.g., "[f32; 4]") |
| if let Some(ref original) = self.original_type_str { |
| write!(f, "{}", original) |
| } else { |
| // Fallback - shouldn't happen if properly constructed |
| write!(f, "Array") |
| } |
| } else if self.generics.is_empty() { |
| // 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.full_path, |
| self.generics |
| .iter() |
| .map(|g| g.to_string()) |
| .collect::<Vec<_>>() |
| .join(",") |
| ) |
| } |
| } |
| } |
| |
| pub(super) fn extract_type_name(ty: &Type) -> String { |
| if let Type::Path(type_path) = ty { |
| type_path.path.segments.last().unwrap().ident.to_string() |
| } 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() |
| } |
| } |
| |
| /// 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 { |
| if let Some(seg) = type_path.path.segments.last() { |
| if seg.ident == "Option" { |
| return true; |
| } |
| } |
| } |
| false |
| } |
| |
| pub(super) fn parse_generic_tree(ty: &Type) -> TypeNode { |
| // Handle trait objects specially - they can't be parsed as normal types |
| if matches!(ty, Type::TraitObject(_)) { |
| return TypeNode { |
| name: "TraitObject".to_string(), |
| full_path: "TraitObject".to_string(), |
| generics: vec![], |
| original_type_str: None, |
| }; |
| } |
| |
| // Handle tuples - make child generics empty |
| if let Type::Tuple(_tuple) = ty { |
| return TypeNode { |
| name: "Tuple".to_string(), |
| full_path: "Tuple".to_string(), |
| generics: vec![], |
| original_type_str: None, |
| }; |
| } |
| |
| // Handle arrays - extract element type and preserve original type string |
| if let Type::Array(array) = ty { |
| let elem_node = parse_generic_tree(&array.elem); |
| // Preserve the original type string including the length (e.g., "[f32; 4]") |
| 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) = |
| &type_path.path.segments.last().unwrap().arguments |
| { |
| args.args |
| .iter() |
| .filter_map(|arg| { |
| if let GenericArgument::Type(ty) = arg { |
| Some(parse_generic_tree(ty)) |
| } else { |
| None |
| } |
| }) |
| .collect() |
| } else { |
| vec![] |
| } |
| } else { |
| vec![] |
| }; |
| 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! { |
| fory_core::meta::FieldType::new( |
| fory_core::types::TypeId::LIST as u32, |
| true, |
| vec![fory_core::meta::FieldType { |
| type_id: fory_core::types::TypeId::UNKNOWN as u32, |
| nullable: true, |
| ref_tracking: false, |
| generics: vec![], |
| }] |
| ) |
| }; |
| } |
| |
| // Special handling for arrays: treat them as lists with element type generic |
| if node.name == "Array" { |
| if let Some(elem_node) = node.generics.first() { |
| let elem_token = generic_tree_to_tokens(elem_node); |
| // Check if element is primitive to determine the correct type ID |
| let is_primitive_elem = PRIMITIVE_TYPE_NAMES.contains(&elem_node.name.as_str()); |
| if is_primitive_elem { |
| // For primitive arrays, use primitive array type ID |
| let type_id_token = match elem_node.name.as_str() { |
| "bool" => quote! { fory_core::types::TypeId::BOOL_ARRAY as u32 }, |
| "i8" => quote! { fory_core::types::TypeId::INT8_ARRAY as u32 }, |
| "i16" => quote! { fory_core::types::TypeId::INT16_ARRAY as u32 }, |
| "i32" => quote! { fory_core::types::TypeId::INT32_ARRAY as u32 }, |
| "i64" => quote! { fory_core::types::TypeId::INT64_ARRAY as u32 }, |
| "i128" => quote! { fory_core::types::TypeId::INT128_ARRAY as u32 }, |
| "f32" => quote! { fory_core::types::TypeId::FLOAT32_ARRAY as u32 }, |
| "f64" => quote! { fory_core::types::TypeId::FLOAT64_ARRAY as u32 }, |
| "u8" => quote! { fory_core::types::TypeId::BINARY as u32 }, |
| "u16" => quote! { fory_core::types::TypeId::UINT16_ARRAY as u32 }, |
| "u32" => quote! { fory_core::types::TypeId::UINT32_ARRAY as u32 }, |
| "u64" => quote! { fory_core::types::TypeId::UINT64_ARRAY as u32 }, |
| "u128" => quote! { fory_core::types::TypeId::U128_ARRAY as u32 }, |
| _ => quote! { fory_core::types::TypeId::LIST as u32 }, |
| }; |
| return quote! { |
| fory_core::meta::FieldType::new( |
| #type_id_token, |
| false, |
| vec![] |
| ) |
| }; |
| } else { |
| // For non-primitive arrays, use LIST type ID with element type as generic |
| return quote! { |
| fory_core::meta::FieldType::new( |
| fory_core::types::TypeId::LIST as u32, |
| false, |
| vec![#elem_token] |
| ) |
| }; |
| } |
| } else { |
| // Array without element type info - shouldn't happen |
| return quote! { compile_error!("Array missing element type"); }; |
| } |
| } |
| |
| // If Option, unwrap it before generating children |
| let (nullable, base_node) = if node.name == "Option" { |
| if let Some(inner) = node.generics.first() { |
| if inner.name == "Option" { |
| return quote! { compile_error!("Nested adjacent Option is not allowed!"); }; |
| } |
| // Special handling for Option<Tuple> |
| if inner.name == "Tuple" { |
| return quote! { |
| fory_core::meta::FieldType::new( |
| fory_core::types::TypeId::LIST as u32, |
| true, |
| vec![fory_core::meta::FieldType { |
| type_id: fory_core::types::TypeId::UNKNOWN as u32, |
| nullable: true, |
| ref_tracking: false, |
| generics: vec![], |
| }] |
| ) |
| }; |
| } |
| // Unwrap Option and propagate parsing |
| (true, inner) |
| } else { |
| return quote! { compile_error!("Missing Option inner type"); }; |
| } |
| } else { |
| (!PRIMITIVE_TYPE_NAMES.contains(&node.name.as_str()), node) |
| }; |
| |
| // If Rc or Arc, unwrap to inner type - these are reference wrappers |
| // that don't add type info to the field type (handled by ref_tracking flag) |
| let base_node = if base_node.name == "Rc" || base_node.name == "Arc" { |
| if let Some(inner) = base_node.generics.first() { |
| inner |
| } else { |
| base_node |
| } |
| } else { |
| base_node |
| }; |
| |
| // `Vec<Option<primitive>>` rule stays as is |
| if let Some(ts) = try_vec_of_option_primitive(base_node) { |
| return ts; |
| } |
| |
| // Try primitive Vec type |
| let primitive_vec = try_primitive_vec_type(base_node); |
| |
| // Recursively generate children token streams |
| let children_tokens: Vec<TokenStream> = if primitive_vec.is_none() { |
| base_node |
| .generics |
| .iter() |
| .map(generic_tree_to_tokens) |
| .collect() |
| } else { |
| vec![] |
| }; |
| |
| // Build the syn::Type from the DISPLAY of base_node, not the original node if Option |
| let ty: syn::Type = syn::parse_str(&base_node.to_string()).unwrap(); |
| |
| // Get type ID |
| let get_type_id = if let Some(ts) = primitive_vec { |
| ts |
| } else { |
| quote! { |
| <#ty as fory_core::serializer::Serializer>::fory_get_type_id(type_resolver)? |
| } |
| }; |
| |
| quote! { |
| { |
| let type_id = #get_type_id; |
| let mut generics = vec![#(#children_tokens),*] as Vec<fory_core::meta::FieldType>; |
| // For tuples and sets, if no generic info is available, add UNKNOWN element |
| // This handles type aliases to tuples where we can't detect the tuple at macro time |
| if (type_id == fory_core::types::TypeId::LIST as u32 |
| || type_id == fory_core::types::TypeId::SET as u32) |
| && generics.is_empty() { |
| generics.push(fory_core::meta::FieldType::new( |
| fory_core::types::TypeId::UNKNOWN as u32, |
| true, |
| vec![] |
| )); |
| } |
| let is_custom = !fory_core::types::is_internal_type(type_id & 0xff); |
| if is_custom { |
| if type_resolver.is_xlang() && generics.len() > 0 { |
| return Err(fory_core::error::Error::unsupported("serialization of generic structs and enums is not supported in xlang mode")); |
| } else { |
| generics = vec![]; |
| } |
| } |
| fory_core::meta::FieldType::new(type_id, #nullable, generics) |
| } |
| } |
| } |
| |
| type FieldGroup = Vec<(String, String, u32)>; |
| type FieldGroups = ( |
| FieldGroup, |
| FieldGroup, |
| FieldGroup, |
| FieldGroup, |
| FieldGroup, |
| FieldGroup, |
| FieldGroup, |
| ); |
| |
| fn extract_option_inner(s: &str) -> Option<&str> { |
| s.strip_prefix("Option<")?.strip_suffix(">") |
| } |
| |
| const PRIMITIVE_TYPE_NAMES: [&str; 13] = [ |
| "bool", "i8", "i16", "i32", "i64", "i128", "f32", "f64", "u8", "u16", "u32", "u64", "u128", |
| ]; |
| |
| fn get_primitive_type_id(ty: &str) -> u32 { |
| match ty { |
| "bool" => TypeId::BOOL as u32, |
| "i8" => TypeId::INT8 as u32, |
| "i16" => TypeId::INT16 as u32, |
| "i32" => TypeId::INT32 as u32, |
| "i64" => TypeId::INT64 as u32, |
| "f32" => TypeId::FLOAT32 as u32, |
| "f64" => TypeId::FLOAT64 as u32, |
| "u8" => TypeId::UINT8 as u32, |
| "u16" => TypeId::UINT16 as u32, |
| "u32" => TypeId::UINT32 as u32, |
| "u64" => TypeId::UINT64 as u32, |
| "u128" => TypeId::U128 as u32, |
| "i128" => TypeId::INT128 as u32, |
| _ => unreachable!("Unknown primitive type: {}", ty), |
| } |
| } |
| |
| pub(super) fn is_primitive_type(ty: &str) -> bool { |
| PRIMITIVE_TYPE_NAMES.contains(&ty) |
| } |
| |
| /// Mapping of primitive type names to their writer and reader method names |
| /// Order: (type_name, writer_method, reader_method) |
| static PRIMITIVE_IO_METHODS: &[(&str, &str, &str)] = &[ |
| ("bool", "write_bool", "read_bool"), |
| ("i8", "write_i8", "read_i8"), |
| ("i16", "write_i16", "read_i16"), |
| ("i32", "write_varint32", "read_varint32"), |
| ("i64", "write_varint64", "read_varint64"), |
| ("f32", "write_f32", "read_f32"), |
| ("f64", "write_f64", "read_f64"), |
| ("u8", "write_u8", "read_u8"), |
| ("u16", "write_u16", "read_u16"), |
| ("u32", "write_u32", "read_u32"), |
| ("u64", "write_u64", "read_u64"), |
| ("usize", "write_usize", "read_usize"), |
| ("u128", "write_u128", "read_u128"), |
| ]; |
| |
| /// Check if a type is a direct primitive type (numeric or String, not wrapped in Option, Vec, etc.) |
| pub(super) fn is_direct_primitive_type(ty: &Type) -> bool { |
| if let Type::Path(type_path) = ty { |
| if let Some(seg) = type_path.path.segments.last() { |
| // Check if it's a simple type path without generics |
| if matches!(seg.arguments, PathArguments::None) { |
| let type_name = seg.ident.to_string(); |
| // Check for String type |
| if type_name == "String" { |
| return true; |
| } |
| // Check for numeric primitive types |
| return PRIMITIVE_IO_METHODS |
| .iter() |
| .any(|(name, _, _)| *name == type_name.as_str()); |
| } |
| } |
| } |
| false |
| } |
| |
| /// Get the writer method name for a primitive numeric type |
| /// Panics if type_name is not a primitive type |
| pub(super) fn get_primitive_writer_method(type_name: &str) -> &'static str { |
| PRIMITIVE_IO_METHODS |
| .iter() |
| .find(|(name, _, _)| *name == type_name) |
| .map(|(_, writer, _)| *writer) |
| .unwrap_or_else(|| panic!("type_name '{}' must be a primitive type", type_name)) |
| } |
| |
| /// Get the reader method name for a primitive numeric type |
| /// Panics if type_name is not a primitive type |
| pub(super) fn get_primitive_reader_method(type_name: &str) -> &'static str { |
| PRIMITIVE_IO_METHODS |
| .iter() |
| .find(|(name, _, _)| *name == type_name) |
| .map(|(_, _, reader)| *reader) |
| .unwrap_or_else(|| panic!("type_name '{}' must be a primitive type", type_name)) |
| } |
| |
| pub(crate) fn get_type_id_by_type_ast(ty: &Type) -> u32 { |
| let ty_str: String = ty |
| .to_token_stream() |
| .to_string() |
| .chars() |
| .filter(|c| !c.is_whitespace()) |
| .collect::<String>(); |
| get_type_id_by_name(&ty_str) |
| } |
| |
| /// Get the type ID for a given type string. |
| /// |
| /// Returns: |
| /// - `type_id` for known types (primitives, internal types, collections) |
| /// - `UNKNOWN` for unknown/user-defined types |
| pub(crate) fn get_type_id_by_name(ty: &str) -> u32 { |
| let ty = extract_option_inner(ty).unwrap_or(ty); |
| // Check primitive types |
| if PRIMITIVE_TYPE_NAMES.contains(&ty) { |
| return get_primitive_type_id(ty); |
| } |
| |
| // Check internal types |
| match ty { |
| "String" => return TypeId::STRING as u32, |
| "NaiveDate" => return TypeId::LOCAL_DATE as u32, |
| "NaiveDateTime" => return TypeId::TIMESTAMP as u32, |
| "Duration" => return TypeId::DURATION as u32, |
| "Decimal" => return TypeId::DECIMAL as u32, |
| "Vec<u8>" | "bytes" => return TypeId::BINARY as u32, |
| _ => {} |
| } |
| |
| // Check primitive arrays (Vec) |
| match ty { |
| "Vec<bool>" => return TypeId::BOOL_ARRAY as u32, |
| "Vec<i8>" => return TypeId::INT8_ARRAY as u32, |
| "Vec<i16>" => return TypeId::INT16_ARRAY as u32, |
| "Vec<i32>" => return TypeId::INT32_ARRAY as u32, |
| "Vec<i64>" => return TypeId::INT64_ARRAY as u32, |
| "Vec<i128>" => return TypeId::INT128_ARRAY as u32, |
| "Vec<f16>" => return TypeId::FLOAT16_ARRAY as u32, |
| "Vec<f32>" => return TypeId::FLOAT32_ARRAY as u32, |
| "Vec<f64>" => return TypeId::FLOAT64_ARRAY as u32, |
| "Vec<u16>" => return TypeId::UINT16_ARRAY as u32, |
| "Vec<u32>" => return TypeId::UINT32_ARRAY as u32, |
| "Vec<u64>" => return TypeId::UINT64_ARRAY as u32, |
| "Vec<u128>" => return TypeId::U128_ARRAY as u32, |
| _ => {} |
| } |
| |
| // Check primitive arrays (fixed-size arrays [T; N]) |
| // These will be serialized similarly to Vec but with fixed size |
| if ty.starts_with('[') && ty.contains(';') { |
| // Extract the element type from [T; N] |
| if let Some(elem_ty) = ty.strip_prefix('[').and_then(|s| s.split(';').next()) { |
| match elem_ty { |
| "bool" => return TypeId::BOOL_ARRAY as u32, |
| "i8" => return TypeId::INT8_ARRAY as u32, |
| "i16" => return TypeId::INT16_ARRAY as u32, |
| "i32" => return TypeId::INT32_ARRAY as u32, |
| "i64" => return TypeId::INT64_ARRAY as u32, |
| "i128" => return TypeId::INT128_ARRAY as u32, |
| "f16" => return TypeId::FLOAT16_ARRAY as u32, |
| "f32" => return TypeId::FLOAT32_ARRAY as u32, |
| "f64" => return TypeId::FLOAT64_ARRAY as u32, |
| "u16" => return TypeId::UINT16_ARRAY as u32, |
| "u32" => return TypeId::UINT32_ARRAY as u32, |
| "u64" => return TypeId::UINT64_ARRAY as u32, |
| "u128" => return TypeId::U128_ARRAY as u32, |
| _ => { |
| // Non-primitive array elements, treat as LIST |
| return TypeId::LIST as u32; |
| } |
| } |
| } |
| } |
| |
| // Check collection types |
| if ty.starts_with("Vec<") |
| || ty.starts_with("VecDeque<") |
| || ty.starts_with("LinkedList<") |
| || ty.starts_with("BinaryHeap<") |
| { |
| return TypeId::LIST as u32; |
| } |
| |
| if ty.starts_with("HashSet<") || ty.starts_with("BTreeSet<") { |
| return TypeId::SET as u32; |
| } |
| |
| if ty.starts_with("HashMap<") || ty.starts_with("BTreeMap<") { |
| return TypeId::MAP as u32; |
| } |
| |
| // Check tuple types (represented as "Tuple" by extract_type_name or starts with '(') |
| if ty == "Tuple" || ty.starts_with('(') { |
| return TypeId::LIST as u32; |
| } |
| |
| // Unknown type |
| TypeId::UNKNOWN as u32 |
| } |
| |
| fn get_primitive_type_size(type_id_num: u32) -> i32 { |
| let type_id = TypeId::try_from(type_id_num as i16).unwrap(); |
| match type_id { |
| TypeId::BOOL => 1, |
| TypeId::INT8 => 1, |
| TypeId::INT16 => 2, |
| TypeId::INT32 => 4, |
| TypeId::VAR32 => 4, |
| TypeId::INT64 => 8, |
| TypeId::VAR64 => 8, |
| TypeId::H64 => 8, |
| TypeId::FLOAT16 => 2, |
| TypeId::FLOAT32 => 4, |
| TypeId::FLOAT64 => 8, |
| TypeId::INT128 => 16, |
| TypeId::UINT8 => 1, |
| TypeId::UINT16 => 2, |
| TypeId::UINT32 => 4, |
| TypeId::VARU32 => 4, |
| TypeId::UINT64 => 8, |
| TypeId::VARU64 => 8, |
| TypeId::HU64 => 8, |
| TypeId::U128 => 16, |
| TypeId::USIZE => std::mem::size_of::<usize>() as i32, |
| TypeId::ISIZE => std::mem::size_of::<isize>() as i32, |
| _ => unreachable!(), |
| } |
| } |
| |
| fn is_compress(type_id: u32) -> bool { |
| // Only signed integer types are marked as compressible |
| // to maintain backward compatibility with field ordering |
| [ |
| TypeId::INT32 as u32, |
| TypeId::INT64 as u32, |
| TypeId::VAR32 as u32, |
| TypeId::VAR64 as u32, |
| TypeId::H64 as u32, |
| ] |
| .contains(&type_id) |
| } |
| |
| fn is_internal_type_id(type_id: u32) -> bool { |
| [ |
| TypeId::STRING as u32, |
| TypeId::LOCAL_DATE as u32, |
| TypeId::TIMESTAMP as u32, |
| TypeId::DURATION as u32, |
| TypeId::DECIMAL as u32, |
| TypeId::BINARY as u32, |
| TypeId::BOOL_ARRAY as u32, |
| TypeId::INT8_ARRAY as u32, |
| TypeId::INT16_ARRAY as u32, |
| TypeId::INT32_ARRAY as u32, |
| TypeId::INT64_ARRAY as u32, |
| TypeId::INT128_ARRAY as u32, |
| TypeId::FLOAT16_ARRAY as u32, |
| TypeId::FLOAT32_ARRAY as u32, |
| TypeId::FLOAT64_ARRAY as u32, |
| TypeId::UINT16_ARRAY as u32, |
| TypeId::UINT32_ARRAY as u32, |
| TypeId::UINT64_ARRAY as u32, |
| TypeId::U128_ARRAY as u32, |
| ] |
| .contains(&type_id) |
| } |
| |
| /// Group fields into serialization categories while normalizing field names to snake_case. |
| /// The returned groups preserve the ordering rules required by the serialization layout. |
| fn group_fields_by_type(fields: &[&Field]) -> FieldGroups { |
| let mut primitive_fields = Vec::new(); |
| let mut nullable_primitive_fields = Vec::new(); |
| let mut internal_type_fields = Vec::new(); |
| let mut list_fields = Vec::new(); |
| let mut set_fields = Vec::new(); |
| let mut map_fields = Vec::new(); |
| let mut other_fields = Vec::new(); |
| |
| // First handle Forward fields separately to avoid borrow checker issues |
| for (idx, field) in fields.iter().enumerate() { |
| if is_forward_field(&field.ty) { |
| 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)); |
| } |
| } |
| |
| let mut group_field = |ident: String, ty: &str| { |
| let type_id = get_type_id_by_name(ty); |
| // Categorize based on type_id |
| if PRIMITIVE_TYPE_NAMES.contains(&ty) { |
| primitive_fields.push((ident, ty.to_string(), type_id)); |
| } else if is_internal_type_id(type_id) { |
| internal_type_fields.push((ident, ty.to_string(), type_id)); |
| } else if type_id == TypeId::LIST as u32 { |
| list_fields.push((ident, ty.to_string(), type_id)); |
| } else if type_id == TypeId::SET as u32 { |
| set_fields.push((ident, ty.to_string(), type_id)); |
| } else if type_id == TypeId::MAP as u32 { |
| map_fields.push((ident, ty.to_string(), type_id)); |
| } else { |
| // User-defined type |
| other_fields.push((ident, ty.to_string(), type_id)); |
| } |
| }; |
| |
| 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 |
| if is_forward_field(&field.ty) { |
| continue; |
| } |
| |
| let ty: String = field |
| .ty |
| .to_token_stream() |
| .to_string() |
| .chars() |
| .filter(|c| !c.is_whitespace()) |
| .collect::<String>(); |
| // handle Option<Primitive> specially |
| if let Some(inner) = extract_option_inner(&ty) { |
| if PRIMITIVE_TYPE_NAMES.contains(&inner) { |
| let type_id = get_primitive_type_id(inner); |
| nullable_primitive_fields.push((ident, ty.to_string(), type_id)); |
| } else { |
| group_field(ident, inner); |
| } |
| } else { |
| group_field(ident, &ty); |
| } |
| } |
| |
| fn numeric_sorter(a: &(String, String, u32), b: &(String, String, u32)) -> std::cmp::Ordering { |
| let compress_a = is_compress(a.2); |
| let compress_b = is_compress(b.2); |
| let size_a = get_primitive_type_size(a.2); |
| let size_b = get_primitive_type_size(b.2); |
| compress_a |
| .cmp(&compress_b) |
| .then_with(|| size_b.cmp(&size_a)) |
| // Use descending type_id order to match Java's COMPARATOR_BY_PRIMITIVE_TYPE_ID |
| .then_with(|| b.2.cmp(&a.2)) |
| .then_with(|| a.0.cmp(&b.0)) |
| } |
| |
| fn type_id_then_name_sorter( |
| a: &(String, String, u32), |
| b: &(String, String, u32), |
| ) -> std::cmp::Ordering { |
| a.2.cmp(&b.2).then_with(|| a.0.cmp(&b.0)) |
| } |
| |
| fn name_sorter(a: &(String, String, u32), b: &(String, String, u32)) -> std::cmp::Ordering { |
| a.0.cmp(&b.0) |
| } |
| |
| primitive_fields.sort_by(numeric_sorter); |
| nullable_primitive_fields.sort_by(numeric_sorter); |
| internal_type_fields.sort_by(type_id_then_name_sorter); |
| list_fields.sort_by(name_sorter); |
| set_fields.sort_by(name_sorter); |
| map_fields.sort_by(name_sorter); |
| other_fields.sort_by(type_id_then_name_sorter); |
| |
| ( |
| primitive_fields, |
| nullable_primitive_fields, |
| internal_type_fields, |
| list_fields, |
| set_fields, |
| map_fields, |
| other_fields, |
| ) |
| } |
| |
| pub(crate) fn get_sorted_field_names(fields: &[&Field]) -> Vec<String> { |
| // For tuple structs, preserve the original field order. |
| // Tuple struct field names are "0", "1", "2", etc., which are positional. |
| // Sorting would break schema evolution when adding fields in the middle |
| // (e.g., (f64, u8) -> (f64, u8, f64) would change sorted order). |
| if is_tuple_struct(fields) { |
| return fields |
| .iter() |
| .enumerate() |
| .map(|(idx, field)| get_field_name(field, idx)) |
| .collect(); |
| } |
| |
| // For named structs, sort by type for optimal memory layout |
| let ( |
| primitive_fields, |
| nullable_primitive_fields, |
| internal_type_fields, |
| list_fields, |
| set_fields, |
| map_fields, |
| other_fields, |
| ) = group_fields_by_type(fields); |
| |
| let mut all_fields = primitive_fields; |
| all_fields.extend(nullable_primitive_fields); |
| all_fields.extend(internal_type_fields); |
| all_fields.extend(list_fields); |
| all_fields.extend(set_fields); |
| all_fields.extend(map_fields); |
| all_fields.extend(other_fields); |
| |
| all_fields.into_iter().map(|(name, _, _)| name).collect() |
| } |
| |
| pub(crate) fn get_filtered_fields_iter<'a>( |
| fields: &'a [&'a Field], |
| ) -> impl Iterator<Item = &'a Field> { |
| fields.iter().filter(|field| !is_skip_field(field)).copied() |
| } |
| |
| pub(super) fn get_filtered_source_fields_iter<'a>( |
| source_fields: &'a [crate::util::SourceField<'a>], |
| ) -> impl Iterator<Item = &'a crate::util::SourceField<'a>> { |
| source_fields.iter().filter(|sf| !is_skip_field(sf.field)) |
| } |
| |
| pub(super) fn get_sort_fields_ts(fields: &[&Field]) -> TokenStream { |
| let filterd_fields: Vec<&Field> = get_filtered_fields_iter(fields).collect(); |
| let sorted_names = get_sorted_field_names(&filterd_fields); |
| let names = sorted_names.iter().map(|name| { |
| quote! { #name } |
| }); |
| quote! { |
| &[#(#names),*] |
| } |
| } |
| |
| /// Field metadata for fingerprint computation. |
| struct FieldFingerprintInfo { |
| /// Field name (snake_case) or field ID as string |
| name_or_id: String, |
| /// Whether the field has explicit nullable=true/false set via #[fory(nullable)] |
| explicit_nullable: Option<bool>, |
| /// Whether reference tracking is enabled |
| ref_tracking: bool, |
| /// The type ID (UNKNOWN for user-defined types including enums/unions) |
| type_id: u32, |
| /// Whether the field type is `Option<T>` |
| is_option_type: bool, |
| } |
| |
| /// Computes struct fingerprint string at compile time (during proc-macro execution). |
| /// |
| /// **Fingerprint Format:** `<field_name_or_id>,<type_id>,<ref>,<nullable>;` |
| /// Fields are sorted by name lexicographically. |
| fn compute_struct_fingerprint(fields: &[&Field]) -> String { |
| use super::field_meta::{classify_field_type, parse_field_meta}; |
| |
| let mut field_infos: Vec<FieldFingerprintInfo> = Vec::with_capacity(fields.len()); |
| |
| for (idx, field) in fields.iter().enumerate() { |
| let meta = parse_field_meta(field).unwrap_or_default(); |
| if meta.skip { |
| continue; |
| } |
| |
| let name = get_field_name(field, idx); |
| let field_id = meta.effective_id(); |
| let name_or_id = if field_id >= 0 { |
| field_id.to_string() |
| } else { |
| to_snake_case(&name) |
| }; |
| |
| let type_class = classify_field_type(&field.ty); |
| let ref_tracking = meta.effective_ref_tracking(type_class); |
| let explicit_nullable = meta.nullable; |
| |
| // Get compile-time TypeId (UNKNOWN for user-defined types including enums/unions) |
| let type_id = get_type_id_by_type_ast(&field.ty); |
| |
| // Check if field type is Option<T> |
| let ty_str: String = field |
| .ty |
| .to_token_stream() |
| .to_string() |
| .chars() |
| .filter(|c| !c.is_whitespace()) |
| .collect(); |
| let is_option_type = ty_str.starts_with("Option<"); |
| |
| field_infos.push(FieldFingerprintInfo { |
| name_or_id, |
| explicit_nullable, |
| ref_tracking, |
| type_id, |
| is_option_type, |
| }); |
| } |
| |
| // Sort field infos by name_or_id lexicographically (matches Java/C++ behavior) |
| field_infos.sort_by(|a, b| a.name_or_id.cmp(&b.name_or_id)); |
| |
| // Build fingerprint string |
| let mut fingerprint = String::new(); |
| for info in &field_infos { |
| let ref_flag = if info.ref_tracking { "1" } else { "0" }; |
| let nullable = match info.explicit_nullable { |
| Some(true) => true, |
| Some(false) => false, |
| None => info.is_option_type, |
| }; |
| let nullable_flag = if nullable { "1" } else { "0" }; |
| |
| // User-defined types (UNKNOWN) use 0 in fingerprint, matching Java behavior |
| let effective_type_id = if info.type_id == TypeId::UNKNOWN as u32 { |
| 0 |
| } else { |
| info.type_id |
| }; |
| |
| fingerprint.push_str(&info.name_or_id); |
| fingerprint.push(','); |
| fingerprint.push_str(&effective_type_id.to_string()); |
| fingerprint.push(','); |
| fingerprint.push_str(ref_flag); |
| fingerprint.push(','); |
| fingerprint.push_str(nullable_flag); |
| fingerprint.push(';'); |
| } |
| |
| fingerprint |
| } |
| |
| /// Generates TokenStream for struct version hash (computed at compile time). |
| pub(crate) fn gen_struct_version_hash_ts(fields: &[&Field]) -> TokenStream { |
| let fingerprint = compute_struct_fingerprint(fields); |
| let (hash, _) = fory_core::meta::murmurhash3_x64_128(fingerprint.as_bytes(), 47); |
| let version_hash = (hash & 0xFFFF_FFFF) as i32; |
| |
| quote! { |
| { |
| const VERSION_HASH: i32 = #version_hash; |
| if fory_core::util::ENABLE_FORY_DEBUG_OUTPUT { |
| println!( |
| "[fory-debug] struct {} version fingerprint=\"{}\" hash={}", |
| std::any::type_name::<Self>(), |
| #fingerprint, |
| VERSION_HASH |
| ); |
| } |
| VERSION_HASH |
| } |
| } |
| } |
| |
| /// Represents the determined RefMode for a field |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub(crate) enum FieldRefMode { |
| None, |
| NullOnly, |
| Tracking, |
| } |
| |
| impl ToTokens for FieldRefMode { |
| fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { |
| let ts = match self { |
| FieldRefMode::None => quote! { fory_core::RefMode::None }, |
| FieldRefMode::NullOnly => quote! { fory_core::RefMode::NullOnly }, |
| FieldRefMode::Tracking => quote! { fory_core::RefMode::Tracking }, |
| }; |
| tokens.extend(ts); |
| } |
| } |
| |
| /// Determine the RefMode for a field based on field meta attributes and type. |
| /// This respects `#[fory(ref=false)]` and `#[fory(nullable)]` attributes. |
| pub(crate) fn determine_field_ref_mode(field: &syn::Field) -> FieldRefMode { |
| use super::field_meta::{classify_field_type, parse_field_meta}; |
| |
| let meta = parse_field_meta(field).unwrap_or_default(); |
| let type_class = classify_field_type(&field.ty); |
| let nullable = meta.effective_nullable(type_class); |
| let ref_tracking = meta.effective_ref_tracking(type_class); |
| |
| if ref_tracking { |
| FieldRefMode::Tracking |
| } else if nullable { |
| FieldRefMode::NullOnly |
| } else { |
| FieldRefMode::None |
| } |
| } |
| |
| /// Determine whether to skip writing type info for a struct field based on its type. |
| /// |
| /// According to xlang_serialization_spec.md: |
| /// - Primitive fields: skip type info |
| /// - Nullable primitive fields (`Option<primitive>`): skip type info |
| /// - Internal type fields (String, Date, arrays): skip type info |
| /// - List/Set/Map fields: skip type info |
| /// - Struct fields: WRITE type info |
| /// - Ext fields: WRITE type info |
| /// - Enum fields: skip type info (enum is morphic) |
| /// |
| /// Returns true if type info should be skipped, false if it should be written. |
| pub(crate) fn should_skip_type_info_for_field(ty: &Type) -> bool { |
| let type_id = get_type_id_by_type_ast(ty); |
| if [ |
| TypeId::STRUCT as u32, |
| TypeId::COMPATIBLE_STRUCT as u32, |
| TypeId::NAMED_STRUCT as u32, |
| TypeId::NAMED_COMPATIBLE_STRUCT as u32, |
| TypeId::EXT as u32, |
| TypeId::NAMED_EXT as u32, |
| TypeId::UNKNOWN as u32, |
| ] |
| .contains(&type_id) |
| { |
| // Struct and Ext types need type info |
| return false; |
| } |
| // Primitive, nullable primitive, internal types, List/Set/Map skip type info |
| true |
| } |
| |
| pub(crate) fn is_skip_field(field: &syn::Field) -> bool { |
| super::field_meta::is_skip_field(field) |
| } |
| |
| pub(crate) fn is_skip_enum_variant(variant: &syn::Variant) -> bool { |
| variant.attrs.iter().any(|attr| { |
| attr.path().is_ident("fory") && { |
| let mut skip = false; |
| let _ = attr.parse_nested_meta(|meta| { |
| if meta.path.is_ident("skip") { |
| skip = true; |
| } |
| Ok(()) |
| }); |
| skip |
| } |
| }) |
| } |
| |
| pub(crate) fn is_default_value_variant(variant: &syn::Variant) -> bool { |
| variant |
| .attrs |
| .iter() |
| .any(|attr| attr.path().is_ident("default")) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use syn::parse_quote; |
| |
| #[test] |
| fn group_fields_normalizes_names_and_preserves_ordering() { |
| let fields: Vec<syn::Field> = vec![ |
| parse_quote!(pub camelCase: i32), |
| parse_quote!(pub optionalValue: Option<i64>), |
| parse_quote!(pub simpleString: String), |
| parse_quote!(pub listItems: Vec<String>), |
| parse_quote!(pub setItems: HashSet<i32>), |
| parse_quote!(pub mapValues: HashMap<String, i32>), |
| parse_quote!(pub customType: CustomType), |
| ]; |
| let field_refs: Vec<&syn::Field> = fields.iter().collect(); |
| |
| let ( |
| primitive_fields, |
| nullable_primitive_fields, |
| internal_type_fields, |
| list_fields, |
| set_fields, |
| map_fields, |
| other_fields, |
| ) = group_fields_by_type(&field_refs); |
| |
| let primitive_names: Vec<&str> = primitive_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(primitive_names, vec!["camel_case"]); |
| |
| let nullable_names: Vec<&str> = nullable_primitive_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(nullable_names, vec!["optional_value"]); |
| |
| let internal_names: Vec<&str> = internal_type_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(internal_names, vec!["simple_string"]); |
| |
| let list_names: Vec<&str> = list_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(list_names, vec!["list_items"]); |
| |
| let set_names: Vec<&str> = set_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(set_names, vec!["set_items"]); |
| |
| let map_names: Vec<&str> = map_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(map_names, vec!["map_values"]); |
| |
| let other_names: Vec<&str> = other_fields |
| .iter() |
| .map(|(name, _, _)| name.as_str()) |
| .collect(); |
| assert_eq!(other_names, vec!["custom_type"]); |
| |
| let sorted_names = get_sorted_field_names(&field_refs); |
| assert_eq!( |
| sorted_names, |
| vec![ |
| "camel_case".to_string(), |
| "optional_value".to_string(), |
| "simple_string".to_string(), |
| "list_items".to_string(), |
| "set_items".to_string(), |
| "map_values".to_string(), |
| "custom_type".to_string(), |
| ] |
| ); |
| } |
| } |