Skip to content

Commit 4b932ca

Browse files
committed
chore: Rework error iterator
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent c2341f1 commit 4b932ca

File tree

16 files changed

+331
-33
lines changed

16 files changed

+331
-33
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
- `evaluate()` top-level function for convenient access to structured validation output.
88
- **CLI**: Schema-only validation now also validates all referenced schemas. [#804](https://github.com/Stranger6667/jsonschema/issues/804)
99
- Support for additional `contentEncoding` values per RFC 4648: `base64url`, `base32`, `base32hex`, and `base16`. These encodings are now validated alongside the existing `base64` support in Draft 6 and 7. [#26](https://github.com/Stranger6667/jsonschema/issues/26)
10+
- `validator.iter_errors(instance).into_errors()`. It returns a `ValidationErrors` type that collects validation errors and implements `std::error::Error`. [#451](https://github.com/Stranger6667/jsonschema/issues/451)
1011

1112
### Changed
1213

1314
- **BREAKING**: `ValidationError` fields are private; use `instance()`, `kind()`, `instance_path()`, and `schema_path()` instead of accessing struct fields directly.
15+
- **BREAKING**: `ErrorIterator` is now a newtype wrapper instead of `Box<dyn ValidationErrorIterator>`.
1416

1517
### Performance
1618

MIGRATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ let instance_path = error.instance_path();
1313
let schema_path = error.schema_path();
1414
```
1515

16+
### `ErrorIterator` is now a struct
17+
18+
`ErrorIterator` used to be a `type` alias to `Box<dyn ValidationErrorIterator<'_>>` and is now a struct wrapping that iterator.
19+
1620
## Upgrading from 0.35.x to 0.36.0
1721

1822
### Removal of `Validator::apply`, `Output`, and `BasicOutput`

crates/jsonschema/src/error.rs

Lines changed: 295 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ use std::{
4646
error,
4747
fmt::{self, Formatter, Write},
4848
iter::{empty, once},
49+
slice,
4950
string::FromUtf8Error,
51+
vec,
5052
};
5153

5254
/// An error that can occur during validation.
@@ -88,15 +90,145 @@ impl<'a, T> ValidationErrorIterator<'a> for T where
8890
{
8991
}
9092

91-
pub type ErrorIterator<'a> = Box<dyn ValidationErrorIterator<'a> + 'a>;
93+
/// A lazily-evaluated iterator over validation errors.
94+
///
95+
/// Use [`into_errors()`](Self::into_errors) to convert into [`ValidationErrors`],
96+
/// which implements [`std::error::Error`] for integration with error handling libraries.
97+
pub struct ErrorIterator<'a> {
98+
iter: Box<dyn ValidationErrorIterator<'a> + 'a>,
99+
}
100+
101+
impl<'a> ErrorIterator<'a> {
102+
#[inline]
103+
pub(crate) fn from_iterator<T>(iterator: T) -> Self
104+
where
105+
T: ValidationErrorIterator<'a> + 'a,
106+
{
107+
Self {
108+
iter: Box::new(iterator),
109+
}
110+
}
111+
112+
/// Collects all errors into [`ValidationErrors`], which implements [`std::error::Error`].
113+
#[inline]
114+
#[must_use]
115+
pub fn into_errors(self) -> ValidationErrors<'a> {
116+
ValidationErrors {
117+
errors: self.collect(),
118+
}
119+
}
120+
}
121+
122+
/// An owned collection of validation errors that implements [`std::error::Error`].
123+
///
124+
/// Obtain this by calling [`ErrorIterator::into_errors()`].
125+
pub struct ValidationErrors<'a> {
126+
errors: Vec<ValidationError<'a>>,
127+
}
128+
129+
impl<'a> ValidationErrors<'a> {
130+
#[inline]
131+
#[must_use]
132+
pub fn len(&self) -> usize {
133+
self.errors.len()
134+
}
135+
136+
#[inline]
137+
#[must_use]
138+
pub fn is_empty(&self) -> bool {
139+
self.errors.is_empty()
140+
}
141+
142+
/// Returns the errors as a slice.
143+
#[inline]
144+
#[must_use]
145+
pub fn as_slice(&self) -> &[ValidationError<'a>] {
146+
&self.errors
147+
}
148+
149+
#[inline]
150+
pub fn iter(&self) -> slice::Iter<'_, ValidationError<'a>> {
151+
self.errors.iter()
152+
}
153+
154+
#[inline]
155+
pub fn iter_mut(&mut self) -> slice::IterMut<'_, ValidationError<'a>> {
156+
self.errors.iter_mut()
157+
}
158+
}
159+
160+
impl fmt::Display for ValidationErrors<'_> {
161+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
162+
if self.errors.is_empty() {
163+
f.write_str("Validation succeeded")
164+
} else {
165+
writeln!(f, "Validation errors:")?;
166+
for (idx, error) in self.errors.iter().enumerate() {
167+
writeln!(f, "{:02}: {error}", idx + 1)?;
168+
}
169+
Ok(())
170+
}
171+
}
172+
}
173+
174+
impl fmt::Debug for ValidationErrors<'_> {
175+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
176+
f.debug_struct("ValidationErrors")
177+
.field("errors", &self.errors)
178+
.finish()
179+
}
180+
}
181+
182+
impl error::Error for ValidationErrors<'_> {}
183+
184+
impl<'a> Iterator for ErrorIterator<'a> {
185+
type Item = ValidationError<'a>;
186+
187+
#[inline]
188+
fn next(&mut self) -> Option<Self::Item> {
189+
self.iter.as_mut().next()
190+
}
191+
192+
#[inline]
193+
fn size_hint(&self) -> (usize, Option<usize>) {
194+
self.iter.size_hint()
195+
}
196+
}
197+
198+
impl<'a> IntoIterator for ValidationErrors<'a> {
199+
type Item = ValidationError<'a>;
200+
type IntoIter = vec::IntoIter<ValidationError<'a>>;
201+
202+
fn into_iter(self) -> Self::IntoIter {
203+
self.errors.into_iter()
204+
}
205+
}
206+
207+
impl<'a, 'b> IntoIterator for &'b ValidationErrors<'a> {
208+
type Item = &'b ValidationError<'a>;
209+
type IntoIter = slice::Iter<'b, ValidationError<'a>>;
210+
211+
fn into_iter(self) -> Self::IntoIter {
212+
self.errors.iter()
213+
}
214+
}
215+
216+
impl<'a, 'b> IntoIterator for &'b mut ValidationErrors<'a> {
217+
type Item = &'b mut ValidationError<'a>;
218+
type IntoIter = slice::IterMut<'b, ValidationError<'a>>;
219+
220+
fn into_iter(self) -> Self::IntoIter {
221+
self.errors.iter_mut()
222+
}
223+
}
92224

93225
// Empty iterator means no error happened
94226
pub(crate) fn no_error<'a>() -> ErrorIterator<'a> {
95-
Box::new(empty())
227+
ErrorIterator::from_iterator(empty())
96228
}
97229
// A wrapper for one error
98230
pub(crate) fn error(instance: ValidationError) -> ErrorIterator {
99-
Box::new(once(instance))
231+
ErrorIterator::from_iterator(once(instance))
100232
}
101233

102234
/// Kinds of errors that may happen during validation
@@ -1395,6 +1527,166 @@ mod tests {
13951527

13961528
use test_case::test_case;
13971529

1530+
fn owned_error(instance: Value, kind: ValidationErrorKind) -> ValidationError<'static> {
1531+
ValidationError::new(Cow::Owned(instance), kind, Location::new(), Location::new())
1532+
}
1533+
1534+
#[test]
1535+
fn error_iterator_into_errors_collects_all_errors() {
1536+
let iterator = ErrorIterator::from_iterator(
1537+
vec![
1538+
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
1539+
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
1540+
]
1541+
.into_iter(),
1542+
);
1543+
let validation_errors = iterator.into_errors();
1544+
let collected: Vec<_> = validation_errors.into_iter().collect();
1545+
assert_eq!(collected.len(), 2);
1546+
assert_eq!(collected[0].to_string(), "1 is less than the minimum of 2");
1547+
assert_eq!(
1548+
collected[1].to_string(),
1549+
"3 is greater than the maximum of 2"
1550+
);
1551+
}
1552+
1553+
#[test]
1554+
fn validation_errors_display_reports_success() {
1555+
let errors = ValidationErrors { errors: Vec::new() };
1556+
assert_eq!(format!("{errors}"), "Validation succeeded");
1557+
}
1558+
1559+
#[test]
1560+
fn validation_errors_display_lists_messages() {
1561+
let errors = ValidationErrors {
1562+
errors: vec![
1563+
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
1564+
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
1565+
],
1566+
};
1567+
let rendered = format!("{errors}");
1568+
assert!(rendered.contains("Validation errors:"));
1569+
assert!(rendered.contains("01: 1 is less than the minimum of 2"));
1570+
assert!(rendered.contains("02: 3 is greater than the maximum of 2"));
1571+
}
1572+
1573+
#[test]
1574+
fn validation_errors_len_and_is_empty() {
1575+
let empty = ValidationErrors { errors: vec![] };
1576+
assert_eq!(empty.len(), 0);
1577+
assert!(empty.is_empty());
1578+
1579+
let errors = ValidationErrors {
1580+
errors: vec![owned_error(
1581+
json!(1),
1582+
ValidationErrorKind::Minimum { limit: json!(2) },
1583+
)],
1584+
};
1585+
assert_eq!(errors.len(), 1);
1586+
assert!(!errors.is_empty());
1587+
}
1588+
1589+
#[test]
1590+
fn validation_errors_as_slice() {
1591+
let errors = ValidationErrors {
1592+
errors: vec![
1593+
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
1594+
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
1595+
],
1596+
};
1597+
1598+
let slice = errors.as_slice();
1599+
assert_eq!(slice.len(), 2);
1600+
assert_eq!(slice[0].to_string(), "1 is less than the minimum of 2");
1601+
assert_eq!(slice[1].to_string(), "3 is greater than the maximum of 2");
1602+
}
1603+
1604+
#[test]
1605+
fn validation_errors_iter() {
1606+
let errors = ValidationErrors {
1607+
errors: vec![
1608+
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
1609+
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
1610+
],
1611+
};
1612+
1613+
let collected: Vec<_> = errors.iter().map(ValidationError::to_string).collect();
1614+
assert_eq!(collected.len(), 2);
1615+
assert_eq!(collected[0], "1 is less than the minimum of 2");
1616+
assert_eq!(collected[1], "3 is greater than the maximum of 2");
1617+
}
1618+
1619+
#[test]
1620+
#[allow(clippy::explicit_iter_loop)]
1621+
fn validation_errors_iter_mut() {
1622+
let mut errors = ValidationErrors {
1623+
errors: vec![owned_error(
1624+
json!(1),
1625+
ValidationErrorKind::Minimum { limit: json!(2) },
1626+
)],
1627+
};
1628+
1629+
// Verify we can get mutable references via iter_mut()
1630+
for error in errors.iter_mut() {
1631+
let _ = error.to_string();
1632+
}
1633+
}
1634+
1635+
#[test]
1636+
fn validation_errors_into_iterator_by_ref() {
1637+
let errors = ValidationErrors {
1638+
errors: vec![owned_error(
1639+
json!(1),
1640+
ValidationErrorKind::Minimum { limit: json!(2) },
1641+
)],
1642+
};
1643+
1644+
let collected: Vec<_> = (&errors).into_iter().collect();
1645+
assert_eq!(collected.len(), 1);
1646+
// Verify errors is still usable
1647+
assert_eq!(errors.len(), 1);
1648+
}
1649+
1650+
#[test]
1651+
fn validation_errors_into_iterator_by_mut_ref() {
1652+
let mut errors = ValidationErrors {
1653+
errors: vec![owned_error(
1654+
json!(1),
1655+
ValidationErrorKind::Minimum { limit: json!(2) },
1656+
)],
1657+
};
1658+
1659+
let collected: Vec<_> = (&mut errors).into_iter().collect();
1660+
assert_eq!(collected.len(), 1);
1661+
// Verify errors is still usable
1662+
assert_eq!(errors.len(), 1);
1663+
}
1664+
1665+
#[test]
1666+
fn error_iterator_size_hint() {
1667+
let vec = vec![
1668+
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
1669+
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
1670+
];
1671+
let iterator = ErrorIterator::from_iterator(vec.into_iter());
1672+
let (lower, upper) = iterator.size_hint();
1673+
assert_eq!(lower, 2);
1674+
assert_eq!(upper, Some(2));
1675+
}
1676+
1677+
#[test]
1678+
fn validation_errors_debug() {
1679+
let errors = ValidationErrors {
1680+
errors: vec![owned_error(
1681+
json!(1),
1682+
ValidationErrorKind::Minimum { limit: json!(2) },
1683+
)],
1684+
};
1685+
let debug_output = format!("{errors:?}");
1686+
assert!(debug_output.contains("ValidationErrors"));
1687+
assert!(debug_output.contains("errors"));
1688+
}
1689+
13981690
#[test]
13991691
fn single_type_error() {
14001692
let instance = json!(42);

crates/jsonschema/src/keywords/additional_items.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl Validate for AdditionalItemsObjectValidator {
3737
.skip(self.items_count)
3838
.flat_map(|(idx, item)| self.node.iter_errors(item, &location.push(idx)))
3939
.collect();
40-
Box::new(errors.into_iter())
40+
ErrorIterator::from_iterator(errors.into_iter())
4141
} else {
4242
no_error()
4343
}

0 commit comments

Comments
 (0)