diff --git a/documentation/source/messaging.md b/documentation/source/messaging.md index ff016a0c..394bc020 100644 --- a/documentation/source/messaging.md +++ b/documentation/source/messaging.md @@ -119,3 +119,34 @@ struct Inner { my_field: bool, } ``` + +## Ignoring fields / variants + +To ignore a field or variant, annotate it with `#[serde(skip)]`. This is useful when transferring partially private data because it allows you to specify which data is exposed to Dart. See Serde's documentation on [variant](https://serde.rs/variant-attrs.html) and [field](https://serde.rs/field-attrs.html) attributes for more information on how Serde handles this attribute. + +```{code-block} rust +:caption: Rust +#[derive(Serialize, RustSignal)] +struct UpdateMessage { + event: String, + struct_data: StructData, + enum_data: EnumData, +} + +#[derive(Serialize, SignalPiece)] +struct StructData { + my_public_field: bool, + #[serde(skip)] + my_private_field: bool, +} + +#[derive(Serialize, SignalPiece)] +enum EnumData { + Variant1(i32, #[serde(skip)] i32), + Variant2 { + my_public_field: bool, + #[serde(skip)] + my_private_field: bool, + }, +} +``` diff --git a/flutter_package/example/native/hub/src/signals/complex_types.rs b/flutter_package/example/native/hub/src/signals/complex_types.rs index 38475be1..9ef49ae0 100644 --- a/flutter_package/example/native/hub/src/signals/complex_types.rs +++ b/flutter_package/example/native/hub/src/signals/complex_types.rs @@ -14,8 +14,10 @@ pub enum SerdeData { OtherTypes(Box), UnitVariant, NewTypeVariant(String), - TupleVariant(u32, u64), + TupleVariant(u32, #[serde(skip)] NotSerializable, u64), StructVariant { + #[serde(skip)] + ignored: NotSerializable, f0: UnitStruct, f1: NewTypeStruct, f2: TupleStruct, @@ -107,3 +109,6 @@ pub enum CStyleEnum { D, E = 10, } + +#[derive(Default, PartialEq, Clone)] +pub struct NotSerializable; diff --git a/flutter_package/example/native/hub/src/testing/mod.rs b/flutter_package/example/native/hub/src/testing/mod.rs index 022762e9..cb3130f6 100644 --- a/flutter_package/example/native/hub/src/testing/mod.rs +++ b/flutter_package/example/native/hub/src/testing/mod.rs @@ -1,7 +1,7 @@ use crate::signals::{ - CStyleEnum, ComplexSignalTestResult, List, NewTypeStruct, OtherTypes, - PrimitiveTypes, SerdeData, SimpleList, Struct, Tree, TupleStruct, UnitStruct, - UnitTestEnd, UnitTestStart, + CStyleEnum, ComplexSignalTestResult, List, NewTypeStruct, NotSerializable, + OtherTypes, PrimitiveTypes, SerdeData, SimpleList, Struct, Tree, TupleStruct, + UnitStruct, UnitTestEnd, UnitTestStart, }; use rinf::{DartSignal, RustSignal}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; @@ -163,9 +163,10 @@ fn get_complex_signals() -> Vec { "test.\u{10348}.\u{00a2}\u{0939}\u{20ac}\u{d55c}..".to_string(), ); - let v5 = SerdeData::TupleVariant(3, 6); + let v5 = SerdeData::TupleVariant(3, NotSerializable, 6); let v6 = SerdeData::StructVariant { + ignored: NotSerializable, f0: UnitStruct, f1: NewTypeStruct(1), f2: TupleStruct(2, 3), diff --git a/rust_crate_cli/src/tool/generate.rs b/rust_crate_cli/src/tool/generate.rs index ec60450d..64ca1ac4 100644 --- a/rust_crate_cli/src/tool/generate.rs +++ b/rust_crate_cli/src/tool/generate.rs @@ -14,8 +14,8 @@ use std::sync::mpsc::channel; use std::time::Duration; use syn::spanned::Spanned; use syn::{ - Attribute, Expr, File, GenericArgument, Item, ItemEnum, ItemStruct, Lit, - PathArguments, Type, TypeArray, TypePath, TypeTuple, + Attribute, Expr, Field, File, GenericArgument, Item, ItemEnum, ItemStruct, + Lit, PathArguments, Type, TypeArray, TypePath, TypeTuple, Variant, }; static GEN_MOD: &str = "signals"; @@ -225,6 +225,7 @@ fn trace_struct(traced: &mut Traced, item: &ItemStruct) { let fields: Vec = unnamed .unnamed .iter() + .filter(is_kept) .map(|field| to_type_format(&field.ty)) .collect(); if fields.is_empty() { @@ -239,6 +240,7 @@ fn trace_struct(traced: &mut Traced, item: &ItemStruct) { let fields = named .named .iter() + .filter(is_kept) .filter_map(|field| { field.ident.as_ref().map(|ident| Named { name: ident.to_string(), @@ -264,6 +266,7 @@ fn trace_enum(traced: &mut Traced, item: &ItemEnum) { let variants: BTreeMap> = item .variants .iter() + .filter(is_kept) .map(|variant| { let name = variant.ident.to_string(); let variant_format = match &variant.fields { @@ -272,6 +275,7 @@ fn trace_enum(traced: &mut Traced, item: &ItemEnum) { let fields = unnamed .unnamed .iter() + .filter(is_kept) .map(|field| to_type_format(&field.ty)) .collect::>(); if fields.is_empty() { @@ -286,6 +290,7 @@ fn trace_enum(traced: &mut Traced, item: &ItemEnum) { let fields = named .named .iter() + .filter(is_kept) .filter_map(|field| { field.ident.as_ref().map(|ident| Named { name: ident.to_string(), @@ -311,6 +316,38 @@ fn trace_enum(traced: &mut Traced, item: &ItemEnum) { traced.registry.insert(type_name, container); } +/// Returns `false` if Serde skips `item` during serialization. +fn is_kept(item: &T) -> bool { + !item.get_attrs().iter().any(|attr| { + if !attr.path().is_ident("serde") { + return false; + } + let mut skip = false; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("skip") { + skip = true; + } + Ok(()) + }); + skip + }) +} + +/// Helper trait required for [`is_kept`]. +trait GetAttrs { + fn get_attrs(&self) -> &Vec; +} +impl GetAttrs for &Field { + fn get_attrs(&self) -> &Vec { + &self.attrs + } +} +impl GetAttrs for &Variant { + fn get_attrs(&self) -> &Vec { + &self.attrs + } +} + /// Checks that the name of newly found signal is usable. fn check_signal_name(name: &str, traced: &Traced) -> Result<(), SetupError> { if traced.registry.contains_key(name) { diff --git a/rust_crate_proc/src/lib.rs b/rust_crate_proc/src/lib.rs index 3d17e53d..bc9c02cd 100644 --- a/rust_crate_proc/src/lib.rs +++ b/rust_crate_proc/src/lib.rs @@ -2,8 +2,8 @@ use heck::{ToShoutySnakeCase, ToSnakeCase}; use proc_macro::TokenStream; use quote::quote; use syn::{ - Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Ident, Index, - parse_macro_input, + Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, Field, Fields, + Ident, Index, Token, Variant, parse_macro_input, punctuated::Punctuated, }; static BANNED_LOWER_PREFIX: &str = "rinf"; @@ -29,7 +29,12 @@ pub fn derive_signal_piece(input: TokenStream) -> TokenStream { return create_generic_error(ast); } - // Enforce all fields to implement the foreign signal trait. + // Check the attributes of the variants / fields. + if let Err(error) = check_invalid_attrs(&ast.data) { + return error.to_compile_error().into(); + } + + // Require that all included fields implement the signal trait. let expanded = match &ast.data { Data::Struct(data_struct) => get_struct_signal_impl(data_struct, name), Data::Enum(data_enum) => get_enum_signal_impl(data_enum, name), @@ -77,7 +82,12 @@ fn derive_dart_signal_real( return create_generic_error(ast); } - // Enforce all fields to implement the foreign signal trait. + // Check the attributes of the variants / fields. + if let Err(error) = check_invalid_attrs(&ast.data) { + return error.to_compile_error().into(); + } + + // Require that all included fields implement the signal trait. let where_clause = match &ast.data { Data::Struct(data_struct) => get_struct_where_clause(data_struct), Data::Enum(data_enum) => get_enum_where_clause(data_enum), @@ -195,7 +205,12 @@ fn derive_rust_signal_real( return create_generic_error(ast); } - // Enforce all fields to implement the foreign signal trait. + // Check the attributes of the variants / fields. + if let Err(error) = check_invalid_attrs(&ast.data) { + return error.to_compile_error().into(); + } + + // Require that all included fields implement the signal trait. let where_clause = match &ast.data { Data::Struct(data_struct) => get_struct_where_clause(data_struct), Data::Enum(data_enum) => get_enum_where_clause(data_enum), @@ -255,16 +270,67 @@ fn derive_rust_signal_real( TokenStream::from(expanded) } -/// Enforces all fields of a struct to have the foreign signal trait. +/// Checks if the attributes of the variants / fields are unsupported. +fn check_invalid_attrs(data: &Data) -> syn::Result<()> { + fn check_fields(fields: &Fields) -> syn::Result<()> { + match fields { + Fields::Named(fields) => check_attrs(&fields.named), + Fields::Unnamed(fields) => check_attrs(&fields.unnamed), + Fields::Unit => Ok(()), + } + } + + fn check_attrs( + items: &Punctuated, + ) -> syn::Result<()> { + items.iter().try_for_each(|item| { + item.get_attrs().iter().try_for_each(|attr| { + if !attr.path().is_ident("serde") { + return Ok(()); + } + attr.parse_nested_meta(|meta| match meta.path.get_ident() { + Some(ident) + if ident == "skip_serializing" + || ident == "skip_serializing_if" + || ident == "skip_deserializing" => + { + Err(meta.error(format!( + "`{ident}` is not supported within rinf signal data" + ))) + } + _ => Ok(()), + }) + }) + }) + } + + match data { + Data::Struct(data) => check_fields(&data.fields), + Data::Enum(data) => { + check_attrs(&data.variants)?; + data + .variants + .iter() + .try_for_each(|variant| check_fields(&variant.fields)) + } + Data::Union(_) => Ok(()), // Serde does not support derive for unions + } +} + +/// Require that all included fields of a struct implement the [`SignalPiece`] trait. /// This assists with type-safe development. fn get_struct_where_clause( data_struct: &DataStruct, ) -> proc_macro2::TokenStream { let field_types: Vec<_> = match &data_struct.fields { // For named structs (struct-like), extract the field types. - Fields::Named(all) => all.named.iter().map(|f| &f.ty).collect(), + Fields::Named(all) => { + all.named.iter().filter(is_kept).map(|f| &f.ty).collect() + } // For unnamed structs (tuple-like), extract the field types. - Fields::Unnamed(all) => all.unnamed.iter().map(|f| &f.ty).collect(), + Fields::Unnamed(all) => { + all.unnamed.iter().filter(is_kept).map(|f| &f.ty).collect() + } // For unit-like structs (without any inner data), do nothing. Fields::Unit => Vec::new(), }; @@ -273,18 +339,23 @@ fn get_struct_where_clause( } } -/// Enforces all fields of an enum variant to have the foreign signal trait. +/// Require that all included variants of an enum implement the [`SignalPiece`] trait. /// This assists with type-safe development. fn get_enum_where_clause(data_enum: &DataEnum) -> proc_macro2::TokenStream { let variant_types: Vec<_> = data_enum .variants .iter() + .filter(is_kept) .flat_map(|variant| { match &variant.fields { // For named variants (struct-like), extract the field types. - Fields::Named(all) => all.named.iter().map(|f| &f.ty).collect(), + Fields::Named(all) => { + all.named.iter().filter(is_kept).map(|f| &f.ty).collect() + } // For unnamed variants (tuple-like), extract the field types. - Fields::Unnamed(all) => all.unnamed.iter().map(|f| &f.ty).collect(), + Fields::Unnamed(all) => { + all.unnamed.iter().filter(is_kept).map(|f| &f.ty).collect() + } // For unit-like variants (without any inner data), do nothing. Fields::Unit => Vec::new(), } @@ -305,6 +376,7 @@ fn get_struct_signal_impl( let fields = named_fields .named .iter() + .filter(is_kept) .filter_map(|field| field.ident.clone()); quote! { impl rinf::SignalPiece for #name { @@ -316,8 +388,13 @@ fn get_struct_signal_impl( } } Fields::Unnamed(unnamed_fields) => { - let field_indices: Vec = - (0..unnamed_fields.unnamed.len()).map(Index::from).collect(); + let field_indices: Vec = unnamed_fields + .unnamed + .iter() + .enumerate() + .filter(is_kept) + .map(|(index, _)| Index::from(index)) + .collect(); quote! { impl rinf::SignalPiece for #name { fn be_signal_piece(&self) { @@ -343,35 +420,42 @@ fn get_enum_signal_impl( data_enum: &DataEnum, name: &Ident, ) -> proc_macro2::TokenStream { - let variants = data_enum.variants.iter().map(|variant| { + let variants = data_enum.variants.iter().filter(is_kept).map(|variant| { let variant_ident = &variant.ident; match &variant.fields { Fields::Named(named_fields) => { let fields: Vec = named_fields .named .iter() + .filter(is_kept) .filter_map(|field| field.ident.clone()) .collect(); quote! { - Self::#variant_ident { #(#fields),* } => { + Self::#variant_ident { #(#fields, )* .. } => { use rinf::SignalPiece; #(SignalPiece::be_signal_piece(#fields);)* } } } Fields::Unnamed(unnamed_fields) => { - let field_indices: Vec = - (0..unnamed_fields.unnamed.len()).map(Index::from).collect(); - let field_vars: Vec = field_indices + let field_vars: Vec = unnamed_fields + .unnamed .iter() - .map(|i| { - Ident::new(&format!("field_{}", i.index), variant_ident.span()) + .enumerate() + .map(|(index, field)| match is_kept(field) { + true => Ident::new(&format!("field_{index}"), variant_ident.span()), + false => Ident::new("_", variant_ident.span()), }) .collect(); + let field_vars_filtered: Vec = field_vars + .iter() + .filter(|&ident| ident != "_") + .cloned() + .collect(); quote! { Self::#variant_ident(#(#field_vars),*) => { use rinf::SignalPiece; - #(SignalPiece::be_signal_piece(#field_vars);)* + #(SignalPiece::be_signal_piece(#field_vars_filtered);)* } } } @@ -387,12 +471,60 @@ fn get_enum_signal_impl( fn be_signal_piece(&self) { match self { #( #variants )* + _ => {} } } } } } +/// Returns `false` if Serde skips `item` during serialization. +fn is_kept(item: &T) -> bool { + !item.get_attrs().iter().any(|attr| { + if !attr.path().is_ident("serde") { + return false; + } + let mut skip = false; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("skip") { + skip = true; + } + Ok(()) + }); + skip + }) +} + +/// Helper trait required for [`check_invalid_attrs`] and [`is_kept`]. +trait GetAttrs { + fn get_attrs(&self) -> &Vec; +} +impl GetAttrs for Field { + fn get_attrs(&self) -> &Vec { + &self.attrs + } +} +impl GetAttrs for &Field { + fn get_attrs(&self) -> &Vec { + &self.attrs + } +} +impl GetAttrs for Variant { + fn get_attrs(&self) -> &Vec { + &self.attrs + } +} +impl GetAttrs for &Variant { + fn get_attrs(&self) -> &Vec { + &self.attrs + } +} +impl GetAttrs for (usize, &Field) { + fn get_attrs(&self) -> &Vec { + &self.1.attrs + } +} + fn create_generic_error(ast: DeriveInput) -> TokenStream { Error::new_spanned(ast.generics, "A foreign signal type cannot be generic") .to_compile_error()