Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions rust/fory-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@
//! `map`, or `tuple` metadata to select serializers recursively at child nodes.
//! - **`#[fory(default)]`**: Marks the fallible deserialization default `ForyUnion` variant.
//! `ForyUnion` requires exactly one default variant.
//! - **`#[fory(native)]`**: Selects the Rust-native `enum` representation for a `ForyUnion`
//! enum even when every variant is unit or carries only one value. The enum declares no
//! `#[fory(unknown)]` carrier, uses normal registration, and is rejected in xlang mode.
//! Valid only on `ForyUnion`.
//!
//! ## Field Types
//!
Expand Down Expand Up @@ -239,7 +243,11 @@ pub fn proc_macro_derive_fory_struct(input: proc_macro::TokenStream) -> TokenStr
.into_compile_error()
.into();
}
derive_serializer(input)
let attrs = match parse_fory_attrs(&input.attrs, false) {
Ok(attrs) => attrs,
Err(err) => return err.into_compile_error().into(),
};
derive_serializer(input, attrs)
}

/// Derive macro for pure enum serialization.
Expand All @@ -263,28 +271,33 @@ pub fn proc_macro_derive_fory_enum(input: proc_macro::TokenStream) -> TokenStrea
.into_compile_error()
.into();
}
derive_serializer(input)
let attrs = match parse_fory_attrs(&input.attrs, false) {
Ok(attrs) => attrs,
Err(err) => return err.into_compile_error().into(),
};
derive_serializer(input, attrs)
}

/// Derives serialization for data-carrying Rust enums.
///
/// Xlang-compatible unit and single-payload variants use the Fory `UNION`
/// representation. Native multi-field tuple and named variants use the native
/// `ENUM` representation.
/// `ENUM` representation. `#[fory(native)]` selects the native `ENUM`
/// representation for any variant shape.
#[proc_macro_derive(ForyUnion, attributes(fory))]
pub fn proc_macro_derive_fory_union(input: proc_macro::TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
if let Err(err) = object::derive_union::validate_input(&input) {
let attrs = match parse_fory_attrs(&input.attrs, true) {
Ok(attrs) => attrs,
Err(err) => return err.into_compile_error().into(),
};
if let Err(err) = object::derive_union::validate_input(&input, attrs.native) {
return err.into_compile_error().into();
}
derive_serializer(input)
derive_serializer(input, attrs)
}

fn derive_serializer(input: DeriveInput) -> TokenStream {
let attrs = match parse_fory_attrs(&input.attrs) {
Ok(attrs) => attrs,
Err(err) => return err.into_compile_error().into(),
};
fn derive_serializer(input: DeriveInput, attrs: ForyAttrs) -> TokenStream {
let runtime_root = match runtime_root::resolve_runtime_root() {
Ok(root) => root,
Err(err) => return err.into_compile_error().into(),
Expand Down Expand Up @@ -340,14 +353,17 @@ pub(crate) struct ForyAttrs {
pub generate_default: bool,
pub evolving: Option<bool>,
pub target: Option<Type>,
pub native: bool,
}

/// Parse fory attributes and return ForyAttrs
fn parse_fory_attrs(attrs: &[Attribute]) -> syn::Result<ForyAttrs> {
/// Parse fory attributes and return ForyAttrs. `native` is accepted only when
/// `allow_native` is set by the `ForyUnion` derive.
fn parse_fory_attrs(attrs: &[Attribute], allow_native: bool) -> syn::Result<ForyAttrs> {
let mut debug_flag: Option<bool> = None;
let mut generate_default_flag: Option<bool> = None;
let mut evolving_flag: Option<bool> = None;
let mut target: Option<Type> = None;
let mut native_flag: Option<bool> = None;

for attr in attrs {
if attr.path().is_ident("fory") {
Expand Down Expand Up @@ -411,6 +427,26 @@ fn parse_fory_attrs(attrs: &[Attribute]) -> syn::Result<ForyAttrs> {
));
}
target = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("native") {
if !allow_native {
return Err(meta.error("`native` is valid only for ForyUnion"));
}
let value = if meta.input.is_empty() {
true
} else {
let lit: LitBool = meta.value()?.parse()?;
lit.value
};
native_flag = match native_flag {
Some(existing) if existing != value => {
return Err(syn::Error::new(
meta.path.span(),
"conflicting `native` attribute values",
));
}
Some(_) => native_flag,
None => Some(value),
};
} else {
return Err(meta.error("unsupported type-level fory attribute"));
}
Expand All @@ -433,6 +469,7 @@ fn parse_fory_attrs(attrs: &[Attribute]) -> syn::Result<ForyAttrs> {
generate_default: generate_default_flag.unwrap_or(false),
evolving: evolving_flag,
target,
native: native_flag.unwrap_or(false),
})
}

Expand All @@ -450,7 +487,7 @@ mod tests {
name: String,
}
};
let attrs = parse_fory_attrs(&input.attrs).unwrap();
let attrs = parse_fory_attrs(&input.attrs, false).unwrap();
assert_eq!(
attrs.target.unwrap().to_token_stream().to_string(),
"external :: User"
Expand All @@ -465,7 +502,7 @@ mod tests {
name: String,
}
};
let err = match parse_fory_attrs(&input.attrs) {
let err = match parse_fory_attrs(&input.attrs, false) {
Ok(_) => panic!("external structural serializers must reject `generate_default`"),
Err(err) => err,
};
Expand Down
62 changes: 24 additions & 38 deletions rust/fory-derive/src/object/derive_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ use crate::util::{extract_fields, source_fields, SourceField};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{DataEnum, Fields};

const NATIVE_ENUM_XLANG_ERROR: &str =
"#[fory(native)] and multi-field enum variants are Rust-native and not representable in xlang mode";

fn temp_var_name(i: usize) -> String {
format!("f{}", i)
}
Expand Down Expand Up @@ -231,16 +235,15 @@ fn gen_read_single_payload(source_fields: &[SourceField<'_>]) -> TokenStream {
}
}

/// For Union-compatible enums with data variants, return UNION TypeId in xlang mode.
pub fn gen_actual_type_id(data_enum: &DataEnum) -> TokenStream {
let is_union_compatible = is_union_compatible_enum(data_enum);
/// For xlang unions, return UNION TypeId in xlang mode.
pub fn gen_actual_type_id(data_enum: &DataEnum, xlang_union: bool) -> TokenStream {
let has_data_variants = data_enum
.variants
.iter()
.any(|v| !matches!(v.fields, Fields::Unit));

if is_union_compatible && has_data_variants {
// Union-compatible enum: use typed/named union IDs in xlang mode
if xlang_union {
// Xlang union: use typed/named union IDs in xlang mode
quote! {
Ok(if xlang {
if register_by_name {
Expand All @@ -255,9 +258,7 @@ pub fn gen_actual_type_id(data_enum: &DataEnum) -> TokenStream {
} else if has_data_variants {
quote! {
if xlang {
Err(fory_core::Error::not_allowed(
"multi-field tuple and named enum variants are not representable in xlang mode",
))
Err(fory_core::Error::not_allowed(#NATIVE_ENUM_XLANG_ERROR))
} else {
Ok(fory_core::serializer::enum_::actual_type_id(
type_id,
Expand Down Expand Up @@ -695,14 +696,15 @@ fn compatible_variant_writes(

pub fn gen_write_data(
data_enum: &DataEnum,
xlang_union: bool,
generics: &syn::Generics,
target_path: &TokenStream,
) -> TokenStream {
let native_only = data_enum
.variants
.iter()
.any(|variant| !matches!(variant.fields, Fields::Unit))
&& !is_union_compatible_enum(data_enum);
&& !xlang_union;
let default_variant_value = data_enum
.variants
.iter()
Expand All @@ -717,9 +719,7 @@ pub fn gen_write_data(
compatible_variant_writes(data_enum, generics, target_path, default_variant_value);
let xlang_write = if native_only {
quote! {
Err(fory_core::Error::not_allowed(
"multi-field tuple and named enum variants are not representable in xlang mode",
))
Err(fory_core::Error::not_allowed(#NATIVE_ENUM_XLANG_ERROR))
}
} else {
quote! {
Expand Down Expand Up @@ -749,15 +749,9 @@ pub fn gen_write_data(
}
}

pub fn gen_write_type_info(data_enum: &DataEnum) -> TokenStream {
let is_union_compatible = is_union_compatible_enum(data_enum);
let has_data_variants = data_enum
.variants
.iter()
.any(|v| !matches!(v.fields, Fields::Unit));

if is_union_compatible && has_data_variants {
// Union-compatible with data: write typed/named union type info in xlang mode
pub fn gen_write_type_info(xlang_union: bool) -> TokenStream {
if xlang_union {
// Xlang union: write typed/named union type info in xlang mode
quote! {
if context.is_xlang() {
let provider_type_id = ::std::any::TypeId::of::<Self>();
Expand Down Expand Up @@ -792,7 +786,7 @@ pub fn gen_read_with_type_info(_: &DataEnum) -> TokenStream {
/// Check if enum is Union-compatible:
/// - Must have at least one data-carrying variant (single-field)
/// - All variants must be either unit or single-field
fn is_union_compatible_enum(data_enum: &DataEnum) -> bool {
pub(crate) fn is_union_compatible_enum(data_enum: &DataEnum) -> bool {
let has_data_variant = data_enum
.variants
.iter()
Expand All @@ -819,16 +813,10 @@ fn xlang_union_case_id(data_enum: &DataEnum, idx: usize, variant: &syn::Variant)
}

/// Generate the static TypeId for enum.
/// For Union-compatible enums with data variants, return UNION TypeId
/// to ensure correct type info handling in xlang mode struct field read/write.
pub fn gen_static_type_id(data_enum: &DataEnum) -> TokenStream {
let is_union_compatible = is_union_compatible_enum(data_enum);
let has_data_variants = data_enum
.variants
.iter()
.any(|v| !matches!(v.fields, Fields::Unit));

if is_union_compatible && has_data_variants {
/// For xlang unions, return UNION TypeId to ensure correct type info handling
/// in xlang mode struct field read/write.
pub fn gen_static_type_id(xlang_union: bool) -> TokenStream {
if xlang_union {
quote! { fory_core::TypeId::UNION }
} else {
quote! { fory_core::TypeId::ENUM }
Expand Down Expand Up @@ -1096,10 +1084,10 @@ fn compatible_variant_reads(

pub fn gen_read_data(
data_enum: &DataEnum,
xlang_union: bool,
generics: &syn::Generics,
target_path: &TokenStream,
) -> TokenStream {
let is_union_compatible = is_union_compatible_enum(data_enum);
let has_data_variants = data_enum
.variants
.iter()
Expand Down Expand Up @@ -1127,7 +1115,7 @@ pub fn gen_read_data(

let default_variant_construction = variant_default_value(default_variant, target_path);

let unknown_xlang_branch = if is_union_compatible && has_data_variants {
let unknown_xlang_branch = if xlang_union {
// ForyUnion validation guarantees xlang-compatible ADTs have the
// runtime unknown carrier. A skip/default fallback here would drop
// forward-compatible payload metadata instead of preserving it.
Expand Down Expand Up @@ -1158,11 +1146,9 @@ pub fn gen_read_data(
}
}
};
let xlang_read = if has_data_variants && !is_union_compatible {
let xlang_read = if has_data_variants && !xlang_union {
quote! {
Err(fory_core::Error::not_allowed(
"multi-field tuple and named enum variants are not representable in xlang mode",
))
Err(fory_core::Error::not_allowed(#NATIVE_ENUM_XLANG_ERROR))
}
} else {
quote! {
Expand Down
Loading
Loading