Skip to main content

mdx_gen/
validation.rs

1//! Validation primitives used by [`MarkdownOptions::validate`].
2//!
3//! Inlined from the formerly-vendored `euxis-commons` crate so
4//! `mdx-gen` has no path-only workspace dependencies at publish time.
5//! The surface is trimmed to what the crate actually uses:
6//! [`ValidationError`](crate::validation::ValidationError),
7//! [`Validator`](crate::validation::Validator), and a
8//! [`ValidationResult`](crate::validation::ValidationResult) alias.
9//!
10//! [`MarkdownOptions::validate`]: crate::MarkdownOptions::validate
11
12/// Validation error types produced by [`Validator`] checks.
13#[derive(Debug, Clone, PartialEq, Eq)]
14#[allow(missing_docs)]
15pub enum ValidationError {
16    /// Value is empty when it shouldn't be.
17    Empty,
18    /// Value is too short.
19    TooShort { min: usize, actual: usize },
20    /// Value is too long.
21    TooLong { max: usize, actual: usize },
22    /// Value is below minimum.
23    BelowMin { min: String, actual: String },
24    /// Value is above maximum.
25    AboveMax { max: String, actual: String },
26    /// Value doesn't match expected pattern.
27    InvalidPattern { pattern: String },
28    /// Value is not in allowed set.
29    NotInSet { allowed: Vec<String> },
30    /// Custom validation error.
31    Custom(String),
32}
33
34impl std::fmt::Display for ValidationError {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::Empty => write!(f, "Value cannot be empty"),
38            Self::TooShort { min, actual } => {
39                write!(
40                    f,
41                    "Value too short: minimum {min}, got {actual}"
42                )
43            }
44            Self::TooLong { max, actual } => {
45                write!(f, "Value too long: maximum {max}, got {actual}")
46            }
47            Self::BelowMin { min, actual } => {
48                write!(
49                    f,
50                    "Value below minimum: min {min}, got {actual}"
51                )
52            }
53            Self::AboveMax { max, actual } => {
54                write!(
55                    f,
56                    "Value above maximum: max {max}, got {actual}"
57                )
58            }
59            Self::InvalidPattern { pattern } => {
60                write!(f, "Value doesn't match pattern: {pattern}")
61            }
62            Self::NotInSet { allowed } => {
63                write!(f, "Value not in allowed set: {allowed:?}")
64            }
65            Self::Custom(msg) => write!(f, "{msg}"),
66        }
67    }
68}
69
70impl std::error::Error for ValidationError {}
71
72/// Result type for validation operations.
73pub type ValidationResult<T> = Result<T, ValidationError>;
74
75/// Builder for composing multiple validations into a single pass.
76///
77/// Each [`check`](Self::check) captures a `(field, ValidationError)`
78/// pair on failure without short-circuiting, so callers receive every
79/// problem at once.
80#[derive(Debug, Default)]
81pub struct Validator {
82    errors: Vec<(String, ValidationError)>,
83}
84
85impl Validator {
86    /// Create a new validator.
87    #[must_use]
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Run a validation closure and record any error under `field`.
93    pub fn check<F>(&mut self, field: &str, validation: F) -> &mut Self
94    where
95        F: FnOnce() -> Result<(), ValidationError>,
96    {
97        if let Err(e) = validation() {
98            self.errors.push((field.to_string(), e));
99        }
100        self
101    }
102
103    /// Returns `true` if no check has failed so far.
104    #[must_use]
105    pub const fn is_valid(&self) -> bool {
106        self.errors.is_empty()
107    }
108
109    /// Returns a slice of all accumulated errors.
110    #[must_use]
111    pub fn errors(&self) -> &[(String, ValidationError)] {
112        &self.errors
113    }
114
115    /// Consume the validator and return `Ok(())` on success, or the
116    /// full list of `(field, error)` pairs otherwise.
117    ///
118    /// # Errors
119    ///
120    /// Returns the list of validation errors if any checks failed.
121    pub fn finish(self) -> Result<(), Vec<(String, ValidationError)>> {
122        if self.errors.is_empty() {
123            Ok(())
124        } else {
125            Err(self.errors)
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn validation_error_display_covers_every_variant() {
136        assert_eq!(
137            ValidationError::Empty.to_string(),
138            "Value cannot be empty"
139        );
140        assert_eq!(
141            ValidationError::TooShort { min: 3, actual: 1 }.to_string(),
142            "Value too short: minimum 3, got 1"
143        );
144        assert_eq!(
145            ValidationError::TooLong { max: 5, actual: 9 }.to_string(),
146            "Value too long: maximum 5, got 9"
147        );
148        assert_eq!(
149            ValidationError::BelowMin {
150                min: "1".into(),
151                actual: "0".into()
152            }
153            .to_string(),
154            "Value below minimum: min 1, got 0"
155        );
156        assert_eq!(
157            ValidationError::AboveMax {
158                max: "10".into(),
159                actual: "11".into()
160            }
161            .to_string(),
162            "Value above maximum: max 10, got 11"
163        );
164        assert_eq!(
165            ValidationError::InvalidPattern {
166                pattern: "email".into()
167            }
168            .to_string(),
169            "Value doesn't match pattern: email"
170        );
171        assert!(ValidationError::NotInSet {
172            allowed: vec!["a".into(), "b".into()]
173        }
174        .to_string()
175        .starts_with("Value not in allowed set:"));
176        assert_eq!(
177            ValidationError::Custom("x".into()).to_string(),
178            "x"
179        );
180    }
181
182    #[test]
183    fn validation_error_implements_std_error() {
184        let err = ValidationError::Empty;
185        let _: &dyn std::error::Error = &err;
186    }
187
188    #[test]
189    fn validator_accumulates_every_failure() {
190        let mut v = Validator::new();
191        v.check("name", || Err(ValidationError::Empty));
192        v.check("pattern", || {
193            Err(ValidationError::InvalidPattern {
194                pattern: "email".into(),
195            })
196        });
197        assert!(!v.is_valid());
198        assert_eq!(v.errors().len(), 2);
199        let errs = v.finish().unwrap_err();
200        assert_eq!(errs.len(), 2);
201        assert_eq!(errs[0].0, "name");
202        assert_eq!(errs[1].0, "pattern");
203    }
204
205    #[test]
206    fn validator_finish_ok_when_no_checks_failed() {
207        let v = Validator::new();
208        assert!(v.finish().is_ok());
209    }
210
211    #[test]
212    fn validator_check_skips_recording_on_ok() {
213        let mut v = Validator::new();
214        v.check("field", || Ok(()));
215        assert!(v.is_valid());
216        assert!(v.errors().is_empty());
217    }
218}