Skip to content

Commit

Permalink
deer: implement Deserialize for tuples (#2418)
Browse files Browse the repository at this point in the history
* feat: glue helper trait `TupleExt`

* feat: take code from #1875 and fix

* test: array impl

* fix: miri
  • Loading branch information
indietyp authored Apr 28, 2023
1 parent 0581e25 commit 46c5058
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 0 deletions.
1 change: 1 addition & 0 deletions libs/deer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ similar-asserts = { version = "1.4.2", features = ['serde'] }
deer-desert = { path = "./desert", features = ['pretty'] }
proptest = "1.1.0"
paste = "1.0.12"
seq-macro = "0.3.3"

[build-dependencies]
rustc_version = "0.4.0"
Expand Down
69 changes: 69 additions & 0 deletions libs/deer/src/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Temporary helper trait for folding reports until [#2377](https://github.com/hashintel/hash/discussions/2377)
//! is resolved and implemented.

use error_stack::{Context, Report};

pub(crate) trait TupleExt {
type Context: Context;
type Ok;

fn fold_reports(self) -> Result<Self::Ok, Report<Self::Context>>;
}

#[rustfmt::skip]
macro_rules! all_the_tuples {
($name:ident) => {
$name!([T1], T2);
$name!([T1, T2], T3);
$name!([T1, T2, T3], T4);
$name!([T1, T2, T3, T4], T5);
$name!([T1, T2, T3, T4, T5], T6);
$name!([T1, T2, T3, T4, T5, T6], T7);
$name!([T1, T2, T3, T4, T5, T6, T7], T8);
$name!([T1, T2, T3, T4, T5, T6, T7, T8], T9);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], T14);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], T15);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
};
}

impl<T1, C: Context> TupleExt for (Result<T1, Report<C>>,) {
type Context = C;
type Ok = (T1,);

fn fold_reports(self) -> Result<Self::Ok, Report<Self::Context>> {
self.0.map(|value| (value,))
}
}

macro_rules! impl_tuple_ext {
([$($elem:ident),*], $other:ident) => {
#[allow(non_snake_case)]
impl<C: Context $(, $elem)*, $other> TupleExt for ($(Result<$elem, Report<C>>, )* Result<$other, Report<C>>) {
type Context = C;
type Ok = ($($elem ,)* $other);

fn fold_reports(self) -> Result<Self::Ok, Report<Self::Context>> {
let ( $($elem ,)* $other ) = self;

let lhs = ( $($elem ,)* ).fold_reports();

match (lhs, $other) {
(Ok(( $($elem ,)* )), Ok(rhs)) => Ok(($($elem ,)* rhs)),
(Ok(_), Err(err)) | (Err(err), Ok(_)) => Err(err),
(Err(mut lhs), Err(rhs)) => {
lhs.extend_one(rhs);

Err(lhs)
}
}
}
}
};
}

all_the_tuples!(impl_tuple_ext);
1 change: 1 addition & 0 deletions libs/deer/src/impls/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ mod result;
mod string;
mod sync;
mod time;
mod tuples;
mod unit;
115 changes: 115 additions & 0 deletions libs/deer/src/impls/core/tuples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use core::marker::PhantomData;

use error_stack::{Report, Result, ResultExt};

use crate::{
error::{
ArrayLengthError, DeserializeError, ExpectedLength, Location, ReceivedLength, Variant,
VisitorError,
},
ext::TupleExt,
ArrayAccess, Deserialize, Deserializer, Document, Reflection, Schema, Visitor,
};

#[rustfmt::skip]
macro_rules! all_the_tuples {
($name:ident) => {
$name!( 1, V01, R01; T1);
$name!( 2, V02, R02; T1, T2);
$name!( 3, V03, R03; T1, T2, T3);
$name!( 4, V04, R04; T1, T2, T3, T4);
$name!( 5, V05, R05; T1, T2, T3, T4, T5);
$name!( 6, V06, R06; T1, T2, T3, T4, T5, T6);
$name!( 7, V07, R07; T1, T2, T3, T4, T5, T6, T7);
$name!( 8, V08, R08; T1, T2, T3, T4, T5, T6, T7, T8);
$name!( 9, V09, R09; T1, T2, T3, T4, T5, T6, T7, T8, T9);
$name!(10, V10, R10; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
$name!(11, V11, R11; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
$name!(12, V12, R12; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
$name!(13, V13, R13; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
$name!(14, V14, R14; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
$name!(15, V15, R15; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
$name!(16, V16, R16; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
};
}

macro_rules! impl_tuple {
($(#[$meta:meta])* $expected:literal, $visitor:ident, $reflection:ident; $($elem:ident),*) => {
pub struct $reflection<$($elem: ?Sized,)*>($(PhantomData<fn() -> *const $elem>,)*);

impl<$($elem,)*> Reflection for $reflection<$($elem,)*>
where
$($elem: Reflection + ?Sized),*
{
fn schema(doc: &mut Document) -> Schema {
Schema::new("array")
.with("prefixItems", [$(doc.add::<$elem>()),*])
.with("items", false)
}
}


// we do not use &'de as the return type, as that would mean that `Deserialize<'de>`
// must be `'de`, which we cannot guarantee
struct $visitor<$($elem,)*>(PhantomData<fn() -> *const ( $($elem ,)* )>);


#[automatically_derived]
impl<'de, $($elem,)*> Visitor<'de> for $visitor<$($elem,)*>
where
$($elem: Deserialize<'de>),*
{
type Value = ($($elem,)*);

fn expecting(&self) -> Document {
Self::Value::reflection()
}

#[allow(non_snake_case)]
fn visit_array<T>(self, mut v: T) -> Result<Self::Value, VisitorError>
where
T: ArrayAccess<'de>,
{
v.set_bounded($expected).change_context(VisitorError)?;

let mut length = 0;

$(
let $elem = match v.next() {
None => {
return Err(Report::new(ArrayLengthError.into_error())
.attach(ExpectedLength::new($expected))
.attach(ReceivedLength::new(length))
.change_context(VisitorError));
}
Some(value) => value.attach(Location::Tuple(length)),
};

length += 1;
)*

let value = ($($elem,)*).fold_reports();

(value, v.end())
.fold_reports()
.map(|(value, _)| value)
.change_context(VisitorError)
}
}

$(#[$meta])*
impl<'de, $($elem,)*> Deserialize<'de> for ($($elem,)*)
where
$($elem: Deserialize<'de>),*
{
type Reflection = $reflection<$($elem::Reflection),*>;

fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, DeserializeError> {
de.deserialize_array($visitor::<$($elem,)*>(PhantomData))
.change_context(DeserializeError)
}
}
};
}

all_the_tuples!(impl_tuple);
1 change: 1 addition & 0 deletions libs/deer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub mod error;
mod impls;
#[macro_use]
mod macros;
mod ext;
mod number;
pub mod schema;
pub mod value;
Expand Down
105 changes: 105 additions & 0 deletions libs/deer/tests/test_impls_core_tuples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use deer::Deserialize;
use deer_desert::{assert_tokens, assert_tokens_error, error, Token};
use proptest::prelude::*;
use seq_macro::seq;
use serde_json::json;

#[rustfmt::skip]
macro_rules! all_the_tuples {
($name:ident) => {
$name!( 1, u8);
$name!( 2, u8, u16);
$name!( 3, u8, u16, u32);
$name!( 4, u8, u16, u32, u64);
$name!( 5, u8, u16, u32, u64, f32);
$name!( 6, u8, u16, u32, u64, f32, f64);
$name!( 7, u8, u16, u32, u64, f32, f64, i8);
$name!( 8, u8, u16, u32, u64, f32, f64, i8, i16);
$name!( 9, u8, u16, u32, u64, f32, f64, i8, i16, i32);

// Tuples larger than 9 elements are not supported by proptest, but because the
// code generated for each variant is pretty much the same we can assume that those are
// also ok. In the future we might want to test them with a more sophisticated macro.
// $name!(10, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64);
// $name!(11, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64, u8);
// $name!(12, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64, u8, u16);
// $name!(13, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64, u8, u16, u32);
// $name!(14, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64, u8, u16, u32, u64);
// $name!(15, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64, u8, u16, u32, u64, f32);
// $name!(16, u8, u16, u32, u64, f32, f64, i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
};
}

macro_rules! impl_test_case {
($length:literal, $($types:ty),*) => {
paste::paste! {
#[cfg(not(miri))]
proptest! {
#[test]
fn [< tuple $length _ok >](value in any::<($($types,)*)>()) {
seq!(N in 0..$length {
let stream = [
Token::Array {length: Some($length)},
#(Token::Number(value.N.into()),)*
Token::ArrayEnd,
];
});

assert_tokens(&value, &stream);
}
}
}
};
}

all_the_tuples!(impl_test_case);

#[test]
fn tuple_insufficient_length_err() {
assert_tokens_error::<(u8, u16)>(
&error!([{
ns: "deer",
id: ["value", "missing"],
properties: {
"expected": u16::reflection(),
"location": [{"type": "tuple", "value": 1}]
}
}]),
&[
Token::Array { length: Some(1) },
Token::Number(12.into()),
Token::ArrayEnd,
],
);
}

#[test]
fn tuple_too_many_items_err() {
assert_tokens_error::<(u8, u16)>(
&error!([{
ns: "deer",
id: ["array", "length"],
properties: {
"expected": 2,
"received": 3,
"location": []
}
}]),
&[
Token::Array { length: Some(3) },
Token::Number(12.into()),
Token::Number(13.into()),
Token::Number(14.into()),
Token::ArrayEnd,
],
);
}

#[test]
fn tuple_fallback_to_default_ok() {
assert_tokens(&(Some(12u8), None::<u16>), &[
Token::Array { length: Some(1) },
Token::Number(12.into()),
Token::ArrayEnd,
]);
}

0 comments on commit 46c5058

Please sign in to comment.