Skip to main content

metrique_writer_core/
validate.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt;
5
6/// An error type that describes why an [`crate::Entry`] isn't valid. This can be because it violated general contracts
7/// (e.g. writing multiple values with the same name) or because it violated a format-specific contract (e.g. using a
8/// reserved property name).
9///
10/// Unlike the happy-case path, errors are free to allocate. We won't bend over backwards to ensure fast performance in
11/// reporting why entries are invalid!
12#[derive(Clone)]
13pub struct ValidationError(Vec<String>);
14
15impl ValidationError {
16    /// Create a build that can be used to compose multiple validation failures into a single [`ValidationError`]. Note
17    /// that if no validation failures are added to the builder, [`ValidationErrorBuilder::build()`] will return
18    /// [`Ok`], which is useful to track if a side-effect produced any errors.
19    pub fn builder() -> ValidationErrorBuilder {
20        ValidationErrorBuilder::default()
21    }
22
23    /// Extend this error with all of the validation failures recorded in `other`.
24    pub fn extend(&mut self, other: Self) {
25        self.0.extend(other.0);
26    }
27
28    /// Add the field `name` context for all of the validation failures reported in `self`.
29    pub fn for_field(mut self, name: &str) -> Self {
30        for err in self.0.iter_mut() {
31            *err = format!("for `{name}`: {err}");
32        }
33        self
34    }
35
36    /// Record a generic validation failure with a reason string.
37    pub fn invalid(reason: impl Into<String>) -> Self {
38        Self(vec![reason.into()])
39    }
40}
41
42impl fmt::Debug for ValidationError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.debug_list().entries(&self.0).finish()
45    }
46}
47
48impl fmt::Display for ValidationError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str(&self.0.join(", "))
51    }
52}
53
54impl std::error::Error for ValidationError {}
55
56/// Builder to record validation failures over time and bundle them into a single [`ValidationError`] Note that if no
57/// validation failures are added to the builder, [`ValidationErrorBuilder::build()`] will return [`None`], which is
58/// useful to track if a side-effect produced any errors.
59#[derive(Debug, Clone, Default)]
60pub struct ValidationErrorBuilder(Vec<String>);
61
62impl ValidationErrorBuilder {
63    /// Returns [`Ok`] if no validation failures were recorded, otherwise [`Err`] [`ValidationError`] containing all of
64    /// the recorded validation falures.
65    pub fn build(self) -> Result<(), ValidationError> {
66        if self.0.is_empty() {
67            Ok(())
68        } else {
69            Err(ValidationError(self.0))
70        }
71    }
72
73    // We use a $method(), $method_mut() pattern to allow for both chained builder use and for recording on a builder
74    // field of a struct.
75
76    /// Record a generic validation failure with a reason string.
77    pub fn invalid(mut self, reason: impl Into<String>) -> Self {
78        self.invalid_mut(reason);
79        self
80    }
81
82    /// Record a generic validation failure with a reason string, but only require `&mut Self`.
83    pub fn invalid_mut(&mut self, reason: impl Into<String>) -> &mut Self {
84        self.0.push(reason.into());
85        self
86    }
87
88    /// Extend this error with all of the validation failures recorded in `error`.
89    pub fn extend(mut self, error: ValidationError) -> Self {
90        self.extend_mut(error);
91        self
92    }
93
94    /// Extend this error with all of the validation failures recorded in `error`, but only require `&mut Self`.
95    pub fn extend_mut(&mut self, error: ValidationError) -> &mut Self {
96        self.0.extend(error.0);
97        self
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use crate::validate::ValidationError;
104
105    #[test]
106    fn record_invalid() {
107        assert_contains(
108            &ValidationError::invalid("custom message"),
109            "custom message",
110        );
111        assert_contains(
112            &ValidationError::builder()
113                .invalid("custom message")
114                .build()
115                .unwrap_err(),
116            "custom message",
117        );
118
119        let multiple = ValidationError::builder()
120            .invalid("first")
121            .invalid("second")
122            .build()
123            .unwrap_err();
124        assert_contains(&multiple, "first");
125        assert_contains(&multiple, "second");
126    }
127
128    #[test]
129    fn extendable() {
130        let mut extended = ValidationError::invalid("first");
131        extended.extend(ValidationError::invalid("second"));
132        assert_contains(&extended, "first");
133        assert_contains(&extended, "second");
134
135        let extended = ValidationError::builder()
136            .invalid("first")
137            .extend(ValidationError::invalid("second"))
138            .build()
139            .unwrap_err();
140        assert_contains(&extended, "first");
141        assert_contains(&extended, "second");
142    }
143
144    #[test]
145    fn add_field_context() {
146        let contextualized = ValidationError::invalid("custom message").for_field("my_field");
147        assert_contains(&contextualized, "custom message");
148        assert_contains(&contextualized, "my_field");
149    }
150
151    #[test]
152    fn build_returns_ok_if_no_errors() {
153        assert!(ValidationError::builder().build().is_ok());
154    }
155
156    fn assert_contains(error: &ValidationError, s: &str) {
157        assert!(format!("{}", error).contains(s));
158        assert!(format!("{:?}", error).contains(s));
159    }
160}