Skip to main content

doido_model/
validation.rs

1//! Active Record-style validations.
2//!
3//! Rust models have no shared base class, so a model opts in by implementing
4//! [`Validate`] and building an [`Errors`] with the provided validators
5//! (`presence`, `length`, …) or ad-hoc [`Errors::add`] calls.
6
7/// A collection of `(field, message)` validation errors (Rails `model.errors`).
8#[derive(Debug, Default, Clone)]
9pub struct Errors {
10    items: Vec<(String, String)>,
11}
12
13impl Errors {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    /// Add a raw error message for `field`.
19    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) -> &mut Self {
20        self.items.push((field.into(), message.into()));
21        self
22    }
23
24    /// Whether there are no errors (the record is valid).
25    pub fn is_empty(&self) -> bool {
26        self.items.is_empty()
27    }
28
29    /// The number of errors.
30    pub fn len(&self) -> usize {
31        self.items.len()
32    }
33
34    /// The messages recorded for a specific field.
35    pub fn on(&self, field: &str) -> Vec<&str> {
36        self.items
37            .iter()
38            .filter(|(f, _)| f == field)
39            .map(|(_, m)| m.as_str())
40            .collect()
41    }
42
43    /// Human-readable `"field message"` strings for every error.
44    pub fn full_messages(&self) -> Vec<String> {
45        self.items
46            .iter()
47            .map(|(field, message)| format!("{field} {message}"))
48            .collect()
49    }
50
51    /// Require a non-blank string (Rails `validates :field, presence: true`).
52    pub fn presence(&mut self, field: &str, value: &str) -> &mut Self {
53        if value.trim().is_empty() {
54            self.add(field, "can't be blank");
55        }
56        self
57    }
58
59    /// Constrain a string's character length (Rails `length: { minimum:, maximum: }`).
60    pub fn length(
61        &mut self,
62        field: &str,
63        value: &str,
64        min: Option<usize>,
65        max: Option<usize>,
66    ) -> &mut Self {
67        let len = value.chars().count();
68        if let Some(min) = min {
69            if len < min {
70                self.add(field, format!("is too short (minimum is {min} characters)"));
71            }
72        }
73        if let Some(max) = max {
74            if len > max {
75                self.add(field, format!("is too long (maximum is {max} characters)"));
76            }
77        }
78        self
79    }
80}
81
82/// Implemented by models that can validate themselves.
83pub trait Validate {
84    /// Collect validation errors for the current state.
85    fn validate(&self) -> Errors;
86
87    /// Whether the record currently passes validation.
88    fn is_valid(&self) -> bool {
89        self.validate().is_empty()
90    }
91}