diff --git a/libs/deer/Cargo.toml b/libs/deer/Cargo.toml index 9d9982dde02..16615c385b5 100644 --- a/libs/deer/Cargo.toml +++ b/libs/deer/Cargo.toml @@ -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" diff --git a/libs/deer/src/ext.rs b/libs/deer/src/ext.rs new file mode 100644 index 00000000000..e3d45e8e017 --- /dev/null +++ b/libs/deer/src/ext.rs @@ -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>; +} + +#[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 TupleExt for (Result>,) { + type Context = C; + type Ok = (T1,); + + fn fold_reports(self) -> Result> { + self.0.map(|value| (value,)) + } +} + +macro_rules! impl_tuple_ext { + ([$($elem:ident),*], $other:ident) => { + #[allow(non_snake_case)] + impl TupleExt for ($(Result<$elem, Report>, )* Result<$other, Report>) { + type Context = C; + type Ok = ($($elem ,)* $other); + + fn fold_reports(self) -> Result> { + 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); diff --git a/libs/deer/src/impls/core.rs b/libs/deer/src/impls/core.rs index 45a0c125dc8..e6bc6c493d3 100644 --- a/libs/deer/src/impls/core.rs +++ b/libs/deer/src/impls/core.rs @@ -13,4 +13,5 @@ mod result; mod string; mod sync; mod time; +mod tuples; mod unit; diff --git a/libs/deer/src/impls/core/tuples.rs b/libs/deer/src/impls/core/tuples.rs new file mode 100644 index 00000000000..ab9b818abb4 --- /dev/null +++ b/libs/deer/src/impls/core/tuples.rs @@ -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 *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 *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(self, mut v: T) -> Result + 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>(de: D) -> Result { + de.deserialize_array($visitor::<$($elem,)*>(PhantomData)) + .change_context(DeserializeError) + } + } + }; +} + +all_the_tuples!(impl_tuple); diff --git a/libs/deer/src/lib.rs b/libs/deer/src/lib.rs index b0cf0e3acda..157bc315dae 100644 --- a/libs/deer/src/lib.rs +++ b/libs/deer/src/lib.rs @@ -38,6 +38,7 @@ pub mod error; mod impls; #[macro_use] mod macros; +mod ext; mod number; pub mod schema; pub mod value; diff --git a/libs/deer/tests/test_impls_core_tuples.rs b/libs/deer/tests/test_impls_core_tuples.rs new file mode 100644 index 00000000000..093b9a27bb4 --- /dev/null +++ b/libs/deer/tests/test_impls_core_tuples.rs @@ -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::), &[ + Token::Array { length: Some(1) }, + Token::Number(12.into()), + Token::ArrayEnd, + ]); +}