diff --git a/README.md b/README.md index 9f86cc3..3d728b5 100644 --- a/README.md +++ b/README.md @@ -238,13 +238,16 @@ struct MyVec(Vec); impl garde::Validate for MyVec { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), garde::Errors> { - garde::Errors::list(|errors| { - for item in self.0.iter() { - errors.push(item.validate(ctx)); - } - }) - .finish() + fn validate_into( + &self, + ctx: &Self::Context, + mut parent: &mut dyn FnMut() -> garde::Path, + report: &mut garde::Report + ) { + for (index, item) in self.0.iter().enumerate() { + let mut path = garde::util::nested_path!(parent, index); + item.validate_into(ctx, &mut path, report); + } } } @@ -261,13 +264,6 @@ struct Bar { } ``` -To make implementing the trait easier, the `Errors` type supports a nesting builders. -- For list-like or tuple-like data structures, use `Errors::list`, and its `.push` method to attach nested `Errors`. -- For map-like data structures, use `Errors::fields`, and its `.insert` method to attach nested `Errors`. -- For a "flat" error list, use `Errors::simple`, and its `.push` method to attach individual errors. - -The `ListErrorBuilder::push` and `ListErrorBuilder::insert` methods will ignore any errors which are empty (via `Errors::is_empty`). - ### Integration with web frameworks - [`axum`](https://crates.io/crates/axum): https://crates.io/crates/axum_garde diff --git a/garde/Cargo.toml b/garde/Cargo.toml index 0242f57..1e85886 100644 --- a/garde/Cargo.toml +++ b/garde/Cargo.toml @@ -21,7 +21,7 @@ default = [ "email-idna", "regex", ] -serde = ["dep:serde"] +serde = ["dep:serde", "compact_str/serde"] derive = ["dep:garde_derive"] url = ["dep:url"] credit-card = ["dep:card-validate"] @@ -29,16 +29,21 @@ phone-number = ["dep:phonenumber"] email = ["regex"] email-idna = ["dep:idna"] regex = ["dep:regex", "dep:once_cell", "garde_derive?/regex"] -pattern = ["regex"] # for backward compatibility with <0.14.0 +pattern = ["regex"] # for backward compatibility with <0.14.0 [dependencies] garde_derive = { version = "0.14.0", path = "../garde_derive", optional = true, default-features = false } +smallvec = { version = "1.11.0", default-features = false } +compact_str = { version = "0.7.1", default-features = false } + serde = { version = "1", features = ["derive"], optional = true } url = { version = "2", optional = true } card-validate = { version = "2.3", optional = true } phonenumber = { version = "0.3.2+8.13.9", optional = true } -regex = { version = "1", default-features = false, features = ["std"], optional = true } +regex = { version = "1", default-features = false, features = [ + "std", +], optional = true } once_cell = { version = "1", optional = true } idna = { version = "0.3", optional = true } @@ -47,3 +52,13 @@ trybuild = { version = "1.0" } insta = { version = "1.29" } owo-colors = { version = "3.5.0" } glob = "0.3.1" + +criterion = "0.4" + +[[bench]] +name = "validation" +harness = false + +[profile.profiling] +inherits = "release" +debug = true diff --git a/garde/benches/validation.rs b/garde/benches/validation.rs new file mode 100644 index 0000000..c00ef24 --- /dev/null +++ b/garde/benches/validation.rs @@ -0,0 +1,178 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use garde::Validate; + +#[derive(Debug, garde::Validate)] +struct Test<'a> { + #[garde(alphanumeric)] + alphanumeric: Option<&'a str>, + #[garde(ascii)] + ascii: Option<&'a str>, + #[garde(byte_length(min = 1))] + byte_length_min1_str: Option<&'a str>, + #[garde(byte_length(min = 1))] + byte_length_min1_u8_slice: Option<&'a [u8]>, + #[garde(contains("a"))] + contains_a: Option<&'a str>, + #[garde(credit_card)] + credit_card: Option<&'a str>, + #[garde(email)] + email: Option<&'a str>, + #[garde(ip)] + ip: Option<&'a str>, + #[garde(length(min = 1))] + length_min1: Option<&'a str>, + #[garde(pattern(r"a|b"))] + pat_a_or_b: Option<&'a str>, + #[garde(phone_number)] + phone_number: Option<&'a str>, + #[garde(prefix("a"))] + prefix_a: Option<&'a str>, + #[garde(range(min = 1))] + range_min1: Option, + #[garde(required)] + required: Option<&'a str>, + #[garde(suffix("a"))] + suffix_a: Option<&'a str>, + #[garde(url)] + url: Option<&'a str>, + #[garde(dive)] + nested: Option>>, +} + +macro_rules! valid_input { + () => { + Test { + alphanumeric: Some("a"), + ascii: Some("a"), + byte_length_min1_str: Some("a"), + byte_length_min1_u8_slice: Some(&[0]), + contains_a: Some("a"), + credit_card: Some("4539571147647251"), + email: Some("test@mail.com"), + ip: Some("127.0.0.1"), + length_min1: Some("a"), + pat_a_or_b: Some("a"), + phone_number: Some("+14152370800"), + prefix_a: Some("a"), + range_min1: Some(1), + required: Some("a"), + suffix_a: Some("a"), + url: Some("http://test.com"), + nested: None, + } + }; + ($nested:expr) => { + Test { + alphanumeric: Some("a"), + ascii: Some("a"), + byte_length_min1_str: Some("a"), + byte_length_min1_u8_slice: Some(&[0]), + contains_a: Some("a"), + credit_card: Some("4539571147647251"), + email: Some("test@mail.com"), + ip: Some("127.0.0.1"), + length_min1: Some("a"), + pat_a_or_b: Some("a"), + phone_number: Some("+14152370800"), + prefix_a: Some("a"), + range_min1: Some(1), + required: Some("a"), + suffix_a: Some("a"), + url: Some("http://test.com"), + nested: Some(Box::new($nested)), + } + }; +} + +macro_rules! invalid_input { + () => { + Test { + alphanumeric: Some("😂"), + ascii: Some("😂"), + byte_length_min1_str: Some(""), + byte_length_min1_u8_slice: Some(&[]), + contains_a: Some("😂"), + credit_card: Some("😂"), + email: Some("😂"), + ip: Some("😂"), + length_min1: Some(""), + pat_a_or_b: Some("😂"), + phone_number: Some("😂"), + prefix_a: Some(""), + range_min1: Some(0), + required: None, + suffix_a: Some("😂"), + url: Some("😂"), + nested: None, + } + }; + ($nested:expr) => { + Test { + alphanumeric: Some("😂"), + ascii: Some("😂"), + byte_length_min1_str: Some(""), + byte_length_min1_u8_slice: Some(&[]), + contains_a: Some("😂"), + credit_card: Some("😂"), + email: Some("😂"), + ip: Some("😂"), + length_min1: Some(""), + pat_a_or_b: Some("😂"), + phone_number: Some("😂"), + prefix_a: Some(""), + range_min1: Some(0), + required: None, + suffix_a: Some("😂"), + url: Some("😂"), + nested: Some(Box::new($nested)), + } + }; +} + +fn validate(c: &mut Criterion) { + let inputs = vec![ + ( + "valid", + valid_input!(valid_input!(valid_input!(valid_input!()))), + ), + ( + "invalid", + invalid_input!(invalid_input!(invalid_input!(invalid_input!()))), + ), + ]; + + for (name, input) in inputs { + c.bench_function(&format!("validate `{name}`"), |b| { + b.iter(|| { + let _ = black_box(input.validate(&())); + }) + }); + } +} + +fn display(c: &mut Criterion) { + let inputs = vec![ + ( + "valid", + valid_input!(valid_input!(valid_input!(valid_input!()))).validate(&()), + ), + ( + "invalid", + invalid_input!(invalid_input!(invalid_input!(invalid_input!()))).validate(&()), + ), + ]; + + for (name, input) in inputs { + c.bench_function(&format!("display `{name}`"), |b| { + b.iter(|| { + let _ = black_box(match &input { + Ok(()) => String::new(), + Err(e) => e.to_string(), + }); + }) + }); + } +} + +criterion_group!(benches, validate, display); +criterion_main!(benches); diff --git a/garde/src/error.rs b/garde/src/error.rs index 74436fa..4b9c8dc 100644 --- a/garde/src/error.rs +++ b/garde/src/error.rs @@ -1,297 +1,274 @@ //! Error types used by `garde`. //! -//! Even though these are primarily meant for usage in derive macros, care was taken to maintain -//! composability of the various error constructors. -//! -//! The entrypoint of this module is the [`Errors`] type. -//! -//! An important highlight is the [`Errors::flatten`] function, which may be used to print readable errors. +//! The entrypoint of this module is the [`Error`] type. +#![allow(dead_code)] +mod rc_list; use std::borrow::Cow; -use std::collections::BTreeMap; -/// This type encapsulates a single validation error. -#[derive(Clone, Debug)] +use compact_str::{CompactString, ToCompactString}; +use smallvec::SmallVec; + +use self::rc_list::List; + +/// A validation error report. +/// +/// This type is used as a container for errors aggregated during validation. +/// It is a flat list of `(Path, Error)`. +/// A single field or list item may have any number of errors attached to it. +/// +/// It is possible to extract all errors for specific field using the [`select`] macro. +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct Report { + errors: Vec<(Path, Error)>, +} + +impl Report { + /// Create an empty [`Report`]. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { errors: Vec::new() } + } + + /// Append an [`Error`] into this report at the given [`Path`]. + pub fn append(&mut self, path: Path, error: Error) { + self.errors.push((path, error)); + } + + /// Iterate over all `(Path, Error)` pairs. + pub fn iter(&self) -> impl Iterator { + self.errors.iter() + } + + /// Returns `true` if the report contains no validation errors. + pub fn is_empty(&self) -> bool { + self.errors.is_empty() + } +} + +impl std::fmt::Display for Report { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (path, error) in self.iter() { + writeln!(f, "{path}: {error}")?; + } + Ok(()) + } +} + +impl std::error::Error for Report {} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Error { - /// The error message. - /// - /// Care should be taken to ensure this error message makes sense in the following context: - /// ```text,ignore - /// field_name: {message} - /// ``` - pub message: Cow<'static, str>, + message: CompactString, } impl Error { - /// Create a simple error from an error message. - pub fn new(message: impl Into>) -> Self { + pub fn new(message: impl PathComponentKind) -> Self { Self { - message: message.into(), + message: message.to_compact_string(), } } + + pub fn message(&self) -> &str { + self.message.as_ref() + } } impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.message) } } impl std::error::Error for Error {} -/// This type encapsulates a set of (potentially nested) validation errors. -#[derive(Clone, Debug)] -pub enum Errors { - /// Errors attached directly to a field. - /// - /// For example, `#[garde(length(min=1, max=100))]` on a `T: Length` field. - Simple(Vec), - /// Errors which are attached both to a field and its inner value. - /// - /// For example, `#[garde(length(min=1, max=100), dive)]` on a `Vec` field. - Nested { - outer: Box, - inner: Box, - }, - /// A list of errors. - /// - /// For example, `#[garde(dive)]` on a `Vec` field. - List(Vec), - /// A map of field names to errors. - /// - /// For example, `#[garde(dive)]` on a `HashMap` field. - Fields(BTreeMap, Errors>), +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Path { + components: List<(Kind, CompactString)>, } -impl From> for Errors { - fn from(value: Result<(), Errors>) -> Self { - match value { - Ok(()) => Errors::empty(), - Err(errors) => errors, - } - } +#[doc(hidden)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Kind { + Key, + Index, } -impl Errors { - /// Finish building the error. - /// - /// If `!self.is_empty()`, this returns `Err(self)`, otherwise this returns `Ok(())`. - /// - /// This exists to make converting the error into a `Result` easier - /// in manual implementations of `Validate`. - pub fn finish(self) -> Result<(), Errors> { - if !self.is_empty() { - Err(self) - } else { - Ok(()) - } - } +pub trait PathComponentKind: std::fmt::Display + ToCompactString + private::Sealed { + fn component_kind() -> Kind; +} - /// If the error is empty, returns true. - /// - /// - For [`Errors::Simple`] and [`Errors::List`] the inner list must be empty. - /// - For [`Errors::Fields`] the inner map must be empty. - /// - For [`Errors::Nested`] both the list of errors *and* the nested error must be empty. - pub fn is_empty(&self) -> bool { - match self { - Errors::Simple(v) => v.is_empty(), - Errors::List(v) => v.is_empty(), - Errors::Fields(v) => v.is_empty(), - Errors::Nested { outer, inner } => outer.is_empty() && inner.is_empty(), +macro_rules! impl_path_component_kind { + ($(@$($G:lifetime)*;)? $T:ty => $which:ident) => { + impl $(<$($G),*>)? private::Sealed for $T {} + impl $(<$($G),*>)? PathComponentKind for $T { + fn component_kind() -> Kind { + Kind::$which + } } } +} - /// Recursively flattens the error, returning a list of `(path, error)`. - /// - /// `path` is generated by appending a suffix to the path each time the traversal function recurses: - /// - For [`Errors::List`], it appends `[{index}]` to each item. - /// - For [`Errors::Fields`], it appends `.{key}` for each key-value pair. - /// - For [`Errors::Nested`], it does not append anything. - /// - For [`Errors::Simple`], it does not append anything. - /// - /// For example: - /// ```text,ignore - /// let errors = Errors::Fields({ - /// "a": Errors::List([ - /// Errors::Simple([ - /// "length is lower than 15", - /// "not alphanumeric" - /// ]) - /// ]), - /// "b": Errors::Fields({ - /// "c": Errors::Simple([ - /// "not a valid url" - /// ]) - /// }) - /// }); - /// - /// println!("{errors}"); - /// ``` - /// Would print: - /// ```text,ignore - /// value.a[0]: length is lower than 15 - /// value.a[0]: not alphanumeric - /// value.b.c: not a valid url - /// ``` - pub fn flatten(&self) -> Vec<(String, Error)> { - fn flatten_inner(out: &mut Vec<(String, Error)>, current_path: String, errors: &Errors) { - match errors { - Errors::Simple(errors) => { - for error in errors { - out.push((current_path.clone(), error.clone())); - } - } - Errors::Nested { outer, inner } => { - flatten_inner(out, current_path.clone(), inner); - flatten_inner(out, current_path, outer); - } - Errors::List(errors) => { - for (i, errors) in errors.iter().enumerate() { - flatten_inner(out, format!("{current_path}[{i}]"), errors); - } - } - Errors::Fields(errors) => { - for (key, errors) in errors.iter() { - flatten_inner(out, format!("{current_path}.{key}"), errors); - } - } - } - } +impl_path_component_kind!(usize => Index); +impl_path_component_kind!(@'a; &'a str => Key); +impl_path_component_kind!(@'a; Cow<'a, str> => Key); +impl_path_component_kind!(String => Key); +impl_path_component_kind!(CompactString => Key); - let mut errors = vec![]; - flatten_inner(&mut errors, "value".to_string(), self); - errors +impl<'a, T: PathComponentKind> private::Sealed for &'a T {} +impl<'a, T: PathComponentKind> PathComponentKind for &'a T { + fn component_kind() -> Kind { + T::component_kind() } +} - /// Creates an empty list of errors. - /// - /// This is used as a fallback in case there is nothing to validate (such as when a field is marked `#[garde(skip)]`). +mod private { + pub trait Sealed {} +} + +impl Path { pub fn empty() -> Self { - Errors::Simple(Vec::new()) + Self { + components: List::new(), + } } - /// Creates a list of [`Error`] constructed via `f`. - pub fn simple(f: F) -> Errors - where - F: FnMut(&mut SimpleErrorBuilder), - { - SimpleErrorBuilder::dive(f) + pub fn new(component: C) -> Self { + Self { + components: List::new().append((C::component_kind(), component.to_compact_string())), + } } - /// Creates a nested [`Errors`] from a list of simple errors constructed via `outer`, and an arbitrary `inner` error. - pub fn nested(outer: Errors, inner: Errors) -> Errors { - Errors::Nested { - outer: Box::new(outer), - inner: Box::new(inner), + pub fn join(&self, component: C) -> Self { + Self { + components: self + .components + .append((C::component_kind(), component.to_compact_string())), } } - /// Creates a list of [`Errors`] constructed via `f`. - pub fn list(f: F) -> Errors - where - F: FnMut(&mut ListErrorBuilder), - { - ListErrorBuilder::dive(f) + #[doc(hidden)] + pub fn __iter_components_rev(&self) -> rc_list::Iter<'_, (Kind, CompactString)> { + self.components.iter() } - /// Creates a map of field names to [`Errors`] constructed via `f`. - pub fn fields(f: F) -> Errors - where - F: FnMut(&mut FieldsErrorBuilder), - { - FieldsErrorBuilder::dive(f) + #[doc(hidden)] + pub fn __iter_components(&self) -> impl DoubleEndedIterator { + let mut components = TempComponents::with_capacity(self.components.len()); + for (kind, component) in self.components.iter() { + components.push((*kind, component)); + } + components.into_iter() } } -// TODO: remove rename, change rules to not require field_name +type TempComponents<'a> = SmallVec<[(Kind, &'a CompactString); 8]>; -pub struct SimpleErrorBuilder { - inner: Vec, -} +impl std::fmt::Debug for Path { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + struct Components<'a> { + path: &'a Path, + } -impl SimpleErrorBuilder { - fn dive(mut f: F) -> Errors - where - F: FnMut(&mut SimpleErrorBuilder), - { - let mut builder = SimpleErrorBuilder { inner: Vec::new() }; - f(&mut builder); - Errors::Simple(builder.inner) - } + impl<'a> std::fmt::Debug for Components<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut list = f.debug_list(); + list.entries(self.path.__iter_components().rev().map(|(_, c)| c)) + .finish() + } + } - pub fn push(&mut self, error: Error) { - self.inner.push(error); + f.debug_struct("Path") + .field("components", &Components { path: self }) + .finish() } } -pub struct ListErrorBuilder { - inner: Vec, +impl std::fmt::Display for Path { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut components = self.__iter_components().rev().peekable(); + let mut first = true; + while let Some((kind, component)) = components.next() { + if first && kind == Kind::Index { + f.write_str("[")?; + } + first = false; + f.write_str(component.as_str())?; + if kind == Kind::Index { + f.write_str("]")?; + } + if let Some((kind, _)) = components.peek() { + match kind { + Kind::Key => f.write_str(".")?, + Kind::Index => f.write_str("[")?, + } + } + } + + Ok(()) + } } -impl ListErrorBuilder { - fn dive(mut f: F) -> Errors +#[cfg(feature = "serde")] +impl serde::Serialize for Path { + fn serialize(&self, serializer: S) -> Result where - F: FnMut(&mut ListErrorBuilder), + S: serde::Serializer, { - let mut builder = ListErrorBuilder { inner: Vec::new() }; - f(&mut builder); - Errors::List(builder.inner) - } - - pub fn push(&mut self, entry: impl Into) { - let entry = entry.into(); - - if entry.is_empty() { - return; - } - - self.inner.push(entry); + let str = self.to_compact_string(); + serializer.serialize_str(str.as_str()) } } -pub struct FieldsErrorBuilder { - inner: BTreeMap, Errors>, +#[doc(hidden)] +#[macro_export] +macro_rules! __select { + ($report:expr, $($component:ident).*) => {{ + let report = &$report; + let needle = [$(stringify!($component)),*]; + report.iter() + .filter(move |(path, _)| { + let components = path.__iter_components_rev(); + let needle = needle.iter().rev(); + + components.map(|(_, v)| v.as_str()).zip(needle).all(|(a, b)| &a == b) + }) + .map(|(_, error)| error) + }} } -impl FieldsErrorBuilder { - fn dive(mut f: F) -> Errors - where - F: FnMut(&mut FieldsErrorBuilder), - { - let mut builder = FieldsErrorBuilder { - inner: BTreeMap::new(), - }; - f(&mut builder); - Errors::Fields(builder.inner) - } +pub use crate::__select as select; - pub fn insert(&mut self, field: impl Into>, entry: impl Into) { - let entry = entry.into(); +#[cfg(test)] +mod tests { + use super::*; - if entry.is_empty() { - return; - } + const _: () = { + fn assert() {} + let _ = assert::; + }; - let existing = self.inner.insert(field.into(), entry); - assert!( - existing.is_none(), - "each field should only be dived into once" - ) + #[test] + fn path_join() { + let path = Path::new("a").join("b").join("c"); + assert_eq!(path.to_string(), "a.b.c"); } -} -impl std::fmt::Display for Errors { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let errors = self.flatten(); - let mut iter = errors.iter().peekable(); - while let Some((path, error)) = iter.next() { - write!(f, "{path}: {error}")?; - if iter.peek().is_some() { - writeln!(f)?; - } - } - Ok(()) + #[test] + fn report_select() { + let mut report = Report::new(); + report.append(Path::new("a").join("b"), Error::new("lol")); + report.append( + Path::new("a").join("b").join("c"), + Error::new("that seems wrong"), + ); + report.append(Path::new("a").join("b").join("c"), Error::new("pog")); + + assert_eq!( + select!(report, a.b.c).collect::>(), + [&Error::new("that seems wrong"), &Error::new("pog")] + ); } } - -impl std::error::Error for Errors {} diff --git a/garde/src/error/rc_list/mod.rs b/garde/src/error/rc_list/mod.rs new file mode 100644 index 0000000..e65e951 --- /dev/null +++ b/garde/src/error/rc_list/mod.rs @@ -0,0 +1,136 @@ +use std::mem::{swap, transmute}; +use std::sync::Arc; + +/// A reverse singly-linked list. +/// +/// Each node in the list is reference counted, +/// meaning cloning the list is safe and cheap. +/// +/// We're optimizing for cloning the list and +/// appending an item onto its end, both of which +/// are O(1). +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct List { + node: Option>>, + length: usize, +} + +impl List { + pub fn new() -> Self { + Self { + node: None, + length: 0, + } + } + + pub fn len(&self) -> usize { + self.length + } + + pub fn is_empty(&self) -> bool { + self.length == 0 + } + + pub fn append(&self, value: T) -> Self { + Self { + node: Some(Arc::new(Node { + prev: self.node.clone(), + value, + })), + length: self.length + 1, + } + } + + pub fn iter(&self) -> Iter<'_, T> { + Iter::new(self) + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Node { + prev: Option>>, + value: T, +} + +pub struct Iter<'a, T> { + list: &'a List, + next: Option>>, + node: Option>>, +} + +impl<'a, T> Iter<'a, T> { + fn new(list: &'a List) -> Self { + Self { + list, + next: None, + node: list.node.clone(), + } + } +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + let mut node = self.node.take(); + swap(&mut self.next, &mut node); + if let Some(prev) = self.next.as_ref().and_then(|node| node.prev.as_ref()) { + self.node = Some(Arc::clone(prev)); + } + self.next.as_ref().map(|next| { + // SAFETY: + // We're returning a reference here, but the reference points + // to the inside of an `Arc>`, meaning the reference + // to it is valid for as long as the `Arc` lives. It lives for + // as long as the `list` it came from, which is longer than + // `self` here. + // The items within `list` will never be moved around or + // mutated in any way, because it is immutable. The only + // supported operation is `append`, which constructs a new + // list with a pointer to the old one. + // The borrow checker will ensure that the items do not + // outlive their parent `list`. + unsafe { transmute(&next.value) } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const _: () = { + fn assert() {} + let _ = assert::>; + }; + + #[test] + fn rc_list_shenanigans() { + let list = List::new(); + assert_eq!(list.len(), 0); + + let mut iter = list.iter(); + let item = iter.next(); + drop(iter); + println!("{item:?}"); + + let a = list.append("a"); + let b = list.append("b"); + let c_a = a.append("c"); + let d_c_a = c_a.append("d"); + + let mut iter = list.iter(); + let item = iter.next(); + drop(iter); + println!("{item:?}"); + + assert_eq!(a.len(), 1); + assert_eq!(a.iter().copied().collect::>(), ["a"]); + assert_eq!(b.len(), 1); + assert_eq!(b.iter().copied().collect::>(), ["b"]); + assert_eq!(c_a.len(), 2); + assert_eq!(c_a.iter().copied().collect::>(), ["c", "a"]); + assert_eq!(d_c_a.len(), 3); + assert_eq!(d_c_a.iter().copied().collect::>(), ["d", "c", "a"]); + } +} diff --git a/garde/src/lib.rs b/garde/src/lib.rs index 07a63cb..5679c97 100644 --- a/garde/src/lib.rs +++ b/garde/src/lib.rs @@ -231,13 +231,16 @@ //! impl garde::Validate for MyVec { //! type Context = T::Context; //! -//! fn validate(&self, ctx: &Self::Context) -> Result<(), garde::Errors> { -//! garde::Errors::list(|errors| { -//! for item in self.0.iter() { -//! errors.push(item.validate(ctx)); -//! } -//! }) -//! .finish() +//! fn validate_into( +//! &self, +//! ctx: &Self::Context, +//! mut parent: &mut dyn FnMut() -> garde::Path, +//! report: &mut garde::Report +//! ) { +//! for (index, item) in self.0.iter().enumerate() { +//! let mut path = garde::util::nested_path!(parent, index); +//! item.validate_into(ctx, &mut path, report); +//! } //! } //! } //! @@ -254,18 +257,6 @@ //! } //! ``` //! -//! To make implementing the trait easier, the [`Errors`][`crate::error::Errors`] type supports a nesting builders. -//! - For list-like or tuple-like data structures, use [`Errors::list`][`crate::error::Errors::list`], -//! and its `.push` method to attach nested [`Errors`][`crate::error::Errors`]. -//! - For map-like data structures, use [`Errors::fields`][`crate::error::Errors::fields`], -//! and its `.insert` method to attach nested [`Errors`][`crate::error::Errors`]. -//! - For a "flat" error list, use [`Errors::simple`][`crate::error::Errors::simple`], -//! and its `.push` method to attach individual errors. -//! -//! The [`ListErrorBuilder::push`][`crate::error::ListErrorBuilder::push`] and -//! [`FieldsErrorBuilder::insert`][`crate::error::FieldsErrorBuilder::insert`] methods -//! will ignore any errors which are empty (via [`Errors::is_empty`][`crate::error::Errors::is_empty`]). -//! //! ### Integration with web frameworks //! //! - [`axum`](https://crates.io/crates/axum): https://crates.io/crates/axum_garde @@ -287,9 +278,67 @@ pub mod error; pub mod rules; pub mod validate; -pub use error::{Error, Errors}; +pub use error::{Error, Path, Report}; #[cfg(feature = "derive")] pub use garde_derive::Validate; pub use validate::{Unvalidated, Valid, Validate}; pub type Result = ::core::result::Result<(), Error>; + +pub mod external { + pub use {compact_str, smallvec}; +} + +#[doc(hidden)] +pub mod util { + use crate::error::PathComponentKind; + use crate::Path; + + #[inline] + pub fn __make_nested_path<'a, C: PathComponentKind + Clone + 'a>( + mut parent: impl FnMut() -> Path + 'a, + component: C, + ) -> impl FnMut() -> Path + 'a { + let mut nested = None::; + + #[inline] + move || MaybeJoin::maybe_join(&mut nested, &mut parent, || component.clone()) + } + + #[doc(hidden)] + #[macro_export] + macro_rules! __nested_path { + ($parent:ident, $key:expr) => { + $crate::util::__make_nested_path(&mut $parent, &$key) + }; + } + + pub use crate::__nested_path as nested_path; + + pub trait MaybeJoin { + fn maybe_join(&mut self, parent: P, component: CF) -> Path + where + C: PathComponentKind, + P: FnMut() -> Path, + CF: Fn() -> C; + } + + impl MaybeJoin for Option { + #[inline] + fn maybe_join(&mut self, mut parent: P, component: CF) -> Path + where + C: PathComponentKind, + P: FnMut() -> Path, + CF: Fn() -> C, + { + match self { + Some(path) => path.clone(), + None => { + let path = parent().join(component()); + *self = Some(path.clone()); + path + } + } + } + } +} diff --git a/garde/src/rules/inner.rs b/garde/src/rules/inner.rs index 547b926..c676669 100644 --- a/garde/src/rules/inner.rs +++ b/garde/src/rules/inner.rs @@ -10,58 +10,53 @@ //! //! The entrypoint is the [`Inner`] trait. Implementing this trait for a type allows that type to be used with the `#[garde(inner(..))]` rule. -use crate::error::ListErrorBuilder; -use crate::Errors; - -pub fn apply(field: &T, ctx: &C, f: F) -> Errors +pub fn apply(field: &T, f: F) where - T: Inner, - F: Fn(&U, &C) -> Errors, + T: Inner, + F: FnMut(&U, &K), { - field.validate_inner(ctx, f) + field.validate_inner(f) } pub trait Inner { - type ErrorBuilder; + type Key; - fn validate_inner(&self, ctx: &C, f: F) -> Errors + fn validate_inner(&self, f: F) where - F: Fn(&T, &C) -> Errors; + F: FnMut(&T, &Self::Key); } impl Inner for Vec { - type ErrorBuilder = ListErrorBuilder; + type Key = usize; - fn validate_inner(&self, ctx: &C, f: F) -> Errors + fn validate_inner(&self, f: F) where - F: Fn(&T, &C) -> Errors, + F: FnMut(&T, &Self::Key), { - self.as_slice().validate_inner(ctx, f) + self.as_slice().validate_inner(f) } } impl Inner for [T; N] { - type ErrorBuilder = ListErrorBuilder; + type Key = usize; - fn validate_inner(&self, ctx: &C, f: F) -> Errors + fn validate_inner(&self, f: F) where - F: Fn(&T, &C) -> Errors, + F: FnMut(&T, &Self::Key), { - self.as_slice().validate_inner(ctx, f) + self.as_slice().validate_inner(f) } } impl<'a, T> Inner for &'a [T] { - type ErrorBuilder = ListErrorBuilder; + type Key = usize; - fn validate_inner(&self, ctx: &C, f: F) -> Errors + fn validate_inner(&self, mut f: F) where - F: Fn(&T, &C) -> Errors, + F: FnMut(&T, &Self::Key), { - Errors::list(|b| { - for item in self.iter() { - b.push(f(item, ctx)); - } - }) + for (index, item) in self.iter().enumerate() { + f(item, &index); + } } } diff --git a/garde/src/validate.rs b/garde/src/validate.rs index 17ec8d6..f250e16 100644 --- a/garde/src/validate.rs +++ b/garde/src/validate.rs @@ -2,12 +2,13 @@ use std::fmt::Debug; -use crate::error::Errors; +use crate::error::{Path, PathComponentKind}; +use crate::Report; /// The core trait of this crate. /// -/// Validation checks all the conditions and returns all errors aggregated into -/// an `Errors` container. +/// Validation runs the fields through every validation rules, +/// and aggregates any errors into a [`Report`]. pub trait Validate { /// A user-provided context. /// @@ -16,7 +17,25 @@ pub trait Validate { /// Validates `Self`, returning an `Err` with an aggregate of all errors if /// the validation failed. - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors>; + /// + /// This method should not be implemented manually. Implement [`Validate::validate_into`] instead, + /// because [`Validate::validate`] has a default implementation that calls [`Validate::validate_into`]. + fn validate(&self, ctx: &Self::Context) -> Result<(), Report> { + let mut report = Report::new(); + self.validate_into(ctx, &mut Path::empty, &mut report); + match report.is_empty() { + true => Ok(()), + false => Err(report), + } + } + + /// Validates `Self`, aggregating all validation errors into `Report`. + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ); } /// A struct which wraps a valid instance of some `T`. @@ -60,7 +79,7 @@ impl Unvalidated { /// Validates `self`, transforming it into a `Valid`. /// This is the only way to create an instance of `Valid`. - pub fn validate(self, ctx: &::Context) -> Result, Errors> { + pub fn validate(self, ctx: &::Context) -> Result, Report> { self.0.validate(ctx)?; Ok(Valid(self.0)) } @@ -81,40 +100,65 @@ impl Debug for Unvalidated { impl<'a, T: ?Sized + Validate> Validate for &'a T { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - ::validate(self, ctx) + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + ::validate_into(self, ctx, parent, report) } } impl<'a, T: ?Sized + Validate> Validate for &'a mut T { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - ::validate(self, ctx) + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + ::validate_into(self, ctx, parent, report) } } impl Validate for std::boxed::Box { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - ::validate(self, ctx) + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + ::validate_into(self, ctx, parent, report) } } impl Validate for std::rc::Rc { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - ::validate(self, ctx) + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + ::validate_into(self, ctx, parent, report) } } impl Validate for std::sync::Arc { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - ::validate(self, ctx) + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + ::validate_into(self, ctx, parent, report) } } @@ -125,20 +169,12 @@ macro_rules! impl_validate_list { $T: Validate { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - let errors = Errors::list(|errors| { - for item in self.iter() { - errors.push( - ::validate(item, ctx) - .err() - .unwrap_or_else(Errors::empty), - ) - } - }); - if !errors.is_empty() { - return Err(errors); + + fn validate_into(&self, ctx: &Self::Context, mut parent: &mut dyn FnMut() -> Path, report: &mut Report) { + for (index, item) in self.iter().enumerate() { + let mut path = $crate::util::nested_path!(parent, index); + ::validate_into(item, ctx, &mut path, report); } - Ok(()) } } }; @@ -154,20 +190,17 @@ impl_validate_list!( [T]); impl Validate for [T; N] { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - let errors = Errors::list(|errors| { - for item in self.iter() { - errors.push( - ::validate(item, ctx) - .err() - .unwrap_or_else(Errors::empty), - ) - } - }); - if !errors.is_empty() { - return Err(errors); + + fn validate_into( + &self, + ctx: &Self::Context, + mut parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + for (index, item) in self.iter().enumerate() { + let mut path = crate::util::nested_path!(parent, index); + ::validate_into(item, ctx, &mut path, report); } - Ok(()) } } @@ -181,26 +214,22 @@ macro_rules! impl_validate_tuple { type Context = $A::Context; #[allow(non_snake_case)] - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - let errors = Errors::list(|errors| { - let ($A, $($T,)*) = self; - errors.push( - <$A as Validate>::validate($A, ctx) - .err() - .unwrap_or_else(|| Errors::empty()) - ); - $( - errors.push( - <$T as Validate>::validate($T, ctx) - .err() - .unwrap_or_else(|| Errors::empty()) - ); - )* - }); - if !errors.is_empty() { - return Err(errors); - } - Ok(()) + fn validate_into(&self, ctx: &Self::Context, mut parent: &mut dyn FnMut() -> Path, report: &mut Report) { + let ($A, $($T,)*) = self; + let mut index = 0usize; + let _index = index; + let mut path = $crate::util::nested_path!(parent, _index); + <$A as Validate>::validate_into($A, ctx, &mut path, report); + drop(path); + index += 1; + $({ + let _index = index; + let mut path = $crate::util::nested_path!(parent, _index); + <$T as Validate>::validate_into($T, ctx, &mut path, report); + drop(path); + index += 1; + })* + let _ = index; } } } @@ -222,70 +251,60 @@ impl_validate_tuple!(A, B, C, D, E, F, G, H, I, J, K, L); impl Validate for () { type Context = (); - fn validate(&self, _: &Self::Context) -> Result<(), Errors> { - Ok(()) - } + fn validate_into(&self, _: &Self::Context, _: &mut dyn FnMut() -> Path, _: &mut Report) {} } impl Validate for std::collections::HashMap where - std::borrow::Cow<'static, str>: From, - K: Clone, + K: Clone + PathComponentKind, V: Validate, { type Context = V::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - let errors = Errors::fields(|errors| { - for (key, value) in self.iter() { - errors.insert( - std::borrow::Cow::from(key.clone()), - ::validate(value, ctx) - .err() - .unwrap_or_else(Errors::empty), - ) - } - }); - if !errors.is_empty() { - return Err(errors); + fn validate_into( + &self, + ctx: &Self::Context, + mut parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + for (key, value) in self.iter() { + let mut path = crate::util::nested_path!(parent, key); + ::validate_into(value, ctx, &mut path, report); } - Ok(()) } } impl Validate for std::collections::BTreeMap where - std::borrow::Cow<'static, str>: From, - K: Clone, + K: Clone + PathComponentKind, V: Validate, { type Context = V::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - let errors = Errors::fields(|errors| { - for (key, value) in self.iter() { - errors.insert( - std::borrow::Cow::from(key.clone()), - ::validate(value, ctx) - .err() - .unwrap_or_else(Errors::empty), - ) - } - }); - if !errors.is_empty() { - return Err(errors); + fn validate_into( + &self, + ctx: &Self::Context, + mut parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + for (key, value) in self.iter() { + let mut path = crate::util::nested_path!(parent, key); + ::validate_into(value, ctx, &mut path, report); } - Ok(()) } } impl Validate for Option { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), Errors> { - match self { - Some(value) => value.validate(ctx), - None => Ok(()), + fn validate_into( + &self, + ctx: &Self::Context, + parent: &mut dyn FnMut() -> Path, + report: &mut Report, + ) { + if let Some(value) = self { + value.validate_into(ctx, parent, report) } } } diff --git a/garde/tests/rules/snapshots/rules__rules__allow_unvalidated__ascii_invalid.snap b/garde/tests/rules/snapshots/rules__rules__allow_unvalidated__ascii_invalid.snap index 8fb7371..9d496f5 100644 --- a/garde/tests/rules/snapshots/rules__rules__allow_unvalidated__ascii_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__allow_unvalidated__ascii_invalid.snap @@ -6,6 +6,6 @@ Test { field: "😂", unvalidated: "", } -value.field: not ascii +field: not ascii diff --git a/garde/tests/rules/snapshots/rules__rules__alphanumeric__alphanumeric_invalid.snap b/garde/tests/rules/snapshots/rules__rules__alphanumeric__alphanumeric_invalid.snap index 6b2929c..ad789bf 100644 --- a/garde/tests/rules/snapshots/rules__rules__alphanumeric__alphanumeric_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__alphanumeric__alphanumeric_invalid.snap @@ -8,7 +8,7 @@ Test { "!!!!", ], } -value.field: not alphanumeric -value.inner[0]: not alphanumeric +field: not alphanumeric +inner[0]: not alphanumeric diff --git a/garde/tests/rules/snapshots/rules__rules__ascii__ascii_invalid.snap b/garde/tests/rules/snapshots/rules__rules__ascii__ascii_invalid.snap index 9996dd0..1dd6ed5 100644 --- a/garde/tests/rules/snapshots/rules__rules__ascii__ascii_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__ascii__ascii_invalid.snap @@ -8,7 +8,7 @@ Test { "😂", ], } -value.field: not ascii -value.inner[0]: not ascii +field: not ascii +inner[0]: not ascii diff --git a/garde/tests/rules/snapshots/rules__rules__byte_length__byte_length_invalid.snap b/garde/tests/rules/snapshots/rules__rules__byte_length__byte_length_invalid.snap index 54e4acf..dfc5649 100644 --- a/garde/tests/rules/snapshots/rules__rules__byte_length__byte_length_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__byte_length__byte_length_invalid.snap @@ -8,8 +8,8 @@ Test { "aaaaaaaaa", ], } -value.field: byte length is lower than 10 -value.inner[0]: length is lower than 10 +field: byte length is lower than 10 +inner[0]: length is lower than 10 Test { field: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -17,8 +17,8 @@ Test { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ], } -value.field: byte length is greater than 100 -value.inner[0]: length is greater than 100 +field: byte length is greater than 100 +inner[0]: length is greater than 100 Test { field: "a😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂", @@ -26,6 +26,6 @@ Test { "a😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂", ], } -value.field: byte length is greater than 100 +field: byte length is greater than 100 diff --git a/garde/tests/rules/snapshots/rules__rules__byte_length__exact_length_invalid.snap b/garde/tests/rules/snapshots/rules__rules__byte_length__exact_length_invalid.snap index c2a86dd..73c3074 100644 --- a/garde/tests/rules/snapshots/rules__rules__byte_length__exact_length_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__byte_length__exact_length_invalid.snap @@ -8,8 +8,8 @@ Exact { "", ], } -value.field: byte length is lower than 4 -value.inner[0]: byte length is lower than 4 +field: byte length is lower than 4 +inner[0]: byte length is lower than 4 Exact { field: "a", @@ -17,8 +17,8 @@ Exact { "a", ], } -value.field: byte length is lower than 4 -value.inner[0]: byte length is lower than 4 +field: byte length is lower than 4 +inner[0]: byte length is lower than 4 Exact { field: "😂😂", @@ -26,7 +26,7 @@ Exact { "😂😂", ], } -value.field: byte length is greater than 4 -value.inner[0]: byte length is greater than 4 +field: byte length is greater than 4 +inner[0]: byte length is greater than 4 diff --git a/garde/tests/rules/snapshots/rules__rules__contains__contains_invalid.snap b/garde/tests/rules/snapshots/rules__rules__contains__contains_invalid.snap index 01eb184..903a6fe 100644 --- a/garde/tests/rules/snapshots/rules__rules__contains__contains_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__contains__contains_invalid.snap @@ -10,9 +10,9 @@ Test { "_____", ], } -value.field: does not contain "test" -value.field_call: does not contain "test" -value.field_path: does not contain "test" -value.inner[0]: does not contain "test" +field: does not contain "test" +field_call: does not contain "test" +field_path: does not contain "test" +inner[0]: does not contain "test" diff --git a/garde/tests/rules/snapshots/rules__rules__credit_card__credit_card_invalid.snap b/garde/tests/rules/snapshots/rules__rules__credit_card__credit_card_invalid.snap index 8b436d0..a607e99 100644 --- a/garde/tests/rules/snapshots/rules__rules__credit_card__credit_card_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__credit_card__credit_card_invalid.snap @@ -8,8 +8,8 @@ Test { "zduhefljsdfKJKJZHUI", ], } -value.field: not a valid credit card number: invalid format -value.inner[0]: not a valid credit card number: invalid format +field: not a valid credit card number: invalid format +inner[0]: not a valid credit card number: invalid format Test { field: "5236313877109141", @@ -17,7 +17,7 @@ Test { "5236313877109141", ], } -value.field: not a valid credit card number: invalid luhn -value.inner[0]: not a valid credit card number: invalid luhn +field: not a valid credit card number: invalid luhn +inner[0]: not a valid credit card number: invalid luhn diff --git a/garde/tests/rules/snapshots/rules__rules__custom__custom_invalid.snap b/garde/tests/rules/snapshots/rules__rules__custom__custom_invalid.snap index 3635465..ee9bcbb 100644 --- a/garde/tests/rules/snapshots/rules__rules__custom__custom_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__custom__custom_invalid.snap @@ -13,10 +13,10 @@ Test { ], uses_ctx: "", } -value.a: not equal to test -value.b: `b` is not equal to test -value.inner_a[0]: not equal to test -value.inner_b[0]: `b` is not equal to test -value.uses_ctx: length is lower than 4 +a: not equal to test +b: `b` is not equal to test +inner_a[0]: not equal to test +inner_b[0]: `b` is not equal to test +uses_ctx: length is lower than 4 diff --git a/garde/tests/rules/snapshots/rules__rules__custom__multi_custom_invalid.snap b/garde/tests/rules/snapshots/rules__rules__custom__multi_custom_invalid.snap index b7497f3..5b46c0c 100644 --- a/garde/tests/rules/snapshots/rules__rules__custom__multi_custom_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__custom__multi_custom_invalid.snap @@ -8,9 +8,9 @@ Multi { "asdf", ], } -value.field: not equal to test -value.field: not equal to test -value.inner[0]: not equal to test -value.inner[0]: not equal to test +field: not equal to test +field: not equal to test +inner[0]: not equal to test +inner[0]: not equal to test diff --git a/garde/tests/rules/snapshots/rules__rules__dive__email_invalid.snap b/garde/tests/rules/snapshots/rules__rules__dive__email_invalid.snap index 8eba11d..d1f4b1f 100644 --- a/garde/tests/rules/snapshots/rules__rules__dive__email_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__dive__email_invalid.snap @@ -1,5 +1,5 @@ --- -source: garde_derive_tests/tests/./rules/dive.rs +source: garde/tests/./rules/dive.rs expression: snapshot --- Test { @@ -42,15 +42,15 @@ Test { field: "", }, } -value.arc.field: length is lower than 1 -value.array[0].field: length is lower than 1 -value.array_ref[0].field: length is lower than 1 -value.boxed.field: length is lower than 1 -value.by_ref.field: length is lower than 1 -value.field.field: length is lower than 1 -value.rc.field: length is lower than 1 -value.slice[0].field: length is lower than 1 -value.tuples[0].field: length is lower than 1 -value.tuples[1].field: length is lower than 1 +arc.field: length is lower than 1 +array[0].field: length is lower than 1 +array_ref[0].field: length is lower than 1 +boxed.field: length is lower than 1 +by_ref.field: length is lower than 1 +field.field: length is lower than 1 +rc.field: length is lower than 1 +slice[0].field: length is lower than 1 +tuples[0].field: length is lower than 1 +tuples[1].field: length is lower than 1 diff --git a/garde/tests/rules/snapshots/rules__rules__dive_with_rules__dive_with_rules_invalid.snap b/garde/tests/rules/snapshots/rules__rules__dive_with_rules__dive_with_rules_invalid.snap index e567716..f109b29 100644 --- a/garde/tests/rules/snapshots/rules__rules__dive_with_rules__dive_with_rules_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__dive_with_rules__dive_with_rules_invalid.snap @@ -1,11 +1,11 @@ --- -source: garde_tests/tests/./rules/dive_with_rules.rs +source: garde/tests/./rules/dive_with_rules.rs expression: snapshot --- Test { field: [], } -value.field: length is lower than 1 +field: length is lower than 1 Test { field: [ @@ -14,6 +14,6 @@ Test { }, ], } -value.field[0].field: length is lower than 1 +field[0].field: length is lower than 1 diff --git a/garde/tests/rules/snapshots/rules__rules__email__email_invalid.snap b/garde/tests/rules/snapshots/rules__rules__email__email_invalid.snap index 655e3ec..a6d4eb7 100644 --- a/garde/tests/rules/snapshots/rules__rules__email__email_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__email__email_invalid.snap @@ -8,7 +8,7 @@ Test { "invalid.com", ], } -value.field: not a valid email: value is missing `@` -value.inner[0]: not a valid email: value is missing `@` +field: not a valid email: value is missing `@` +inner[0]: not a valid email: value is missing `@` diff --git a/garde/tests/rules/snapshots/rules__rules__inner__alphanumeric_invalid.snap b/garde/tests/rules/snapshots/rules__rules__inner__alphanumeric_invalid.snap index 3d93baa..0199da2 100644 --- a/garde/tests/rules/snapshots/rules__rules__inner__alphanumeric_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__inner__alphanumeric_invalid.snap @@ -9,6 +9,6 @@ Inner { ], ], } -value.inner[0][0]: not alphanumeric +inner[0][0]: not alphanumeric diff --git a/garde/tests/rules/snapshots/rules__rules__ip__ip_any_invalid.snap b/garde/tests/rules/snapshots/rules__rules__ip__ip_any_invalid.snap index 5905975..3dd0adc 100644 --- a/garde/tests/rules/snapshots/rules__rules__ip__ip_any_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__ip__ip_any_invalid.snap @@ -8,8 +8,8 @@ TestIpAny { "256.1.1.1", ], } -value.field: not a valid IP address -value.inner[0]: not a valid IP address +field: not a valid IP address +inner[0]: not a valid IP address TestIpAny { field: "25.1.1.", @@ -17,8 +17,8 @@ TestIpAny { "25.1.1.", ], } -value.field: not a valid IP address -value.inner[0]: not a valid IP address +field: not a valid IP address +inner[0]: not a valid IP address TestIpAny { field: "25,1,1,1", @@ -26,8 +26,8 @@ TestIpAny { "25,1,1,1", ], } -value.field: not a valid IP address -value.inner[0]: not a valid IP address +field: not a valid IP address +inner[0]: not a valid IP address TestIpAny { field: "2a02::223:6cff :fe8a:2e8a", @@ -35,7 +35,7 @@ TestIpAny { "2a02::223:6cff :fe8a:2e8a", ], } -value.field: not a valid IP address -value.inner[0]: not a valid IP address +field: not a valid IP address +inner[0]: not a valid IP address diff --git a/garde/tests/rules/snapshots/rules__rules__ip__ip_v4_invalid.snap b/garde/tests/rules/snapshots/rules__rules__ip__ip_v4_invalid.snap index 12e176d..48b5e5a 100644 --- a/garde/tests/rules/snapshots/rules__rules__ip__ip_v4_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__ip__ip_v4_invalid.snap @@ -8,8 +8,8 @@ TestIpV4 { "256.1.1.1", ], } -value.field: not a valid IPv4 address -value.inner[0]: not a valid IPv4 address +field: not a valid IPv4 address +inner[0]: not a valid IPv4 address TestIpV4 { field: "25.1.1.", @@ -17,8 +17,8 @@ TestIpV4 { "25.1.1.", ], } -value.field: not a valid IPv4 address -value.inner[0]: not a valid IPv4 address +field: not a valid IPv4 address +inner[0]: not a valid IPv4 address TestIpV4 { field: "25,1,1,1", @@ -26,8 +26,8 @@ TestIpV4 { "25,1,1,1", ], } -value.field: not a valid IPv4 address -value.inner[0]: not a valid IPv4 address +field: not a valid IPv4 address +inner[0]: not a valid IPv4 address TestIpV4 { field: "25.1 .1.1", @@ -35,8 +35,8 @@ TestIpV4 { "25.1 .1.1", ], } -value.field: not a valid IPv4 address -value.inner[0]: not a valid IPv4 address +field: not a valid IPv4 address +inner[0]: not a valid IPv4 address TestIpV4 { field: "1.1.1.1\n", @@ -44,8 +44,8 @@ TestIpV4 { "1.1.1.1\n", ], } -value.field: not a valid IPv4 address -value.inner[0]: not a valid IPv4 address +field: not a valid IPv4 address +inner[0]: not a valid IPv4 address TestIpV4 { field: "٧.2٥.3٣.243", @@ -53,7 +53,7 @@ TestIpV4 { "٧.2٥.3٣.243", ], } -value.field: not a valid IPv4 address -value.inner[0]: not a valid IPv4 address +field: not a valid IPv4 address +inner[0]: not a valid IPv4 address diff --git a/garde/tests/rules/snapshots/rules__rules__ip__ip_v6_invalid.snap b/garde/tests/rules/snapshots/rules__rules__ip__ip_v6_invalid.snap index 2d95fd4..7599cf8 100644 --- a/garde/tests/rules/snapshots/rules__rules__ip__ip_v6_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__ip__ip_v6_invalid.snap @@ -8,8 +8,8 @@ TestIpV6 { "foo", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "127.0.0.1", @@ -17,8 +17,8 @@ TestIpV6 { "127.0.0.1", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "12345::", @@ -26,8 +26,8 @@ TestIpV6 { "12345::", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "1::2::3::4", @@ -35,8 +35,8 @@ TestIpV6 { "1::2::3::4", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "1::zzz", @@ -44,8 +44,8 @@ TestIpV6 { "1::zzz", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "1:2", @@ -53,8 +53,8 @@ TestIpV6 { "1:2", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "fe80::223: 6cff:fe8a:2e8a", @@ -62,8 +62,8 @@ TestIpV6 { "fe80::223: 6cff:fe8a:2e8a", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "2a02::223:6cff :fe8a:2e8a", @@ -71,8 +71,8 @@ TestIpV6 { "2a02::223:6cff :fe8a:2e8a", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "::ffff:999.42.16.14", @@ -80,8 +80,8 @@ TestIpV6 { "::ffff:999.42.16.14", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address TestIpV6 { field: "::ffff:zzzz:0a0a", @@ -89,7 +89,7 @@ TestIpV6 { "::ffff:zzzz:0a0a", ], } -value.field: not a valid IPv6 address -value.inner[0]: not a valid IPv6 address +field: not a valid IPv6 address +inner[0]: not a valid IPv6 address diff --git a/garde/tests/rules/snapshots/rules__rules__length__exact_length_invalid.snap b/garde/tests/rules/snapshots/rules__rules__length__exact_length_invalid.snap index 449ef31..3d415ae 100644 --- a/garde/tests/rules/snapshots/rules__rules__length__exact_length_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__length__exact_length_invalid.snap @@ -8,8 +8,8 @@ Exact { "", ], } -value.field: length is lower than 2 -value.inner[0]: length is lower than 2 +field: length is lower than 2 +inner[0]: length is lower than 2 Exact { field: "a", @@ -17,8 +17,8 @@ Exact { "a", ], } -value.field: length is lower than 2 -value.inner[0]: length is lower than 2 +field: length is lower than 2 +inner[0]: length is lower than 2 Exact { field: "aaa", @@ -26,7 +26,7 @@ Exact { "aaa", ], } -value.field: length is greater than 2 -value.inner[0]: length is greater than 2 +field: length is greater than 2 +inner[0]: length is greater than 2 diff --git a/garde/tests/rules/snapshots/rules__rules__length__length_invalid.snap b/garde/tests/rules/snapshots/rules__rules__length__length_invalid.snap index c922bf7..ed4c7a9 100644 --- a/garde/tests/rules/snapshots/rules__rules__length__length_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__length__length_invalid.snap @@ -8,8 +8,8 @@ Test { "aaaaaaaaa", ], } -value.field: length is lower than 10 -value.inner[0]: length is lower than 10 +field: length is lower than 10 +inner[0]: length is lower than 10 Test { field: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -17,7 +17,7 @@ Test { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ], } -value.field: length is greater than 100 -value.inner[0]: length is greater than 100 +field: length is greater than 100 +inner[0]: length is greater than 100 diff --git a/garde/tests/rules/snapshots/rules__rules__multi_rule__multi_rule_invalid.snap b/garde/tests/rules/snapshots/rules__rules__multi_rule__multi_rule_invalid.snap index 670590f..15bf7f3 100644 --- a/garde/tests/rules/snapshots/rules__rules__multi_rule__multi_rule_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__multi_rule__multi_rule_invalid.snap @@ -8,8 +8,8 @@ Test { "text which does not begin with `test`", ], } -value.field: value does not begin with "test" -value.inner[0]: value does not begin with "test" +field: value does not begin with "test" +inner[0]: value does not begin with "test" Test { field: "non-ascii 😂😂😂", @@ -17,10 +17,10 @@ Test { "non-ascii 😂😂😂", ], } -value.field: not ascii -value.field: value does not begin with "test" -value.inner[0]: not ascii -value.inner[0]: value does not begin with "test" +field: not ascii +field: value does not begin with "test" +inner[0]: not ascii +inner[0]: value does not begin with "test" Test { field: "aaaaaaaaa", @@ -28,10 +28,10 @@ Test { "aaaaaaaaa", ], } -value.field: length is lower than 10 -value.field: value does not begin with "test" -value.inner[0]: length is lower than 10 -value.inner[0]: value does not begin with "test" +field: length is lower than 10 +field: value does not begin with "test" +inner[0]: length is lower than 10 +inner[0]: value does not begin with "test" Test { field: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -39,9 +39,9 @@ Test { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ], } -value.field: length is greater than 100 -value.field: value does not begin with "test" -value.inner[0]: length is greater than 100 -value.inner[0]: value does not begin with "test" +field: length is greater than 100 +field: value does not begin with "test" +inner[0]: length is greater than 100 +inner[0]: value does not begin with "test" diff --git a/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap b/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap index 6f7467e..52a4962 100644 --- a/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap @@ -50,22 +50,22 @@ Test { "😂", ), } -value.alphanumeric: not alphanumeric -value.ascii: not ascii -value.byte_length_min1_str: byte length is lower than 1 -value.byte_length_min1_u8_slice: byte length is lower than 1 -value.contains_a: does not contain "a" -value.credit_card: not a valid credit card number: invalid format -value.email: not a valid email: value is missing `@` -value.ip: not a valid IP address -value.length_min1: length is lower than 1 -value.pat_a_or_b: does not match pattern /a|b/ -value.phone_number: not a valid phone number: not a number -value.prefix_a: value does not begin with "a" -value.range_min1: lower than 1 -value.required: not set -value.suffix_a: does not end with "a" -value.url: not a valid url: relative URL without a base +alphanumeric: not alphanumeric +ascii: not ascii +byte_length_min1_str: byte length is lower than 1 +byte_length_min1_u8_slice: byte length is lower than 1 +contains_a: does not contain "a" +credit_card: not a valid credit card number: invalid format +email: not a valid email: value is missing `@` +ip: not a valid IP address +length_min1: length is lower than 1 +pat_a_or_b: does not match pattern /a|b/ +phone_number: not a valid phone number: not a number +prefix_a: value does not begin with "a" +range_min1: lower than 1 +required: not set +suffix_a: does not end with "a" +url: not a valid url: relative URL without a base Test { alphanumeric: None, @@ -85,6 +85,6 @@ Test { suffix_a: None, url: None, } -value.required: not set +required: not set diff --git a/garde/tests/rules/snapshots/rules__rules__pattern__pattern_invalid.snap b/garde/tests/rules/snapshots/rules__rules__pattern__pattern_invalid.snap index 25de4c9..0a9458c 100644 --- a/garde/tests/rules/snapshots/rules__rules__pattern__pattern_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__pattern__pattern_invalid.snap @@ -10,10 +10,10 @@ Test { "dcba", ], } -value.field: does not match pattern /^abcd|efgh$/ -value.field_call: does not match pattern /^abcd|efgh$/ -value.field_path: does not match pattern /^abcd|efgh$/ -value.inner[0]: does not match pattern /^abcd|efgh$/ +field: does not match pattern /^abcd|efgh$/ +field_call: does not match pattern /^abcd|efgh$/ +field_path: does not match pattern /^abcd|efgh$/ +inner[0]: does not match pattern /^abcd|efgh$/ Test { field: "hgfe", @@ -23,9 +23,9 @@ Test { "hgfe", ], } -value.field: does not match pattern /^abcd|efgh$/ -value.field_call: does not match pattern /^abcd|efgh$/ -value.field_path: does not match pattern /^abcd|efgh$/ -value.inner[0]: does not match pattern /^abcd|efgh$/ +field: does not match pattern /^abcd|efgh$/ +field_call: does not match pattern /^abcd|efgh$/ +field_path: does not match pattern /^abcd|efgh$/ +inner[0]: does not match pattern /^abcd|efgh$/ diff --git a/garde/tests/rules/snapshots/rules__rules__phone_number__phone_number_invalid.snap b/garde/tests/rules/snapshots/rules__rules__phone_number__phone_number_invalid.snap index 93bfc49..7397e54 100644 --- a/garde/tests/rules/snapshots/rules__rules__phone_number__phone_number_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__phone_number__phone_number_invalid.snap @@ -8,8 +8,8 @@ Test { "14152370800", ], } -value.field: not a valid phone number: invalid country code -value.inner[0]: not a valid phone number: invalid country code +field: not a valid phone number: invalid country code +inner[0]: not a valid phone number: invalid country code Test { field: "0642926829", @@ -17,8 +17,8 @@ Test { "0642926829", ], } -value.field: not a valid phone number: invalid country code -value.inner[0]: not a valid phone number: invalid country code +field: not a valid phone number: invalid country code +inner[0]: not a valid phone number: invalid country code Test { field: "00642926829", @@ -26,8 +26,8 @@ Test { "00642926829", ], } -value.field: not a valid phone number: invalid country code -value.inner[0]: not a valid phone number: invalid country code +field: not a valid phone number: invalid country code +inner[0]: not a valid phone number: invalid country code Test { field: "A012", @@ -35,8 +35,8 @@ Test { "A012", ], } -value.field: not a valid phone number: invalid country code -value.inner[0]: not a valid phone number: invalid country code +field: not a valid phone number: invalid country code +inner[0]: not a valid phone number: invalid country code Test { field: "TEXT", @@ -44,7 +44,7 @@ Test { "TEXT", ], } -value.field: not a valid phone number: not a number -value.inner[0]: not a valid phone number: not a number +field: not a valid phone number: not a number +inner[0]: not a valid phone number: not a number diff --git a/garde/tests/rules/snapshots/rules__rules__prefix__prefix_invalid.snap b/garde/tests/rules/snapshots/rules__rules__prefix__prefix_invalid.snap index 4dab250..1e34140 100644 --- a/garde/tests/rules/snapshots/rules__rules__prefix__prefix_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__prefix__prefix_invalid.snap @@ -8,8 +8,8 @@ Test { "a", ], } -value.field: value does not begin with "test" -value.inner[0]: value does not begin with "test" +field: value does not begin with "test" +inner[0]: value does not begin with "test" Test { field: "_test", @@ -17,7 +17,7 @@ Test { "_test", ], } -value.field: value does not begin with "test" -value.inner[0]: value does not begin with "test" +field: value does not begin with "test" +inner[0]: value does not begin with "test" diff --git a/garde/tests/rules/snapshots/rules__rules__range__range_invalid.snap b/garde/tests/rules/snapshots/rules__rules__range__range_invalid.snap index 1e36ecb..619e898 100644 --- a/garde/tests/rules/snapshots/rules__rules__range__range_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__range__range_invalid.snap @@ -9,9 +9,9 @@ Test { 9, ], } -value.field: lower than 10 -value.inner[0]: lower than 10 -value.refers_to_field: greater than 9 +field: lower than 10 +inner[0]: lower than 10 +refers_to_field: greater than 9 Test { field: 101, @@ -20,8 +20,8 @@ Test { 101, ], } -value.field: greater than 100 -value.inner[0]: greater than 100 -value.refers_to_field: greater than 101 +field: greater than 100 +inner[0]: greater than 100 +refers_to_field: greater than 101 diff --git a/garde/tests/rules/snapshots/rules__rules__suffix__suffix_invalid.snap b/garde/tests/rules/snapshots/rules__rules__suffix__suffix_invalid.snap index 76580ec..1b3c0c1 100644 --- a/garde/tests/rules/snapshots/rules__rules__suffix__suffix_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__suffix__suffix_invalid.snap @@ -8,8 +8,8 @@ Test { "a", ], } -value.field: does not end with "test" -value.inner[0]: does not end with "test" +field: does not end with "test" +inner[0]: does not end with "test" Test { field: "test_", @@ -17,7 +17,7 @@ Test { "test_", ], } -value.field: does not end with "test" -value.inner[0]: does not end with "test" +field: does not end with "test" +inner[0]: does not end with "test" diff --git a/garde/tests/rules/snapshots/rules__rules__url__url_enum_invalid.snap b/garde/tests/rules/snapshots/rules__rules__url__url_enum_invalid.snap index 5a6edfe..ee74bcd 100644 --- a/garde/tests/rules/snapshots/rules__rules__url__url_enum_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__url__url_enum_invalid.snap @@ -11,13 +11,13 @@ Struct { ], }, } -value.field: not a valid url: relative URL without a base -value.v.field: not a valid url: relative URL without a base -value.v.inner[0]: not a valid url: relative URL without a base +field: not a valid url: relative URL without a base +v.field: not a valid url: relative URL without a base +v.inner[0]: not a valid url: relative URL without a base Tuple( "htt ps://www.youtube.com/watch?v=dQw4w9WgXcQ", ) -value[0]: not a valid url: relative URL without a base +[0]: not a valid url: relative URL without a base diff --git a/garde/tests/rules/snapshots/rules__rules__url__url_invalid.snap b/garde/tests/rules/snapshots/rules__rules__url__url_invalid.snap index 9d8b4e1..c7f2d75 100644 --- a/garde/tests/rules/snapshots/rules__rules__url__url_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__url__url_invalid.snap @@ -8,7 +8,7 @@ Struct { "asdf", ], } -value.field: not a valid url: relative URL without a base -value.inner[0]: not a valid url: relative URL without a base +field: not a valid url: relative URL without a base +inner[0]: not a valid url: relative URL without a base diff --git a/garde/tests/rules/snapshots/rules__rules__url__url_tuple_invalid.snap b/garde/tests/rules/snapshots/rules__rules__url__url_tuple_invalid.snap index daed773..66dea4a 100644 --- a/garde/tests/rules/snapshots/rules__rules__url__url_tuple_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__url__url_tuple_invalid.snap @@ -1,10 +1,10 @@ --- -source: garde_derive_tests/tests/./rules/url.rs +source: garde/tests/./rules/url.rs expression: snapshot --- Tuple( "htt ps://www.youtube.com/watch?v=dQw4w9WgXcQ", ) -value[0]: not a valid url: relative URL without a base +[0]: not a valid url: relative URL without a base diff --git a/garde/tests/rules/url.rs b/garde/tests/rules/url.rs index 599851b..4d474fc 100644 --- a/garde/tests/rules/url.rs +++ b/garde/tests/rules/url.rs @@ -105,20 +105,5 @@ fn url_valid_wrapper() { field: "htt ps://www.youtube.com/watch?v=dQw4w9WgXcQ", inner: &["htt ps://www.youtube.com/watch?v=dQw4w9WgXcQ"], }; - println!("{}", value.validate(&()).unwrap_err()); + println!("{:?}", value.validate(&()).unwrap_err()); } - -/* #[derive(garde::Validate)] -struct _Struct { - field: u64, -} - -#[derive(garde::Validate)] -struct _Tuple(u64); - -#[derive(garde::Validate)] -enum _Enum { - Struct { field: u64 }, - Tuple(u64), -} - */ diff --git a/garde/tests/rules/util.rs b/garde/tests/rules/util.rs index d619a0e..7095d4e 100644 --- a/garde/tests/rules/util.rs +++ b/garde/tests/rules/util.rs @@ -8,11 +8,15 @@ use owo_colors::OwoColorize; pub fn check_ok(cases: &[T], ctx: &T::Context) { let mut some_failed = false; for case in cases { - if let Err(error) = case.validate(ctx) { + if let Err(report) = case.validate(ctx) { eprintln!( "{} input: {case:?}, errors: [{}]", "FAIL".red(), - error.to_string().split('\n').collect::>().join("; ") + report + .iter() + .map(|(path, error)| format!("{path}: {error}")) + .collect::>() + .join("; ") ); some_failed = true; } @@ -28,9 +32,9 @@ pub fn __check_fail(cases: &[T], ctx: &T::Context) -> Strin let mut some_success = false; let mut snapshot = String::new(); for case in cases { - if let Err(error) = case.validate(ctx) { + if let Err(report) = case.validate(ctx) { writeln!(&mut snapshot, "{case:#?}").unwrap(); - for (path, error) in error.flatten() { + for (path, error) in report.iter() { writeln!(&mut snapshot, "{path}: {error}").unwrap(); } writeln!(&mut snapshot).unwrap(); diff --git a/garde/tests/ui/compile-pass/custom.rs b/garde/tests/ui/compile-pass/custom.rs index dada754..16a4a98 100644 --- a/garde/tests/ui/compile-pass/custom.rs +++ b/garde/tests/ui/compile-pass/custom.rs @@ -35,20 +35,16 @@ struct MyVec(Vec); impl garde::Validate for MyVec { type Context = T::Context; - fn validate(&self, ctx: &Self::Context) -> Result<(), garde::Errors> { - let errors = garde::Errors::list(|errors| { - for item in self.0.iter() { - if let Err(e) = item.validate(ctx) { - errors.push(e); - } - } - }); - - if !errors.is_empty() { - return Err(errors); + fn validate_into( + &self, + ctx: &Self::Context, + mut path: &mut dyn FnMut() -> garde::Path, + report: &mut garde::Report, + ) { + for (index, item) in self.0.iter().enumerate() { + let mut path = garde::util::nested_path!(path, index); + item.validate_into(ctx, &mut path, report); } - - Ok(()) } } diff --git a/garde_derive/src/emit.rs b/garde_derive/src/emit.rs index 36a807e..87d4ffc 100644 --- a/garde_derive/src/emit.rs +++ b/garde_derive/src/emit.rs @@ -10,6 +10,15 @@ pub fn emit(input: model::Validate) -> TokenStream2 { input.to_token_stream() } +/* +fn validate_into Path>( + &self, + ctx: &Self::Context, + parent: F, + report: &mut Report, + ); +*/ + impl ToTokens for model::Validate { fn to_tokens(&self, tokens: &mut TokenStream2) { let ident = &self.ident; @@ -22,13 +31,15 @@ impl ToTokens for model::Validate { type Context = #context_ty ; #[allow(clippy::needless_borrow)] - fn validate(&self, #context_ident: &Self::Context) -> ::core::result::Result<(), ::garde::error::Errors> { + fn validate_into( + &self, + #context_ident: &Self::Context, + mut __garde_path: &mut dyn FnMut() -> ::garde::Path, + __garde_report: &mut ::garde::error::Report, + ) { let __garde_user_ctx = &#context_ident; - ( - #kind - ) - .finish() + #kind } } } @@ -71,19 +82,14 @@ struct Validation<'a>(&'a model::ValidateVariant); impl<'a> ToTokens for Validation<'a> { fn to_tokens(&self, tokens: &mut TokenStream2) { - // TODO: deduplicate this a bit match &self.0 { model::ValidateVariant::Struct(fields) => { let fields = Struct(fields); - quote! { - ::garde::error::Errors::fields(|__garde_errors| {#fields}) - } + quote! {{#fields}} } model::ValidateVariant::Tuple(fields) => { let fields = Tuple(fields); - quote! { - ::garde::error::Errors::list(|__garde_errors| {#fields}) - } + quote! {{#fields}} } } .to_tokens(tokens) @@ -99,12 +105,10 @@ impl<'a> ToTokens for Struct<'a> { .iter() .map(|(key, field)| (Binding::Ident(key), field, key.to_string())), |key, value| { - quote! { - __garde_errors.insert( - #key, - #value, - ); - } + quote! {{ + let mut __garde_path = ::garde::util::nested_path!(__garde_path, #key); + #value + }} }, ) .to_tokens(tokens) @@ -119,11 +123,12 @@ impl<'a> ToTokens for Tuple<'a> { self.0 .iter() .enumerate() - .map(|(index, field)| (Binding::Index(index), field, ())), - |(), value| { - quote! { - __garde_errors.push(#value); - } + .map(|(index, field)| (Binding::Index(index), field, index)), + |index, value| { + quote! {{ + let mut __garde_path = ::garde::util::nested_path!(__garde_path, #index); + #value + }} }, ) .to_tokens(tokens) @@ -139,13 +144,7 @@ impl<'a> ToTokens for Inner<'a> { let outer = match rule_set.has_top_level_rules() { true => { let rules = Rules(rule_set); - Some(quote! { - ::garde::error::Errors::simple( - |__garde_errors| { - #rules - } - ) - }) + Some(quote! {#rules}) } false => None, }; @@ -153,16 +152,11 @@ impl<'a> ToTokens for Inner<'a> { let value = match (outer, inner) { (Some(outer), Some(inner)) => quote! { - ::garde::error::Errors::nested( - #outer, - #inner, - ) + #outer + #inner }, (None, Some(inner)) => quote! { - ::garde::error::Errors::nested( - ::garde::error::Errors::empty(), - #inner, - ) + #inner }, (Some(outer), None) => outer, (None, None) => return, @@ -171,11 +165,11 @@ impl<'a> ToTokens for Inner<'a> { quote! { ::garde::rules::inner::apply( &*__garde_binding, - __garde_user_ctx, - |__garde_binding, __garde_user_ctx| { + |__garde_binding, __garde_inner_key| { + let mut __garde_path = ::garde::util::nested_path!(__garde_path, __garde_inner_key); #value } - ) + ); } .to_tokens(tokens) } @@ -205,10 +199,10 @@ impl<'a> ToTokens for Rules<'a> { for custom_rule in rule_set.custom_rules.iter() { quote! { if let Err(__garde_error) = (#custom_rule)(&*__garde_binding, &__garde_user_ctx) { - __garde_errors.push(__garde_error) + __garde_report.append(__garde_path(), __garde_error); } } - .to_tokens(tokens) + .to_tokens(tokens); } for rule in rule_set.rules.iter() { @@ -249,9 +243,10 @@ impl<'a> ToTokens for Rules<'a> { }), }, }; + quote! { if let Err(__garde_error) = (::garde::rules::#name::apply)(&*__garde_binding, #args) { - __garde_errors.push(__garde_error) + __garde_report.append(__garde_path(), __garde_error); } } .to_tokens(tokens) @@ -281,24 +276,19 @@ where for (binding, field, extra) in fields { let rules = Rules(&field.rule_set); let outer = match field.has_top_level_rules() { - true => Some(quote! { - ::garde::error::Errors::simple(|__garde_errors| {#rules}) - }), + true => Some(quote! {{#rules}}), false => None, }; let inner = match (&field.dive, &field.rule_set.inner) { (Some(..), None) => Some(quote! { - ::garde::validate::Validate::validate( + ::garde::validate::Validate::validate_into( &*__garde_binding, __garde_user_ctx, - ) - .err() - .unwrap_or_else(::garde::error::Errors::empty) + &mut __garde_path, + __garde_report, + ); }), - (None, Some(inner)) => { - let inner = Inner(inner); - Some(inner.to_token_stream()) - } + (None, Some(inner)) => Some(Inner(inner).to_token_stream()), (None, None) => None, // TODO: encode this via the type system instead? _ => unreachable!("`dive` and `inner` are mutually exclusive"), @@ -306,28 +296,17 @@ where let value = match (outer, inner) { (Some(outer), Some(inner)) => quote! { - { - let __garde_binding = &*#binding; - ::garde::error::Errors::nested( - #outer, - #inner, - ) - } + let __garde_binding = &*#binding; + #inner + #outer }, (None, Some(inner)) => quote! { - { - let __garde_binding = &*#binding; - ::garde::error::Errors::nested( - ::garde::error::Errors::empty(), - #inner, - ) - } + let __garde_binding = &*#binding; + #inner }, (Some(outer), None) => quote! { - { - let __garde_binding = &*#binding; - #outer - } + let __garde_binding = &*#binding; + #outer }, (None, None) => unreachable!("field should already be skipped"), }; diff --git a/integrations/axum_garde/src/error.rs b/integrations/axum_garde/src/error.rs index 297a17b..eb95328 100644 --- a/integrations/axum_garde/src/error.rs +++ b/integrations/axum_garde/src/error.rs @@ -1,6 +1,6 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use garde::Errors; +use garde::Report; use thiserror::Error; /// Rejection used for [`WithValidation`] @@ -14,7 +14,7 @@ pub enum WithValidationRejection { /// Variant for the payload's validation errors. Responds with status code /// `422 Unprocessable Content` #[error(transparent)] - ValidationError(#[from] Errors), + ValidationError(#[from] Report), } impl IntoResponse for WithValidationRejection { diff --git a/integrations/axum_garde/src/lib.rs b/integrations/axum_garde/src/lib.rs index e0292dd..9396c86 100644 --- a/integrations/axum_garde/src/lib.rs +++ b/integrations/axum_garde/src/lib.rs @@ -34,7 +34,7 @@ missing_debug_implementations, missing_docs )] -#![deny(unreachable_pub, private_in_public)] +#![deny(unreachable_pub)] #![forbid(unsafe_code)] mod error;