diff --git a/libs/deer/src/error.rs b/libs/deer/src/error.rs index cfc0c5427a4..09276978c72 100644 --- a/libs/deer/src/error.rs +++ b/libs/deer/src/error.rs @@ -63,6 +63,7 @@ use core::{ fmt::{self, Debug, Display, Formatter}, }; +pub use duplicate::{DuplicateField, DuplicateFieldError, DuplicateKey, DuplicateKeyError}; use error_stack::{Context, Frame, IntoReport, Report, Result}; pub use extra::{ ArrayLengthError, ExpectedLength, ObjectItemsExtraError, ObjectLengthError, ReceivedKey, @@ -80,6 +81,7 @@ pub use value::{MissingError, ReceivedValue, ValueError}; use crate::error::serialize::{impl_serialize, Export}; +mod duplicate; mod extra; mod internal; mod location; @@ -469,3 +471,18 @@ impl ReportExt for Report { Export::new(self) } } + +pub(crate) trait ResultExtPrivate { + fn extend_one(&mut self, error: Report); +} + +impl ResultExtPrivate for Result { + fn extend_one(&mut self, error: Report) { + match self { + Err(errors) => { + errors.extend_one(error); + } + errors => *errors = Err(error), + } + } +} diff --git a/libs/deer/src/error/duplicate.rs b/libs/deer/src/error/duplicate.rs new file mode 100644 index 00000000000..6db0d434bf4 --- /dev/null +++ b/libs/deer/src/error/duplicate.rs @@ -0,0 +1,113 @@ +use alloc::string::String; +use core::{ + fmt, + fmt::{Display, Formatter}, +}; + +use crate::{ + error::{ErrorProperties, ErrorProperty, Id, Location, Namespace, Variant, NAMESPACE}, + id, +}; + +pub struct DuplicateField(&'static str); + +impl DuplicateField { + #[must_use] + pub const fn new(name: &'static str) -> Self { + Self(name) + } +} + +impl ErrorProperty for DuplicateField { + type Value<'a> = Option<&'static str> where Self: 'a; + + fn key() -> &'static str { + "field" + } + + fn value<'a>(mut stack: impl Iterator) -> Self::Value<'a> { + stack.next().map(|field| field.0) + } +} + +#[derive(Debug)] +pub struct DuplicateFieldError; + +impl Variant for DuplicateFieldError { + type Properties = (Location, DuplicateField); + + const ID: Id = id!["duplicate", "field"]; + const NAMESPACE: Namespace = NAMESPACE; + + fn message( + &self, + fmt: &mut Formatter, + properties: &::Value<'_>, + ) -> fmt::Result { + let (_, field) = properties; + + if let Some(field) = field { + write!(fmt, "duplicate field `{field}`") + } else { + Display::fmt(self, fmt) + } + } +} + +impl Display for DuplicateFieldError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("duplicate field") + } +} + +pub struct DuplicateKey(String); + +impl DuplicateKey { + pub fn new(key: impl Into) -> Self { + Self(key.into()) + } +} + +impl ErrorProperty for DuplicateKey { + type Value<'a> = Option<&'a str> where Self: 'a; + + fn key() -> &'static str { + "key" + } + + fn value<'a>(mut stack: impl Iterator) -> Self::Value<'a> { + stack.next().map(|key| key.0.as_str()) + } +} + +#[derive(Debug)] +pub struct DuplicateKeyError; + +impl Variant for DuplicateKeyError { + type Properties = (Location, DuplicateKey); + + const ID: Id = id!["duplicate", "key"]; + const NAMESPACE: Namespace = NAMESPACE; + + fn message( + &self, + fmt: &mut Formatter, + properties: &::Value<'_>, + ) -> fmt::Result { + let (_, key) = properties; + + if let Some(key) = key { + write!(fmt, "duplicate key `{key}`") + } else { + Display::fmt(self, fmt) + } + } +} + +impl Display for DuplicateKeyError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("duplicate key") + } +} + +// TODO: unit test diff --git a/libs/deer/src/helpers.rs b/libs/deer/src/helpers.rs index c0864368587..4bba14a4254 100644 --- a/libs/deer/src/helpers.rs +++ b/libs/deer/src/helpers.rs @@ -1,8 +1,10 @@ use error_stack::{Result, ResultExt}; +use serde::{ser::SerializeMap, Serialize, Serializer}; use crate::{ error::{DeserializeError, VisitorError}, ext::TupleExt, + schema::Reference, Deserialize, Deserializer, Document, EnumVisitor, FieldVisitor, ObjectAccess, Reflection, Schema, Visitor, }; @@ -123,3 +125,20 @@ impl<'de> Deserialize<'de> for ExpectNone { } // TODO: consider adding an error attachment marker type for "short-circuit" + +pub struct Properties(pub [(&'static str, Reference); N]); + +impl Serialize for Properties { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.0.len()))?; + + for (key, value) in self.0 { + map.serialize_entry(key, &value)?; + } + + map.end() + } +} diff --git a/libs/deer/src/impls.rs b/libs/deer/src/impls.rs index 98160373d02..f6d7ca81e66 100644 --- a/libs/deer/src/impls.rs +++ b/libs/deer/src/impls.rs @@ -1 +1,26 @@ +use error_stack::Result; + +use crate::{error::VisitorError, Deserialize, Document, OptionalVisitor}; + mod core; + +pub(crate) struct UnitVariantVisitor; + +impl<'de> OptionalVisitor<'de> for UnitVariantVisitor { + type Value = (); + + fn expecting(&self) -> Document { + // TODO: in theory also none, cannot be expressed with current schema + <() as Deserialize>::reflection() + } + + fn visit_none(self) -> Result { + Ok(()) + } + + fn visit_null(self) -> Result { + Ok(()) + } + + // we do not implement `visit_some` because we do not allow for some values +} diff --git a/libs/deer/src/impls/core.rs b/libs/deer/src/impls/core.rs index 47c55289ef5..2ee1bc316f9 100644 --- a/libs/deer/src/impls/core.rs +++ b/libs/deer/src/impls/core.rs @@ -6,6 +6,7 @@ mod cmp; mod marker; mod mem; mod num; +mod ops; mod option; mod result; mod string; diff --git a/libs/deer/src/impls/core/ops.rs b/libs/deer/src/impls/core/ops.rs new file mode 100644 index 00000000000..26269d25348 --- /dev/null +++ b/libs/deer/src/impls/core/ops.rs @@ -0,0 +1,404 @@ +use core::{ + marker::PhantomData, + ops::{Bound, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}, +}; + +use error_stack::{Report, Result, ResultExt}; + +use crate::{ + error::{ + ArrayAccessError, DeserializeError, DuplicateField, DuplicateFieldError, Location, + ObjectAccessError, ResultExtPrivate, Variant, VisitorError, + }, + ext::TupleExt, + helpers::Properties, + identifier, + impls::UnitVariantVisitor, + schema::Reference, + value::NoneDeserializer, + ArrayAccess, Deserialize, Deserializer, Document, EnumVisitor, FieldVisitor, ObjectAccess, + Reflection, Schema, StructVisitor, +}; + +identifier! { + enum BoundDiscriminant { + Unbounded = "Unbounded" | b"Unbounded" | 0, + Included = "Included" | b"Included" | 1, + Excluded = "Excluded" | b"Excluded" | 2, + } +} + +struct BoundEnumVisitor(PhantomData *const T>); + +impl<'de, T> EnumVisitor<'de> for BoundEnumVisitor +where + T: Deserialize<'de>, +{ + type Discriminant = BoundDiscriminant; + type Value = Bound; + + fn expecting(&self) -> Document { + Self::Value::reflection() + } + + fn visit_value( + self, + discriminant: Self::Discriminant, + deserializer: D, + ) -> Result + where + D: Deserializer<'de>, + { + match discriminant { + BoundDiscriminant::Unbounded => deserializer + .deserialize_optional(UnitVariantVisitor) + .map(|_| Bound::Unbounded) + .attach(Location::Variant("Unbounded")) + .change_context(VisitorError), + BoundDiscriminant::Included => T::deserialize(deserializer) + .map(Bound::Included) + .attach(Location::Variant("Included")) + .change_context(VisitorError), + BoundDiscriminant::Excluded => T::deserialize(deserializer) + .map(Bound::Excluded) + .attach(Location::Variant("Excluded")) + .change_context(VisitorError), + } + } +} + +pub struct BoundReflection(fn() -> *const T); + +impl Reflection for BoundReflection +where + T: Reflection + ?Sized, +{ + fn schema(doc: &mut Document) -> Schema { + #[derive(serde::Serialize)] + enum BoundOneOf { + Included(Reference), + Excluded(Reference), + Unbounded(Reference), + } + + // TODO: the case where "Unbounded" as a single value is possible cannot be + // represented right now with deer Schema capabilities + Schema::new("object").with("oneOf", [ + BoundOneOf::Included(doc.add::()), + BoundOneOf::Excluded(doc.add::()), + BoundOneOf::Unbounded(doc.add::<<() as Deserialize>::Reflection>()), + ]) + } +} + +impl<'de, T> Deserialize<'de> for Bound +where + T: Deserialize<'de>, +{ + type Reflection = BoundReflection; + + fn deserialize>(de: D) -> Result { + de.deserialize_enum(BoundEnumVisitor(PhantomData)) + .change_context(DeserializeError) + } +} + +identifier! { + enum RangeIdent { + Start = "start" | b"start" | 0, + End = "end" | b"end" | 1, + } +} + +struct RangeFieldVisitor<'a, T, U> { + start: &'a mut Option, + end: &'a mut Option, +} + +impl<'a, 'de, T, U> FieldVisitor<'de> for RangeFieldVisitor<'a, T, U> +where + T: Deserialize<'de>, + U: Deserialize<'de>, +{ + type Key = RangeIdent; + type Value = (); + + fn visit_value(self, key: Self::Key, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + match key { + RangeIdent::Start => { + let value = T::deserialize(deserializer) + .attach(Location::Field("start")) + .change_context(VisitorError)?; + + if self.start.is_some() { + return Err(Report::new(DuplicateFieldError.into_error()) + .attach(DuplicateField::new("start")) + .change_context(VisitorError)); + } + + *self.start = Some(value); + + Ok(()) + } + RangeIdent::End => { + let value = U::deserialize(deserializer) + .attach(Location::Field("end")) + .change_context(VisitorError)?; + + if self.end.is_some() { + return Err(Report::new(DuplicateFieldError.into_error()) + .attach(DuplicateField::new("end")) + .change_context(VisitorError)); + } + + *self.end = Some(value); + + Ok(()) + } + } + } +} + +struct RangeVisitor( + PhantomData *const (T, U)>, + PhantomData *const R>, +); + +impl<'de, T, U, R> StructVisitor<'de> for RangeVisitor +where + T: Deserialize<'de>, + U: Deserialize<'de>, + R: Reflection + ?Sized, +{ + type Value = (T, U); + + fn expecting(&self) -> Document { + R::document() + } + + fn visit_array(self, array: A) -> Result + where + A: ArrayAccess<'de>, + { + let mut array = array.into_bound(2).change_context(VisitorError)?; + + let start = array + .next() + .unwrap_or_else(|| { + Deserialize::deserialize(NoneDeserializer::new(array.context())) + .attach(Location::Tuple(0)) + .change_context(ArrayAccessError) + }) + .attach(Location::Tuple(0)); + + let end = array + .next() + .unwrap_or_else(|| { + Deserialize::deserialize(NoneDeserializer::new(array.context())) + .attach(Location::Tuple(1)) + .change_context(ArrayAccessError) + }) + .attach(Location::Tuple(1)); + + let (start, end, _) = (start, end, array.end()) + .fold_reports() + .change_context(VisitorError)?; + + Ok((start, end)) + } + + fn visit_object(self, mut object: A) -> Result + where + A: ObjectAccess<'de>, + { + let mut start: Option = None; + let mut end: Option = None; + + let mut errors: Result<(), ObjectAccessError> = Ok(()); + + while let Some(field) = object.field(RangeFieldVisitor { + start: &mut start, + end: &mut end, + }) { + if let Err(error) = field { + errors.extend_one(error); + } + } + + let start = start.map_or_else( + || { + Deserialize::deserialize(NoneDeserializer::new(object.context())) + .attach(Location::Field("start")) + .change_context(VisitorError) + }, + Ok, + ); + + let end = end.map_or_else( + || { + Deserialize::deserialize(NoneDeserializer::new(object.context())) + .attach(Location::Field("end")) + .change_context(VisitorError) + }, + Ok, + ); + + let (start, end, ..) = ( + start, + end, + errors.change_context(VisitorError), + object.end().change_context(VisitorError), + ) + .fold_reports()?; + + Ok((start, end)) + } +} + +pub struct RangeReflection(fn() -> *const T, fn() -> *const U); + +impl Reflection for RangeReflection +where + T: Reflection + ?Sized, + U: Reflection + ?Sized, +{ + fn schema(doc: &mut Document) -> Schema { + Schema::new("object").with( + "properties", + Properties([("start", doc.add::()), ("end", doc.add::())]), + ) + } +} + +impl<'de, T> Deserialize<'de> for Range +where + T: Deserialize<'de>, +{ + type Reflection = RangeReflection; + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer + .deserialize_struct(RangeVisitor::( + PhantomData, + PhantomData, + )) + .map(|(start, end)| start..end) + .change_context(DeserializeError) + } +} + +impl<'de, T> Deserialize<'de> for RangeInclusive +where + T: Deserialize<'de>, +{ + type Reflection = RangeReflection; + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer + .deserialize_struct(RangeVisitor::( + PhantomData, + PhantomData, + )) + .map(|(start, end)| start..=end) + .change_context(DeserializeError) + } +} + +// We follow the same deserialization rules as serde, but we also implement `Range` for all types +// This means we need to adapt the existing `Range` deserialization rules to our own +// RangeFrom: {"start": T, "end": null} => RangeFrom { start: T } +// RangeTo: {"start": null, "end": T} => RangeTo { end: T } +// RangeToInclusive: {"start": null, "end": T} => RangeToInclusive { end: T } +// RangeFull: {"start": null, "end": null} => RangeFull +// on an object the keys are optional and on arrays the end can be omitted if it is always null + +impl<'de, T> Deserialize<'de> for RangeFrom +where + T: Deserialize<'de>, +{ + type Reflection = RangeReflection as Deserialize<'de>>::Reflection>; + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // `Option<()>` allows us to deserialize `null` and(!) `none`, `ExpectNone` only allows + // `none`, `()` only allows `null` + deserializer + .deserialize_struct(RangeVisitor::, Self::Reflection>( + PhantomData, + PhantomData, + )) + .map(|(start, _)| start..) + .change_context(DeserializeError) + } +} + +impl<'de, T> Deserialize<'de> for RangeTo +where + T: Deserialize<'de>, +{ + type Reflection = RangeReflection< as Deserialize<'de>>::Reflection, T::Reflection>; + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer + .deserialize_struct(RangeVisitor::, T, Self::Reflection>( + PhantomData, + PhantomData, + )) + .map(|(_, end)| ..end) + .change_context(DeserializeError) + } +} + +impl<'de, T> Deserialize<'de> for RangeToInclusive +where + T: Deserialize<'de>, +{ + type Reflection = RangeReflection< as Deserialize<'de>>::Reflection, T::Reflection>; + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer + .deserialize_struct(RangeVisitor::, T, Self::Reflection>( + PhantomData, + PhantomData, + )) + .map(|(_, end)| ..=end) + .change_context(DeserializeError) + } +} + +impl<'de> Deserialize<'de> for RangeFull { + type Reflection = RangeReflection< + as Deserialize<'de>>::Reflection, + as Deserialize<'de>>::Reflection, + >; + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer + .deserialize_struct(RangeVisitor::, Option<()>, Self::Reflection>( + PhantomData, + PhantomData, + )) + .map(|(..)| ..) + .change_context(DeserializeError) + } +} diff --git a/libs/deer/src/impls/core/option.rs b/libs/deer/src/impls/core/option.rs index df526602321..1e0c17ae631 100644 --- a/libs/deer/src/impls/core/option.rs +++ b/libs/deer/src/impls/core/option.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use error_stack::{Result, ResultExt}; use crate::{ - error::{DeserializeError, VisitorError}, + error::{DeserializeError, Location, VisitorError}, Deserialize, Deserializer, Document, OptionalVisitor, Reflection, Schema, }; @@ -29,8 +29,9 @@ impl<'de, T: Deserialize<'de>> OptionalVisitor<'de> for OptionVisitor { D: Deserializer<'de>, { T::deserialize(deserializer) - .change_context(VisitorError) .map(Some) + .attach(Location::Variant("Some")) + .change_context(VisitorError) } } diff --git a/libs/deer/src/lib.rs b/libs/deer/src/lib.rs index ceeb3da7aaf..dc1b10c3698 100644 --- a/libs/deer/src/lib.rs +++ b/libs/deer/src/lib.rs @@ -49,6 +49,14 @@ pub mod value; extern crate alloc; pub mod export { + // We need to re-export `alloc`, as our macros depend on it, in the case that we're operating in + // an `std` environment most crates do not have a `extern crate alloc` statement. This means + // that `alloc::borrow::ToOwned` is not available. (we would need to use `std::borrow::ToOwned`) + // This means we would need to switch between `std` and `alloc` depending on the environment and + // feature flag, which is prone to errors. (some crates default to no-std dependencies, in that + // case this would fail). By re-exporting `alloc` we can always use `alloc::borrow::ToOwned`. + pub extern crate alloc; + pub use error_stack; } diff --git a/libs/deer/src/macros.rs b/libs/deer/src/macros.rs index a9214c79a01..3c174d59fcb 100644 --- a/libs/deer/src/macros.rs +++ b/libs/deer/src/macros.rs @@ -99,6 +99,47 @@ macro_rules! forward_to_deserialize_any_helper { }; } +/// Helper macro for implementing an identifier deserializer. +/// +/// The syntax is: +/// +/// ```rust +/// use deer::identifier; +/// +/// identifier! { +/// pub enum Identifier { +/// // identifiers can be used in different places, depending on the deserializer +/// // they support str, bytes, and u64 +/// VariantName = "StrVariant" | b"ByteVariant" | 2, +/// // You can exclude a specific variant from one of the `visit_` method implementations +/// // by replacing the value with `_` +/// VariantName2 = _ | b"ByteVariant" | 2, +/// // if there's a duplicate value for a variant, the first one declared will be used. +/// // duplicate variants (not their value(!)) will lead to a compile error +/// } +/// } +/// ``` +/// +/// # Implementation +/// +/// Internally this macro will generate a `match` statement for the `visit_str`, `visit_bytes`, and +/// `visit_u64` methods. Because all generated code must be valid rust, the macro will not generate +/// the match arms first, but instead utilize a stack to generate the match arms. +/// +/// There are two stacks used, one for the match arms, and one for the variant values. The variant +/// values stack is used to provide error messages (as to which variant was expected). +/// +/// Roughly the match internal macro can be described as: +/// +/// ```text +/// @internal match +/// $ty, // <- the type of visit function in which it is used, either `str`, `bytes`, or `u64` +/// $e; // <- the expression we want to match against +/// $name // <- the name of enum we're generating the match for +/// @() // <- the stack of variants to be processed, every entry is a tuple of the form `(variant, value | _)` +/// @() // <- the stack of values, used to generate error messages, will be added to in every iteration +/// $($arms:tt)* // <- generated match arms +/// ``` #[macro_export] macro_rules! identifier { (@internal @@ -112,7 +153,7 @@ macro_rules! identifier { @($($rest),*) @($($stack),*) $($arms)* - ); + ) }; (@internal match $ty:tt, $e:expr; $name:ident @@ -125,7 +166,7 @@ macro_rules! identifier { @($($rest),*) @($($stack, )* $value) $($arms)* $value => Ok($name::$variant), - ); + ) }; (@internal @@ -145,7 +186,7 @@ macro_rules! identifier { $( .attach($crate::error::ExpectedIdentifier::String($stack)) )* - .attach($crate::error::ReceivedIdentifier::String(value.to_owned())) + .attach($crate::error::ReceivedIdentifier::String($crate::export::alloc::borrow::ToOwned::to_owned(value))) .change_context($crate::error::VisitorError) ) } @@ -168,7 +209,7 @@ macro_rules! identifier { $( .attach($crate::error::ExpectedIdentifier::Bytes($stack)) )* - .attach($crate::error::ReceivedIdentifier::Bytes(value.to_owned())) + .attach($crate::error::ReceivedIdentifier::Bytes($crate::export::alloc::borrow::ToOwned::to_owned(value))) .change_context($crate::error::VisitorError) ) } @@ -224,7 +265,7 @@ macro_rules! identifier { @($($stack:literal),*) ) => { impl $crate::Reflection for $name { - fn schema(doc: &mut $crate::Document) -> $crate::Schema { + fn schema(_: &mut $crate::Document) -> $crate::Schema { // we lack the ability to properly express OR, so for now we just default to // output the string representation $crate::Schema::new("string").with("enum", [$($stack),*]) diff --git a/libs/deer/tests/test_impls_core_ops.rs b/libs/deer/tests/test_impls_core_ops.rs new file mode 100644 index 00000000000..c32721a9fe1 --- /dev/null +++ b/libs/deer/tests/test_impls_core_ops.rs @@ -0,0 +1,276 @@ +use core::ops::{Bound, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}; + +use deer::Deserialize; +use deer_desert::{assert_tokens, assert_tokens_error, error, Token}; +use proptest::prelude::*; +use serde_json::json; + +#[cfg(not(miri))] +proptest! { + #[test] + fn bound_included_ok(value in any::()) { + assert_tokens(&Bound::Included(value), &[ + Token::Object { length: Some(2) }, + Token::Str("Included"), + Token::Number(value.into()), + Token::ObjectEnd + ]); + } + + #[test] + fn bound_excluded_ok(value in any::()) { + assert_tokens(&Bound::Excluded(value), &[ + Token::Object { length: Some(2) }, + Token::Str("Excluded"), + Token::Number(value.into()), + Token::ObjectEnd + ]); + } + + #[test] + fn range_array_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Array { length: Some(2) }, + Token::Number(value.start.into()), + Token::Number(value.end.into()), + Token::ArrayEnd, + ]); + } + + #[test] + fn range_object_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(2) }, + Token::Str("start"), + Token::Number(value.start.into()), + Token::Str("end"), + Token::Number(value.end.into()), + Token::ObjectEnd, + ]); + } + + #[test] + fn range_inclusive_array_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Array { length: Some(2) }, + Token::Number((*value.start()).into()), + Token::Number((*value.end()).into()), + Token::ArrayEnd, + ]); + } + + #[test] + fn range_inclusive_object_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(2) }, + Token::Str("start"), + Token::Number((*value.start()).into()), + Token::Str("end"), + Token::Number((*value.end()).into()), + Token::ObjectEnd, + ]); + } + + #[test] + fn range_from_array_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Array { length: Some(1) }, + Token::Number(value.start.into()), + Token::Null, + Token::ArrayEnd, + ]); + } + + #[test] + fn range_from_array_missing_end_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Array { length: Some(1) }, + Token::Number(value.start.into()), + Token::ArrayEnd, + ]); + } + + #[test] + fn range_from_object_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Number(value.start.into()), + Token::Str("end"), + Token::Null, + Token::ObjectEnd, + ]); + } + + #[test] + fn range_from_object_missing_end_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Number(value.start.into()), + Token::ObjectEnd, + ]); + } + + #[test] + fn range_full_array_ok(value in any::()) { + assert_tokens(&value, &[ + Token::Array { length: Some(1) }, + Token::Null, + Token::Null, + Token::ArrayEnd, + ]); + } + + #[test] + fn range_full_missing_end_array_ok(value in any::()) { + assert_tokens(&value, &[ + Token::Array { length: Some(1) }, + Token::Null, + Token::ArrayEnd, + ]); + } + + #[test] + fn range_full_empty_array_ok(value in any::()) { + assert_tokens(&value, &[ + Token::Array { length: Some(0) }, + Token::ArrayEnd, + ]); + } + + #[test] + fn range_full_object_ok(value in any::()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Null, + Token::Str("end"), + Token::Null, + Token::ObjectEnd, + ]); + } + + #[test] + fn range_full_missing_end_object_ok(value in any::()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Null, + Token::ObjectEnd, + ]); + } + + #[test] + fn range_full_empty_object_ok(value in any::()) { + assert_tokens(&value, &[ + Token::Object { length: Some(0) }, + Token::ObjectEnd, + ]); + } + + #[test] + fn range_to_array_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Array { length: Some(1) }, + Token::Null, + Token::Number(value.end.into()), + Token::ArrayEnd, + ]); + } + + #[test] + fn range_to_object_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Null, + Token::Str("end"), + Token::Number(value.end.into()), + Token::ObjectEnd, + ]); + } + + #[test] + fn range_to_object_missing_start_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("end"), + Token::Number(value.end.into()), + Token::ObjectEnd, + ]); + } + + #[test] + fn range_to_inclusive_array_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Array { length: Some(1) }, + Token::Null, + Token::Number(value.end.into()), + Token::ArrayEnd, + ]); + } + + #[test] + fn range_to_inclusive_object_ok(value in any::>()) { + assert_tokens(&value, &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Null, + Token::Str("end"), + Token::Number(value.end.into()), + Token::ObjectEnd, + ]); + } +} + +#[test] +fn range_to_object_missing_end_err() { + assert_tokens_error::>( + &error!([{ + ns: "deer", + id: ["value", "missing"], + properties: { + "location": [{"type": "field", "value": "end"}], + "expected": u64::reflection() + } + }]), + &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Null, + Token::ObjectEnd, + ], + ); +} + +#[test] +fn range_to_inclusive_object_missing_end_err() { + assert_tokens_error::>( + &error!([{ + ns: "deer", + id: ["value", "missing"], + properties: { + "location": [{"type": "field", "value": "end"}], + "expected": u64::reflection() + } + }]), + &[ + Token::Object { length: Some(1) }, + Token::Str("start"), + Token::Null, + Token::ObjectEnd, + ], + ); +} + +#[test] +fn bound_unbounded() { + assert_tokens(&Bound::<()>::Unbounded, &[Token::Str("Unbounded")]); + assert_tokens(&Bound::<()>::Unbounded, &[ + Token::Object { length: Some(1) }, + Token::Str("Unbounded"), + Token::Null, + Token::ObjectEnd, + ]); +} diff --git a/libs/deer/tests/test_impls_core_option.rs b/libs/deer/tests/test_impls_core_option.rs index a3f9f528b56..df088bbaff0 100644 --- a/libs/deer/tests/test_impls_core_option.rs +++ b/libs/deer/tests/test_impls_core_option.rs @@ -1,5 +1,7 @@ -use deer_desert::{assert_tokens, Token}; +use deer::Deserialize; +use deer_desert::{assert_tokens, assert_tokens_error, error, Token}; use proptest::prelude::*; +use serde_json::json; #[cfg(not(miri))] proptest! { @@ -13,3 +15,19 @@ proptest! { fn option_none_ok() { assert_tokens(&None::, &[Token::Null]); } + +#[test] +fn option_error_location() { + assert_tokens_error::>( + &error!([{ + ns: "deer", + id: ["type"], + properties: { + "expected": u8::reflection(), + "received": bool::reflection(), + "location": [{"type": "variant", "value": "Some"}] + } + }]), + &[Token::Bool(true)], + ); +} diff --git a/libs/deer/tests/test_struct_visitor.rs b/libs/deer/tests/test_struct_visitor.rs index 66cf966acc2..1244e025bf6 100644 --- a/libs/deer/tests/test_struct_visitor.rs +++ b/libs/deer/tests/test_struct_visitor.rs @@ -4,7 +4,6 @@ use deer::{ Schema, StructVisitor, Visitor, }; use error_stack::{Report, Result, ResultExt}; -use serde::{ser::SerializeMap, Serialize, Serializer}; use serde_json::json; mod common; @@ -12,7 +11,7 @@ mod common; use common::TupleExt; use deer::{ error::{ExpectedField, Location, ObjectAccessError, ReceivedField, UnknownFieldError}, - schema::Reference, + helpers::Properties, value::NoneDeserializer, }; use deer_desert::{assert_tokens, assert_tokens_error, error, Token}; @@ -279,23 +278,6 @@ impl<'de> StructVisitor<'de> for ExampleVisitor { } } -struct Properties([(&'static str, Reference); N]); - -impl Serialize for Properties { - fn serialize(&self, serializer: S) -> core::result::Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(self.0.len()))?; - - for (key, value) in self.0 { - map.serialize_entry(key, &value)?; - } - - map.end() - } -} - impl Reflection for Example { fn schema(doc: &mut Document) -> Schema { // TODO: we cannot express or constraints right now