Skip to content

Commit

Permalink
update docs + impl Serialize for Report
Browse files Browse the repository at this point in the history
  • Loading branch information
jprochazk committed Sep 1, 2023
1 parent 3f7c325 commit a989bcb
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 47 deletions.
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,15 @@ struct MyVec<T>(Vec<T>);
impl<T: garde::Validate> garde::Validate for MyVec<T> {
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,
current_path: &garde::Path,
report: &mut garde::Report
) {
for (index, item) in self.0.iter().enumerate() {
item.validate_into(ctx, &current_path.join(index), report);
}
}
}

Expand All @@ -261,13 +263,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
Expand Down
12 changes: 7 additions & 5 deletions garde/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,29 @@ 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"]
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 = "1.11.0"
compact_str = "0.7.1"
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 }

Expand Down
51 changes: 37 additions & 14 deletions garde/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,37 @@ 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<Item = &(Path, Error)> {
self.errors.iter()
}

/// Returns `true` if the report contains no validation errors.
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
Expand All @@ -47,6 +59,7 @@ impl std::fmt::Display for Report {
impl std::error::Error for Report {}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Error {
message: CompactString,
}
Expand Down Expand Up @@ -133,6 +146,15 @@ impl Path {
pub fn __iter_components_rev(&self) -> rc_list::Iter<'_, (Kind, CompactString)> {
self.components.iter()
}

#[doc(hidden)]
pub fn __iter_components(&self) -> impl DoubleEndedIterator<Item = (Kind, &CompactString)> {
let mut components = TempComponents::with_capacity(self.components.len());
for (kind, component) in self.components.iter() {
components.push((*kind, component));
}
components.into_iter()
}
}

type TempComponents<'a> = SmallVec<[(Kind, &'a CompactString); 8]>;
Expand All @@ -146,12 +168,7 @@ impl std::fmt::Debug for Path {
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();
let mut components = TempComponents::with_capacity(self.path.components.len());

for (kind, component) in self.path.components.iter() {
components.push((*kind, component));
}
list.entries(components.iter().rev().map(|(_, c)| c))
list.entries(self.path.__iter_components().rev().map(|(_, c)| c))
.finish()
}
}
Expand All @@ -164,20 +181,15 @@ impl std::fmt::Debug for Path {

impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut components = TempComponents::with_capacity(self.components.len());
for (kind, component) in self.components.iter() {
components.push((*kind, component));
}

let mut components = components.iter().rev().peekable();
let mut components = self.__iter_components().rev().peekable();
let mut first = true;
while let Some((kind, component)) = components.next() {
if first && kind == &Kind::Index {
if first && kind == Kind::Index {
f.write_str("[")?;
}
first = false;
f.write_str(component.as_str())?;
if kind == &Kind::Index {
if kind == Kind::Index {
f.write_str("]")?;
}
if let Some((kind, _)) = components.peek() {
Expand All @@ -192,6 +204,17 @@ impl std::fmt::Display for Path {
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for Path {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let str = self.to_compact_string();
serializer.serialize_str(str.as_str())
}
}

#[doc(hidden)]
#[macro_export]
macro_rules! __select {
Expand Down
12 changes: 0 additions & 12 deletions garde/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,18 +256,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
Expand Down
9 changes: 7 additions & 2 deletions garde/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ 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.
///
Expand All @@ -17,6 +17,9 @@ pub trait Validate {

/// Validates `Self`, returning an `Err` with an aggregate of all errors if
/// the validation failed.
///
/// 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, &Path::empty(), &mut report);
Expand All @@ -26,6 +29,8 @@ pub trait Validate {
}
}

/// Validates `Self`, aggregating all validation errors
/// with a path beginning at `current_path` into the `report`.
fn validate_into(&self, ctx: &Self::Context, current_path: &Path, report: &mut Report);
}

Expand Down

0 comments on commit a989bcb

Please sign in to comment.