Skip to content

Commit

Permalink
Implement field skipping and improve test feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog committed Mar 30, 2024
1 parent fe808cb commit 9b4d921
Show file tree
Hide file tree
Showing 20 changed files with 402 additions and 211 deletions.
111 changes: 111 additions & 0 deletions crates/musli-common/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(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::<T>())
}
};

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::<T>())
}
};

assert_eq!(decoded, value, "{WHAT}: {}: roundtrip does not match", type_name::<T>());

$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::<T>())
}
};

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::<T>())
}
};

assert_eq!(value_decoded, value, "{WHAT}: {}: musli-value roundtrip does not match", type_name::<T>());
}

decoded
}

/// Encode and then decode the given value once.
#[doc(hidden)]
#[track_caller]
#[cfg(feature = "test")]
pub fn decode<T>(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::<T>()),
};

match crate::from_slice(out.as_slice()) {
Ok(decoded) => decoded,
Err(err) => panic!("{WHAT}: {}: failed to decode: {err}", type_name::<T>()),
}
}
}
}
2 changes: 1 addition & 1 deletion crates/musli-descriptive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
25 changes: 1 addition & 24 deletions crates/musli-descriptive/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
25 changes: 1 addition & 24 deletions crates/musli-json/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
112 changes: 62 additions & 50 deletions crates/musli-macros/src/de.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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;

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> {
Expand Down Expand Up @@ -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(<Token![:]>::default()),
expr: syn::Expr::Verbatim(quote! {
match #var {
#option_some(#var) => #var,
#option_none => #fallback,
}
}),
expr,
});
}

Expand Down Expand Up @@ -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(|| {
Expand Down Expand Up @@ -1001,8 +1013,8 @@ fn decode_transparent(cx: &Ctxt<'_>, e: &Build<'_>, st_: &Body<'_>) -> Result<To
..
} = *cx;

let [f] = &st_.fields[..] else {
e.transparent_diagnostics(st_.span, &st_.fields);
let [f] = &st_.unskipped_fields[..] else {
e.transparent_diagnostics(st_.span, &st_.unskipped_fields);
return Err(());
};

Expand Down Expand Up @@ -1060,7 +1072,7 @@ fn decode_packed(cx: &Ctxt<'_>, e: &Build<'_>, st_: &Body<'_>) -> Result<TokenSt

let mut assign = Vec::new();

for f in &st_.fields {
for f in &st_.unskipped_fields {
if let Some(span) = f.default_attr {
e.packed_default_diagnostics(span);
}
Expand Down
Loading

0 comments on commit 9b4d921

Please sign in to comment.