Skip to main content

rust_zero_core/
validation.rs

1use serde::Serialize;
2use std::{fmt, ops::RangeInclusive};
3
4/// A single machine-readable configuration or request validation failure.
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub struct Violation {
7    pub field: String,
8    pub code: &'static str,
9    pub message: String,
10}
11
12/// Accumulates all validation failures instead of stopping at the first one.
13#[derive(Debug, Default)]
14pub struct Validation {
15    violations: Vec<Violation>,
16}
17
18impl Validation {
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    pub fn check(
24        &mut self,
25        field: impl Into<String>,
26        condition: bool,
27        code: &'static str,
28        message: impl Into<String>,
29    ) -> &mut Self {
30        if !condition {
31            self.violations.push(Violation {
32                field: field.into(),
33                code,
34                message: message.into(),
35            });
36        }
37        self
38    }
39
40    pub fn required(&mut self, field: impl Into<String>, value: &str) -> &mut Self {
41        self.check(
42            field,
43            !value.trim().is_empty(),
44            "required",
45            "must not be empty",
46        )
47    }
48
49    pub fn length(
50        &mut self,
51        field: impl Into<String>,
52        value: &str,
53        range: RangeInclusive<usize>,
54    ) -> &mut Self {
55        let length = value.chars().count();
56        let message = format!(
57            "length must be between {} and {}",
58            range.start(),
59            range.end()
60        );
61        self.check(field, range.contains(&length), "length", message)
62    }
63
64    pub fn range<T>(
65        &mut self,
66        field: impl Into<String>,
67        value: T,
68        range: RangeInclusive<T>,
69    ) -> &mut Self
70    where
71        T: PartialOrd + fmt::Display,
72    {
73        let message = format!("must be between {} and {}", range.start(), range.end());
74        self.check(field, range.contains(&value), "range", message)
75    }
76
77    pub fn one_of<T>(&mut self, field: impl Into<String>, value: &T, allowed: &[T]) -> &mut Self
78    where
79        T: PartialEq + fmt::Display,
80    {
81        self.check(
82            field,
83            allowed.contains(value),
84            "one_of",
85            format!(
86                "must be one of [{}]",
87                allowed
88                    .iter()
89                    .map(ToString::to_string)
90                    .collect::<Vec<_>>()
91                    .join(", ")
92            ),
93        )
94    }
95
96    pub fn finish(self) -> Result<(), ValidationErrors> {
97        if self.violations.is_empty() {
98            Ok(())
99        } else {
100            Err(ValidationErrors(self.violations))
101        }
102    }
103}
104
105/// Implemented by typed requests and configuration values that validate themselves.
106pub trait Validate {
107    fn validate(&self) -> Result<(), ValidationErrors>;
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ValidationErrors(Vec<Violation>);
112
113impl ValidationErrors {
114    pub fn violations(&self) -> &[Violation] {
115        &self.0
116    }
117
118    pub fn into_violations(self) -> Vec<Violation> {
119        self.0
120    }
121}
122
123impl fmt::Display for ValidationErrors {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        for (index, violation) in self.0.iter().enumerate() {
126            if index > 0 {
127                formatter.write_str("; ")?;
128            }
129            write!(
130                formatter,
131                "{}: {} ({})",
132                violation.field, violation.message, violation.code
133            )?;
134        }
135        Ok(())
136    }
137}
138
139impl std::error::Error for ValidationErrors {}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn collects_multiple_field_failures() {
147        let mut validation = Validation::new();
148        validation
149            .required("name", " ")
150            .range("age", 12, 18..=120)
151            .one_of("mode", &"broken", &["dev", "prod"]);
152
153        let errors = validation.finish().unwrap_err();
154        assert_eq!(errors.violations().len(), 3);
155        assert_eq!(errors.violations()[0].field, "name");
156        assert_eq!(errors.violations()[1].code, "range");
157        assert_eq!(errors.violations()[2].code, "one_of");
158    }
159
160    #[test]
161    fn measures_string_length_in_characters() {
162        let mut validation = Validation::new();
163        validation.length("name", "你好", 2..=2);
164        assert!(validation.finish().is_ok());
165    }
166}