Skip to main content

rustorm_core/
validate.rs

1use crate::error::{OrmError, OrmResult};
2use std::collections::HashMap;
3
4// ---------------------------------------------------------------------------
5// ValidationErrors
6// ---------------------------------------------------------------------------
7
8/// Коллекция ошибок валидации, сгруппированных по имени поля.
9#[derive(Debug, Default, Clone)]
10pub struct ValidationErrors {
11    errors: HashMap<String, Vec<String>>,
12}
13
14impl ValidationErrors {
15    pub fn new() -> Self {
16        Self::default()
17    }
18
19    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
20        self.errors
21            .entry(field.into())
22            .or_default()
23            .push(message.into());
24    }
25
26    pub fn is_empty(&self) -> bool {
27        self.errors.is_empty()
28    }
29
30    pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
31        self.errors.iter()
32    }
33
34    pub fn merge(&mut self, other: ValidationErrors) {
35        for (field, msgs) in other.errors {
36            self.errors.entry(field).or_default().extend(msgs);
37        }
38    }
39
40    pub fn into_orm_error(self) -> OrmError {
41        let msg = self
42            .errors
43            .iter()
44            .map(|(f, msgs)| format!("{}: {}", f, msgs.join(", ")))
45            .collect::<Vec<_>>()
46            .join("; ");
47        OrmError::Validation(msg)
48    }
49}
50
51// ---------------------------------------------------------------------------
52// ValidationContext
53// ---------------------------------------------------------------------------
54
55/// Контекст, передаваемый в пользовательские методы валидации.
56pub struct ValidationContext<'a> {
57    errors: &'a mut ValidationErrors,
58}
59
60impl<'a> ValidationContext<'a> {
61    pub fn new(errors: &'a mut ValidationErrors) -> Self {
62        Self { errors }
63    }
64
65    /// Добавить ошибку валидации.
66    pub fn add_error(&mut self, field: impl Into<String>, message: impl Into<String>) {
67        self.errors.add(field, message);
68    }
69
70    /// Есть ли ошибки.
71    pub fn has_errors(&self) -> bool {
72        !self.errors.is_empty()
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Validate trait
78// ---------------------------------------------------------------------------
79
80/// Трейт синхронной валидации.
81pub trait Validate {
82    /// Валидация атрибутами (`#[validate(...)]`), генерируется макросом.
83    fn validate_attrs(&self) -> ValidationErrors;
84
85    /// Кастомная валидация — реализуется пользователем.
86    fn validate_custom(&self, _ctx: &mut ValidationContext) {}
87
88    /// Запускает всю синхронную валидацию.
89    fn validate(&self) -> OrmResult<()> {
90        let mut errors = self.validate_attrs();
91        {
92            let mut ctx = ValidationContext::new(&mut errors);
93            self.validate_custom(&mut ctx);
94        }
95        if errors.is_empty() {
96            Ok(())
97        } else {
98            Err(errors.into_orm_error())
99        }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Built-in validation helpers
105// ---------------------------------------------------------------------------
106
107static EMAIL_RE: once_cell::sync::Lazy<regex::Regex> =
108    once_cell::sync::Lazy::new(|| regex::Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap());
109
110static URL_RE: once_cell::sync::Lazy<regex::Regex> =
111    once_cell::sync::Lazy::new(|| regex::Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap());
112
113pub fn validate_email(value: &str) -> bool {
114    EMAIL_RE.is_match(value)
115}
116
117pub fn validate_url(value: &str) -> bool {
118    URL_RE.is_match(value)
119}
120
121pub fn validate_min_length(value: &str, min: usize) -> bool {
122    value.len() >= min
123}
124
125pub fn validate_max_length(value: &str, max: usize) -> bool {
126    value.len() <= max
127}
128
129pub fn validate_regex(value: &str, pattern: &str) -> bool {
130    regex::Regex::new(pattern)
131        .map(|re| re.is_match(value))
132        .unwrap_or(false)
133}