diff --git a/crates/musli-common/src/macros.rs b/crates/musli-common/src/macros.rs index 986713354..23fd9c215 100644 --- a/crates/musli-common/src/macros.rs +++ b/crates/musli-common/src/macros.rs @@ -177,3 +177,114 @@ macro_rules! encoding_impls { $crate::encode_with_extensions!($mode); }; } + +#[doc(hidden)] +#[macro_export] +macro_rules! test_include_if { + (#[musli_value] => $($rest:tt)*) => { $($rest)* }; + (=> $($_:tt)*) => {}; +} + +/// Generate test functions which provides rich diagnostics when they fail. +#[doc(hidden)] +#[macro_export] +#[allow(clippy::crate_in_macro_def)] +macro_rules! test_fns { + ($what:expr $(, $(#[$option:ident])*)?) => { + /// Roundtrip encode the given value. + #[doc(hidden)] + #[track_caller] + #[cfg(feature = "test")] + pub fn rt(value: T) -> T + where + T: ::musli::en::Encode + ::musli::de::DecodeOwned + ::core::fmt::Debug + ::core::cmp::PartialEq, + { + const WHAT: &str = $what; + const ENCODING: crate::Encoding = crate::Encoding::new(); + + use ::core::any::type_name; + use ::alloc::string::ToString; + + let format_error = |cx: &crate::context::SystemContext<_, _>| { + use ::alloc::vec::Vec; + + let mut errors = Vec::new(); + + for error in cx.errors() { + errors.push(error.to_string()); + } + + errors.join("\n") + }; + + let mut buf = crate::allocator::buffer(); + let alloc = crate::allocator::new(&mut buf); + let mut cx = crate::context::SystemContext::new(&alloc); + cx.include_type(); + + let out = match ENCODING.to_vec_with(&cx, &value) { + Ok(out) => out, + Err(..) => { + let error = format_error(&cx); + panic!("{WHAT}: {}: failed to encode:\n{error}", type_name::()) + } + }; + + let decoded: T = match ENCODING.from_slice_with(&cx, out.as_slice()) { + Ok(decoded) => decoded, + Err(..) => { + let error = format_error(&cx); + panic!("{WHAT}: {}: failed to decode:\n{error}", type_name::()) + } + }; + + assert_eq!(decoded, value, "{WHAT}: {}: roundtrip does not match", type_name::()); + + $crate::test_include_if! { + $($(#[$option])*)* => + let value_decode: ::musli_value::Value = match ENCODING.from_slice_with(&cx, out.as_slice()) { + Ok(decoded) => decoded, + Err(..) => { + let error = format_error(&cx); + panic!("{WHAT}: {}: failed to decode to value type:\n{error}", type_name::()) + } + }; + + let value_decoded: T = match ::musli_value::decode_with(&cx, &value_decode) { + Ok(decoded) => decoded, + Err(..) => { + let error = format_error(&cx); + panic!("{WHAT}: {}: failed to decode from value type:\n{error}", type_name::()) + } + }; + + assert_eq!(value_decoded, value, "{WHAT}: {}: musli-value roundtrip does not match", type_name::()); + } + + decoded + } + + /// Encode and then decode the given value once. + #[doc(hidden)] + #[track_caller] + #[cfg(feature = "test")] + pub fn decode(value: T) -> T + where + T: ::musli::en::Encode + ::musli::de::DecodeOwned + ::core::fmt::Debug + ::core::cmp::PartialEq, + { + const WHAT: &str = $what; + + use ::core::any::type_name; + + let out = match crate::to_vec(&value) { + Ok(out) => out, + Err(err) => panic!("{WHAT}: {}: failed to encode: {err}", type_name::()), + }; + + match crate::from_slice(out.as_slice()) { + Ok(decoded) => decoded, + Err(err) => panic!("{WHAT}: {}: failed to decode: {err}", type_name::()), + } + } + } +} diff --git a/crates/musli-descriptive/Cargo.toml b/crates/musli-descriptive/Cargo.toml index 71f1d2c6b..4918ff7f6 100644 --- a/crates/musli-descriptive/Cargo.toml +++ b/crates/musli-descriptive/Cargo.toml @@ -23,7 +23,7 @@ rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"] default = ["std", "alloc", "simdutf8", "musli-value"] std = ["musli/std", "musli-common/std", "musli-storage/std", "musli-value?/std"] alloc = ["musli/alloc", "musli-common/alloc", "musli-storage/alloc", "musli-value?/alloc"] -test = [] +test = ["musli-value"] simdutf8 = ["musli-common/simdutf8"] [dependencies] diff --git a/crates/musli-descriptive/src/test.rs b/crates/musli-descriptive/src/test.rs index f73689e38..bba7553aa 100644 --- a/crates/musli-descriptive/src/test.rs +++ b/crates/musli-descriptive/src/test.rs @@ -41,30 +41,7 @@ where } } -/// Roundtrip encode the given value. -#[macro_export] -#[doc(hidden)] -macro_rules! rt { - ($enum:ident :: $variant:ident $($tt:tt)?) => { - $crate::rt!($enum, $enum :: $variant $($tt)*) - }; - - ($struct:ident $($tt:tt)?) => { - $crate::rt!($struct, $struct $($tt)*) - }; - - ($ty:ty, $expr:expr) => {{ - let value: $ty = $expr; - let out = $crate::to_vec(&value).expect(concat!("descriptive: ", stringify!($ty), ": failed to encode")); - let decoded: $ty = $crate::from_slice(out.as_slice()).expect(concat!("descriptive: ", stringify!($ty), ": failed to decode")); - assert_eq!(decoded, $expr, concat!("descriptive: ", stringify!($ty), ": roundtrip does not match")); - - let value_decode: musli_value::Value = $crate::from_slice(out.as_slice()).expect(concat!("descriptive: ", stringify!($ty), ": failed to decode into value type")); - let value_decoded: $ty = musli_value::decode(&value_decode).expect(concat!("descriptive: ", stringify!($ty), ": failed to decode from value type")); - assert_eq!(value_decoded, $expr, concat!("descriptive: ", stringify!($ty), ": value roundtrip does not match")); - decoded - }}; -} +musli_common::test_fns!("descriptive", #[musli_value]); /// Encode a type as one and decode as another. #[inline(never)] diff --git a/crates/musli-json/src/test.rs b/crates/musli-json/src/test.rs index 67a1b8c84..3c0606909 100644 --- a/crates/musli-json/src/test.rs +++ b/crates/musli-json/src/test.rs @@ -5,30 +5,7 @@ use core::fmt::Debug; use musli::mode::DefaultMode; use musli::{Decode, Encode}; -/// Roundtrip encode the given value. -#[macro_export] -#[doc(hidden)] -macro_rules! rt { - ($enum:ident :: $variant:ident $($tt:tt)?) => { - $crate::rt!($enum, $enum :: $variant $($tt)*) - }; - - ($struct:ident $($tt:tt)?) => { - $crate::rt!($struct, $struct $($tt)*) - }; - - ($ty:ty, $expr:expr) => {{ - let value: $ty = $expr; - let out = $crate::to_vec(&value).expect(concat!("json: ", stringify!($ty), ": failed to encode")); - let decoded: $ty = $crate::from_slice(out.as_slice()).expect(concat!("json: ", stringify!($ty), ": failed to decode")); - assert_eq!(decoded, $expr, concat!("json: ", stringify!($ty), ": roundtrip does not match")); - - let value_decode: musli_value::Value = $crate::from_slice(out.as_slice()).expect(concat!("json: ", stringify!($ty), ": failed to decode into value type")); - let value_decoded: $ty = musli_value::decode(&value_decode).expect(concat!("json: ", stringify!($ty), ": failed to decode from value type")); - assert_eq!(value_decoded, $expr, concat!("json: ", stringify!($ty), ": value roundtrip does not match")); - decoded - }}; -} +musli_common::test_fns!("json"); /// Encode a type as one and decode as another. #[inline(never)] diff --git a/crates/musli-macros/src/de.rs b/crates/musli-macros/src/de.rs index 0177d5bb5..fcfb71f78 100644 --- a/crates/musli-macros/src/de.rs +++ b/crates/musli-macros/src/de.rs @@ -1,5 +1,5 @@ use proc_macro2::{Ident, Literal, Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::Token; @@ -7,7 +7,7 @@ use syn::Token; use crate::expander::{Result, TagMethod}; use crate::internals::apply; use crate::internals::attr::{EnumTag, EnumTagging, Packing}; -use crate::internals::build::{Body, Build, BuildData, Enum, Field, Variant}; +use crate::internals::build::{Body, Build, BuildData, Enum, Field, FieldSkip, Variant}; use crate::internals::tokens::Tokens; struct Ctxt<'a> { @@ -745,70 +745,81 @@ fn decode_tagged( let type_name = &st.name; - let mut patterns = Vec::with_capacity(st.fields.len()); + let mut patterns = Vec::with_capacity(st.all_fields.len()); let mut assigns = Punctuated::<_, Token![,]>::new(); let mut fields_with = Vec::new(); - for f in &st.fields { + for f in &st.all_fields { let tag = &f.tag; let var = &f.var; let decode_path = &f.decode_path.1; - let formatted_tag = match &st.name_format_with { - Some((_, path)) => quote!(&#path(&#tag)), - None => quote!(&#tag), - }; + let expr = match &f.skip { + Some(FieldSkip::Default(span)) => { + let ty = f.ty; + syn::Expr::Verbatim(quote_spanned!(*span => #default_function::<#ty>())) + } + Some(FieldSkip::Expr(expr)) => expr.clone(), + None => { + let formatted_tag = match &st.name_format_with { + Some((_, path)) => quote!(&#path(&#tag)), + None => quote!(&#tag), + }; - let enter = cx.trace.then(|| { - let (name, enter) = match &f.member { - syn::Member::Named(name) => ( - syn::Lit::Str(syn::LitStr::new(&name.to_string(), name.span())), - Ident::new("enter_named_field", Span::call_site()), - ), - syn::Member::Unnamed(index) => ( - syn::Lit::Int(syn::LitInt::from(Literal::u32_suffixed(index.index))), - Ident::new("enter_unnamed_field", Span::call_site()), - ), - }; + let enter = cx.trace.then(|| { + let (name, enter) = match &f.member { + syn::Member::Named(name) => ( + syn::Lit::Str(syn::LitStr::new(&name.to_string(), name.span())), + Ident::new("enter_named_field", Span::call_site()), + ), + syn::Member::Unnamed(index) => ( + syn::Lit::Int(syn::LitInt::from(Literal::u32_suffixed(index.index))), + Ident::new("enter_unnamed_field", Span::call_site()), + ), + }; - quote! { - #context_t::#enter(#ctx_var, #name, #formatted_tag); - } - }); + quote! { + #context_t::#enter(#ctx_var, #name, #formatted_tag); + } + }); - let leave = cx.trace.then(|| { - quote! { - #context_t::leave_field(#ctx_var); - } - }); + let leave = cx.trace.then(|| { + quote! { + #context_t::leave_field(#ctx_var); + } + }); - let decode = quote! { - #var = #option_some(#decode_path(#ctx_var, #struct_decoder_var)?); - }; + let decode = quote! { + #var = #option_some(#decode_path(#ctx_var, #struct_decoder_var)?); + }; - fields_with.push((f, decode, (enter, leave))); + fields_with.push((f, decode, (enter, leave))); - let fallback = if f.default_attr.is_some() { - quote!(#default_function()) - } else { - quote! { - return #result_err(#context_t::expected_tag(#ctx_var, #type_name, &#tag)) + let fallback = if f.default_attr.is_some() { + quote!(#default_function()) + } else { + quote! { + return #result_err(#context_t::expected_tag(#ctx_var, #type_name, &#tag)) + } + }; + + let var = &f.var; + + syn::Expr::Verbatim(quote! { + match #var { + #option_some(#var) => #var, + #option_none => #fallback, + } + }) } }; - let var = &f.var; - assigns.push(syn::FieldValue { attrs: Vec::new(), member: f.member.clone(), colon_token: Some(::default()), - expr: syn::Expr::Verbatim(quote! { - match #var { - #option_some(#var) => #var, - #option_none => #fallback, - } - }), + expr, }); } @@ -956,11 +967,12 @@ fn decode_tagged( }); let path = &st.path; - let fields_len = st.fields.len(); + let fields_len = st.unskipped_fields.len(); let decls = st - .fields + .unskipped_fields .iter() + .map(|f| &**f) .map(|Field { var, ty, .. }| quote!(let mut #var: #option<#ty> = #option_none;)); let enter = (cx.trace && cx.trace_body).then(|| { @@ -1001,8 +1013,8 @@ fn decode_transparent(cx: &Ctxt<'_>, e: &Build<'_>, st_: &Body<'_>) -> Result, e: &Build<'_>, st_: &Body<'_>) -> Result, e: &Build<'_>, st: &Body<'_>) -> Result { - let f = match &st.fields[..] { + let f = match &st.unskipped_fields[..] { [f] => f, _ => { - e.transparent_diagnostics(st.span, &st.fields); + e.transparent_diagnostics(st.span, &st.unskipped_fields); return Err(()); } }; @@ -129,7 +129,7 @@ fn encode_struct(cx: &Ctxt<'_>, e: &Build<'_>, st: &Body<'_>) -> Result { - let len = length_test(st.fields.len(), &tests); + let len = length_test(st.unskipped_fields.len(), &tests); let decls = tests.iter().map(|t| &t.decl); encode = quote! {{ @@ -196,10 +196,10 @@ fn insert_fields<'st>( let field_encoder_var = e.cx.ident("field_encoder"); let value_encoder_var = e.cx.ident("value_encoder"); - let mut encoders = Vec::with_capacity(st.fields.len()); - let mut tests = Vec::with_capacity(st.fields.len()); + let mut encoders = Vec::with_capacity(st.all_fields.len()); + let mut tests = Vec::with_capacity(st.all_fields.len()); - for f in &st.fields { + for f in &st.unskipped_fields { let encode_path = &f.encode_path.1; let access = &f.self_access; let tag = &f.tag; @@ -350,8 +350,8 @@ fn encode_variant( None => { match v.st.packing { Packing::Transparent => { - let [f] = &v.st.fields[..] else { - b.transparent_diagnostics(v.span, &v.st.fields); + let [f] = &v.st.unskipped_fields[..] else { + b.transparent_diagnostics(v.span, &v.st.unskipped_fields); return Err(()); }; @@ -372,7 +372,7 @@ fn encode_variant( } Packing::Tagged => { let decls = tests.iter().map(|t| &t.decl); - let len = length_test(v.st.fields.len(), &tests); + let len = length_test(v.st.unskipped_fields.len(), &tests); encode = quote! {{ #encoder_t::encode_struct_fn(#encoder_var, #len, move |#encoder_var| { @@ -432,7 +432,7 @@ fn encode_variant( let decls = tests.iter().map(|t| &t.decl); - let len = length_test(v.st.fields.len(), &tests); + let len = length_test(v.st.unskipped_fields.len(), &tests); let struct_encoder = b.cx.ident("struct_encoder"); let content_struct = b.cx.ident("content_struct"); let pair = b.cx.ident("pair"); diff --git a/crates/musli-macros/src/internals/attr.rs b/crates/musli-macros/src/internals/attr.rs index 7bcf408fd..d6d76da23 100644 --- a/crates/musli-macros/src/internals/attr.rs +++ b/crates/musli-macros/src/internals/attr.rs @@ -12,6 +12,8 @@ use crate::expander::TagMethod; use crate::internals::ATTR; use crate::internals::{Ctxt, Mode}; +use super::build::FieldSkip; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) enum Only { Encode, @@ -572,6 +574,8 @@ layer! { rename: syn::Expr, /// Use a default value for the field if it's not available. is_default: (), + /// Use a default value for the field if it's not available. + skip: FieldSkip, /// Use the alternate TraceDecode for the field. trace: (), /// Use the alternate EncodeBytes for the field. @@ -696,6 +700,18 @@ pub(crate) fn field_attrs(cx: &Ctxt, attrs: &[syn::Attribute]) -> Field { return Ok(()); } + // parse #[musli(skip [= ])] + if meta.path.is_ident("skip") { + let skip = if meta.input.parse::>()?.is_some() { + FieldSkip::Expr(meta.input.parse()?) + } else { + FieldSkip::Default(meta.path.span()) + }; + + new.skip.push((meta.path.span(), skip)); + return Ok(()); + } + // parse #[musli(trace)] if meta.path.is_ident("trace") { new.trace.push((meta.path.span(), ())); diff --git a/crates/musli-macros/src/internals/build.rs b/crates/musli-macros/src/internals/build.rs index eeb3ea7e2..7788a0844 100644 --- a/crates/musli-macros/src/internals/build.rs +++ b/crates/musli-macros/src/internals/build.rs @@ -1,4 +1,5 @@ use std::collections::BTreeSet; +use std::rc::Rc; use proc_macro2::Span; use syn::punctuated::Punctuated; @@ -57,11 +58,11 @@ impl Build<'_> { /// Emit diagnostics for a transparent encode / decode that failed because /// the wrong number of fields existed. - pub(crate) fn transparent_diagnostics(&self, span: Span, fields: &[Field]) { + pub(crate) fn transparent_diagnostics(&self, span: Span, fields: &[Rc]) { if fields.is_empty() { self.cx.error_span( span, - format_args!("#[{ATTR}(transparent)] types must have a single field",), + format_args!("#[{ATTR}(transparent)] types must have a single unskipped field"), ); } else { self.cx.error_span( @@ -113,7 +114,8 @@ pub(crate) enum BuildData<'a> { pub(crate) struct Body<'a> { pub(crate) span: Span, pub(crate) name: &'a syn::LitStr, - pub(crate) fields: Vec>, + pub(crate) unskipped_fields: Vec>>, + pub(crate) all_fields: Vec>>, pub(crate) name_type: Option<&'a (Span, syn::Type)>, pub(crate) name_format_with: Option<&'a (Span, syn::Path)>, pub(crate) packing: Packing, @@ -161,6 +163,14 @@ pub(crate) struct Variant<'a> { pub(crate) patterns: Punctuated, } +/// Field skip configuration. +pub(crate) enum FieldSkip { + /// Skip with a default value. + Default(Span), + /// Skip with an expression. + Expr(syn::Expr), +} + pub(crate) struct Field<'a> { pub(crate) span: Span, pub(crate) index: usize, @@ -168,7 +178,11 @@ pub(crate) struct Field<'a> { pub(crate) decode_path: (Span, syn::Path), pub(crate) tag: syn::Expr, pub(crate) skip_encoding_if: Option<&'a (Span, syn::Path)>, + /// Fill with default value, if missing. pub(crate) default_attr: Option, + /// Skip field entirely and always initialize with the specified expresion, + /// or default value if none is specified. + pub(crate) skip: Option<&'a FieldSkip>, pub(crate) self_access: syn::Expr, pub(crate) member: syn::Member, pub(crate) packing: Packing, @@ -210,7 +224,8 @@ pub(crate) fn setup<'a>( } fn setup_struct<'a>(e: &'a Expander, mode: Mode<'_>, data: &'a StructData<'a>) -> Result> { - let mut fields = Vec::with_capacity(data.fields.len()); + let mut unskipped_fields = Vec::with_capacity(data.fields.len()); + let mut all_fields = Vec::with_capacity(data.fields.len()); let default_field = e.type_attr.default_field(mode).map(|&(_, v)| v); let packing = e @@ -222,7 +237,7 @@ fn setup_struct<'a>(e: &'a Expander, mode: Mode<'_>, data: &'a StructData<'a>) - let mut tag_methods = TagMethods::new(&e.cx); for f in &data.fields { - fields.push(setup_field( + let field = Rc::new(setup_field( e, mode, f, @@ -231,12 +246,19 @@ fn setup_struct<'a>(e: &'a Expander, mode: Mode<'_>, data: &'a StructData<'a>) - None, &mut tag_methods, )?); + + if field.skip.is_none() { + unskipped_fields.push(field.clone()); + } + + all_fields.push(field); } Ok(Body { span: data.span, name: &data.name, - fields, + unskipped_fields, + all_fields, name_type: e.type_attr.name_type(mode), name_format_with: e.type_attr.name_format_with(mode), packing, @@ -297,7 +319,8 @@ fn setup_variant<'a>( fallback: &mut Option<&'a syn::Ident>, tag_methods: &mut TagMethods, ) -> Result> { - let mut fields = Vec::with_capacity(data.fields.len()); + let mut unskipped_fields = Vec::with_capacity(data.fields.len()); + let mut all_fields = Vec::with_capacity(data.fields.len()); let variant_packing = data .attr @@ -346,7 +369,7 @@ fn setup_variant<'a>( let mut field_tag_methods = TagMethods::new(&e.cx); for f in &data.fields { - fields.push(setup_field( + let field = Rc::new(setup_field( e, mode, f, @@ -355,6 +378,12 @@ fn setup_variant<'a>( Some(&mut patterns), &mut field_tag_methods, )?); + + if field.skip.is_none() { + unskipped_fields.push(field.clone()); + } + + all_fields.push(field); } Ok(Variant { @@ -366,7 +395,8 @@ fn setup_variant<'a>( st: Body { span: data.span, name: &data.name, - fields, + unskipped_fields, + all_fields, packing: variant_packing, name_type: data.attr.name_type(mode), name_format_with: data.attr.name_format_with(mode), @@ -391,6 +421,7 @@ fn setup_field<'a>( tag_methods.insert(data.span, tag_method); let skip_encoding_if = data.attr.skip_encoding_if(mode); let default_attr = data.attr.is_default(mode).map(|&(s, ())| s); + let skip = data.attr.skip(mode).map(|(_, skip)| skip); let member = match data.ident { Some(ident) => syn::Member::Named(ident.clone()), @@ -476,6 +507,7 @@ fn setup_field<'a>( tag, skip_encoding_if, default_attr, + skip, self_access, member, packing, diff --git a/crates/musli-storage/src/test.rs b/crates/musli-storage/src/test.rs index 6c03503dd..14dd8e86e 100644 --- a/crates/musli-storage/src/test.rs +++ b/crates/musli-storage/src/test.rs @@ -5,26 +5,7 @@ use core::fmt::Debug; use musli::mode::DefaultMode; use musli::{Decode, Encode}; -/// Roundtrip encode the given value. -#[macro_export] -#[doc(hidden)] -macro_rules! rt { - ($enum:ident :: $variant:ident $($tt:tt)?) => { - $crate::rt!($enum, $enum :: $variant $($tt)*) - }; - - ($struct:ident $($tt:tt)?) => { - $crate::rt!($struct, $struct $($tt)*) - }; - - ($ty:ty, $expr:expr) => {{ - let value: $ty = $expr; - let out = $crate::to_vec(&value).expect(concat!("storage: ", stringify!($ty), ": failed to encode")); - let decoded: $ty = $crate::from_slice(out.as_slice()).expect(concat!("storage: ", stringify!($ty), ": failed to decode")); - assert_eq!(decoded, $expr, concat!("storage: ", stringify!($ty), ": roundtrip does not match")); - decoded - }}; -} +musli_common::test_fns!("storage"); /// Encode a type as one and decode as another. #[inline(never)] diff --git a/crates/musli-value/src/lib.rs b/crates/musli-value/src/lib.rs index e6aa0e052..8567c7abc 100644 --- a/crates/musli-value/src/lib.rs +++ b/crates/musli-value/src/lib.rs @@ -65,3 +65,15 @@ where let cx = musli_common::exports::context::Same::<_, DefaultMode, Error>::new(&alloc); value.decoder::(&cx).decode() } + +/// Decode a [Value] into a type which implements [Decode] using a custom +/// context. +pub fn decode_with<'de, C, T>(cx: &C, value: &'de Value) -> Result +where + C: ?Sized + musli::Context, + T: Decode<'de, C::Mode>, +{ + use musli::de::Decoder; + + value.decoder::(cx).decode() +} diff --git a/crates/musli-wire/src/test.rs b/crates/musli-wire/src/test.rs index 6c132ffde..d725429c7 100644 --- a/crates/musli-wire/src/test.rs +++ b/crates/musli-wire/src/test.rs @@ -41,26 +41,7 @@ where } } -/// Roundtrip encode the given value. -#[macro_export] -#[doc(hidden)] -macro_rules! rt { - ($enum:ident :: $variant:ident $($tt:tt)?) => { - $crate::rt!($enum, $enum :: $variant $($tt)*) - }; - - ($struct:ident $($tt:tt)?) => { - $crate::rt!($struct, $struct $($tt)*) - }; - - ($ty:ty, $expr:expr) => {{ - let value: $ty = $expr; - let out = $crate::to_vec(&value).expect(concat!("wire: ", stringify!($ty), ": failed to encode")); - let decoded: $ty = $crate::from_slice(out.as_slice()).expect(concat!("wire: ", stringify!($ty), ": failed to decode")); - assert_eq!(decoded, $expr, concat!("wire: ", stringify!($ty), ": roundtrip does not match")); - decoded - }}; -} +musli_common::test_fns!("wire"); /// Encode a type as one and decode as another. #[inline(never)] diff --git a/crates/musli/src/derives.rs b/crates/musli/src/derives.rs index 95355b63d..c82d2d954 100644 --- a/crates/musli/src/derives.rs +++ b/crates/musli/src/derives.rs @@ -533,6 +533,8 @@ //! struct Struct { //! #[musli(rename = "other")] //! something: String, +//! #[musli(skip = 42)] +//! skipped_field: u32, //! } //! //! #[derive(Encode, Decode)] @@ -547,6 +549,32 @@ //! //!
//! +//! #### `#[musli(skip [= ])]` +//! +//! This attribute means that the entire field is skipped. If a field is decoded +//! it either uses the provided `` to construct the value, or tries and +//! initialize it with [`Default::default`]. +//! +//!
+//! +//! #### `#[musli(default)]` +//! +//! This constructs the field using [`Default::default`] in case it's not +//! available. This is only used when a field is missing during decoding. +//! +//! ``` +//! use musli::{Encode, Decode}; +//! +//! #[derive(Encode, Decode)] +//! struct Person { +//! name: String, +//! #[musli(default)] +//! age: Option, +//! } +//! ``` +//! +//!
+//! //! #### `#[musli(rename = ..)]` //! //! This allows for renaming a field from its default value. It can take any @@ -729,24 +757,6 @@ //! //!
//! -//! #### `#[musli(default)]` -//! -//! This constructs the field using [Default::default] in case it's not -//! available. This is only used when a field is missing during decoding. -//! -//! ``` -//! use musli::{Encode, Decode}; -//! -//! #[derive(Encode, Decode)] -//! struct Person { -//! name: String, -//! #[musli(default)] -//! age: Option, -//! } -//! ``` -//! -//!
-//! //! #### `#[musli(skip_encoding_if = )]` //! //! This adds a condition to skip encoding a field entirely if the condition is diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 295ce3657..39c0062f9 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -60,32 +60,41 @@ pub mod json { #[macro_export] #[doc(hidden)] macro_rules! rt { - ($enum:ident :: $variant:ident $($tt:tt)?) => { - $crate::rt!($enum, $enum :: $variant $($tt)*) - }; + ($expr:expr) => {{ + let expected = $crate::rt_no_json!($expr); - ($struct:ident $($tt:tt)?) => { - $crate::rt!($struct, $struct $($tt)*) - }; + #[cfg(feature = "musli-json")] + { + let json = ::musli_json::test::rt($expr); + assert_eq!(expected, json); + } + + expected + }}; +} - ($ty:ty, $expr:expr) => {{ +/// Roundtrip the given expression through all supported formats except JSON. +#[macro_export] +#[doc(hidden)] +macro_rules! rt_no_json { + ($expr:expr) => {{ let expected = $expr; #[cfg(feature = "musli-storage")] { - let storage = ::musli_storage::rt!($ty, $expr); + let storage = ::musli_storage::test::rt($expr); assert_eq!(expected, storage); } #[cfg(feature = "musli-wire")] { - let wire = ::musli_wire::rt!($ty, $expr); + let wire = ::musli_wire::test::rt($expr); assert_eq!(expected, wire); } #[cfg(feature = "musli-descriptive")] { - let descriptive = ::musli_descriptive::rt!($ty, $expr); + let descriptive = ::musli_descriptive::test::rt($expr); assert_eq!(expected, descriptive); } @@ -93,6 +102,50 @@ macro_rules! rt { }}; } +/// This is used to test when there is a decode assymmetry, such as the decoded +/// value does not match the encoded one due to things such as skipped fields. +#[macro_export] +#[doc(hidden)] +macro_rules! assert_decode_eq { + ($expr:expr, $expected:expr) => {{ + #[cfg(feature = "musli-storage")] + { + let storage = ::musli_storage::test::decode($expr); + assert_eq!( + storage, $expected, + "storage: decoded value does not match expected" + ); + } + + #[cfg(feature = "musli-wire")] + { + let wire = ::musli_wire::test::decode($expr); + assert_eq!( + wire, $expected, + "wire: decoded value does not match expected" + ); + } + + #[cfg(feature = "musli-descriptive")] + { + let descriptive = ::musli_descriptive::test::decode($expr); + assert_eq!( + descriptive, $expected, + "descriptive: decoded value does not match expected" + ); + } + + #[cfg(feature = "musli-json")] + { + let json = ::musli_json::test::decode($expr); + assert_eq!( + json, $expected, + "json: decoded value does not match expected" + ); + } + }}; +} + /// Call the given macro with the existing feature matrix. #[macro_export] macro_rules! feature_matrix { diff --git a/crates/tests/tests/builtin.rs b/crates/tests/tests/builtin.rs index e018547bd..4125f5201 100644 --- a/crates/tests/tests/builtin.rs +++ b/crates/tests/tests/builtin.rs @@ -1,4 +1,4 @@ #[test] fn vec() { - tests::rt!(Vec::, vec![1, 2, 3, 4]); + tests::rt!(vec![1, 2, 3, 4]); } diff --git a/crates/tests/tests/complex_field.rs b/crates/tests/tests/complex_field.rs index fbc9bba6a..dc2d16e9f 100644 --- a/crates/tests/tests/complex_field.rs +++ b/crates/tests/tests/complex_field.rs @@ -54,7 +54,7 @@ fn bytes_tag_vec() { #[test] fn custom_struct_tag() { - tests::rt!(StructCustomFieldAsStruct { + tests::rt_no_json!(StructCustomFieldAsStruct { field1: 42, field2: 84, }); @@ -71,7 +71,7 @@ pub struct StructCustomTag { #[test] fn custom_tag() { - tests::rt!(StructCustomTag { + tests::rt_no_json!(StructCustomTag { field1: 42, field2: 84, }); @@ -86,7 +86,7 @@ struct StructWithBytesTag { #[test] fn struct_with_bytes_tag() { - tests::rt!(StructWithBytesTag { + tests::rt_no_json!(StructWithBytesTag { string: String::from("Some String"), }); } @@ -105,11 +105,11 @@ enum EnumWithBytesTag { #[test] fn bytes_tag_in_enum() { - tests::rt!(EnumWithBytesTag::Variant1 { + tests::rt_no_json!(EnumWithBytesTag::Variant1 { string: String::from("st"), }); - tests::rt!(EnumWithBytesTag::Variant2 { + tests::rt_no_json!(EnumWithBytesTag::Variant2 { string: String::from("st"), }); } diff --git a/crates/tests/tests/generic_bounds.rs b/crates/tests/tests/generic_bounds.rs index 10d71bfb3..bb1529cc3 100644 --- a/crates/tests/tests/generic_bounds.rs +++ b/crates/tests/tests/generic_bounds.rs @@ -8,10 +8,7 @@ pub struct GenericWithBound { #[test] fn generic_with_bound() { - tests::rt!( - GenericWithBound, - GenericWithBound { - value: String::from("Hello"), - } - ); + tests::rt!(GenericWithBound { + value: String::from("Hello"), + }); } diff --git a/crates/tests/tests/large_enum.rs b/crates/tests/tests/large_enum.rs index 1e88e97b6..a8de0932a 100644 --- a/crates/tests/tests/large_enum.rs +++ b/crates/tests/tests/large_enum.rs @@ -58,13 +58,14 @@ fn large_enum_string_variants() { const IP: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); const IPV6: IpAddr = IpAddr::V6(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8)); - tests::rt!(LargeEnumStringVariants::A(A { id: ID, ip: IP })); - tests::rt!(LargeEnumStringVariants::A(A { id: ID, ip: IPV6 })); - tests::rt!(LargeEnumStringVariants::B(B { + // TODO: Fix this for JSON. + tests::rt_no_json!(LargeEnumStringVariants::A(A { id: ID, ip: IP })); + tests::rt_no_json!(LargeEnumStringVariants::A(A { id: ID, ip: IPV6 })); + tests::rt_no_json!(LargeEnumStringVariants::B(B { id: ID, user_id: USER_ID })); - tests::rt!(LargeEnumStringVariants::C(C { id: ID })); - tests::rt!(LargeEnumStringVariants::D(D { id: ID })); - tests::rt!(LargeEnumStringVariants::E(E { id: ID })); + tests::rt_no_json!(LargeEnumStringVariants::C(C { id: ID })); + tests::rt_no_json!(LargeEnumStringVariants::D(D { id: ID })); + tests::rt_no_json!(LargeEnumStringVariants::E(E { id: ID })); } diff --git a/crates/tests/tests/skip.rs b/crates/tests/tests/skip.rs new file mode 100644 index 000000000..53360334b --- /dev/null +++ b/crates/tests/tests/skip.rs @@ -0,0 +1,30 @@ +#![cfg(feature = "test")] + +use musli::{Decode, Encode}; + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct AllSkipped { + #[musli(skip)] + skip: u32, + #[musli(skip = 42)] + skip_default: u32, +} + +#[test] +fn skip() { + tests::rt!(AllSkipped { + skip: 0, + skip_default: 42, + }); + + tests::assert_decode_eq!( + AllSkipped { + skip: 10, + skip_default: 52, + }, + AllSkipped { + skip: 0, + skip_default: 42, + } + ); +} diff --git a/crates/tests/tests/ui/transparent_error.stderr b/crates/tests/tests/ui/transparent_error.stderr index 64b8a8f8f..01e5bb210 100644 --- a/crates/tests/tests/ui/transparent_error.stderr +++ b/crates/tests/tests/ui/transparent_error.stderr @@ -30,7 +30,7 @@ error: #[musli(transparent)] can only be used on types which have a single field | = note: this error originates in the derive macro `Decode` (in Nightly builds, run with -Z macro-backtrace for more info) -error: #[musli(transparent)] types must have a single field +error: #[musli(transparent)] types must have a single unskipped field --> tests/ui/transparent_error.rs:14:10 | 14 | #[derive(Encode, Decode)] @@ -38,7 +38,7 @@ error: #[musli(transparent)] types must have a single field | = note: this error originates in the derive macro `Encode` (in Nightly builds, run with -Z macro-backtrace for more info) -error: #[musli(transparent)] types must have a single field +error: #[musli(transparent)] types must have a single unskipped field --> tests/ui/transparent_error.rs:14:18 | 14 | #[derive(Encode, Decode)] @@ -46,7 +46,7 @@ error: #[musli(transparent)] types must have a single field | = note: this error originates in the derive macro `Decode` (in Nightly builds, run with -Z macro-backtrace for more info) -error: #[musli(transparent)] types must have a single field +error: #[musli(transparent)] types must have a single unskipped field --> tests/ui/transparent_error.rs:18:10 | 18 | #[derive(Encode, Decode)] @@ -54,7 +54,7 @@ error: #[musli(transparent)] types must have a single field | = note: this error originates in the derive macro `Encode` (in Nightly builds, run with -Z macro-backtrace for more info) -error: #[musli(transparent)] types must have a single field +error: #[musli(transparent)] types must have a single unskipped field --> tests/ui/transparent_error.rs:18:18 | 18 | #[derive(Encode, Decode)] @@ -74,13 +74,13 @@ error: #[musli(transparent)] can only be used on types which have a single field 29 | #[musli(transparent)] | ^ -error: #[musli(transparent)] types must have a single field +error: #[musli(transparent)] types must have a single unskipped field --> tests/ui/transparent_error.rs:31:5 | 31 | #[musli(transparent)] | ^ -error: #[musli(transparent)] types must have a single field +error: #[musli(transparent)] types must have a single unskipped field --> tests/ui/transparent_error.rs:33:5 | 33 | #[musli(transparent)] diff --git a/crates/tests/tests/untagged_variants.rs b/crates/tests/tests/untagged_variants.rs index 2b0e1d227..21a4efc60 100644 --- a/crates/tests/tests/untagged_variants.rs +++ b/crates/tests/tests/untagged_variants.rs @@ -15,7 +15,8 @@ pub enum UntaggedVariants { /// Enums may contain packed variants. #[test] fn untagged_variants() { - tests::rt!(UntaggedVariants::Empty); + // TODO: Fix this for JSON. + tests::rt_no_json!(UntaggedVariants::Empty); tests::rt!(UntaggedVariants::Tuple(42, 84)); tests::rt!(UntaggedVariants::Struct { a: 42, b: 84 }); }