rustorm-core 0.1.0

Core traits, types and utilities for RustORM
Documentation
use crate::error::{OrmError, OrmResult};
use std::collections::HashMap;

// ---------------------------------------------------------------------------
// ValidationErrors
// ---------------------------------------------------------------------------

/// Коллекция ошибок валидации, сгруппированных по имени поля.
#[derive(Debug, Default, Clone)]
pub struct ValidationErrors {
    errors: HashMap<String, Vec<String>>,
}

impl ValidationErrors {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
        self.errors
            .entry(field.into())
            .or_default()
            .push(message.into());
    }

    pub fn is_empty(&self) -> bool {
        self.errors.is_empty()
    }

    pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
        self.errors.iter()
    }

    pub fn merge(&mut self, other: ValidationErrors) {
        for (field, msgs) in other.errors {
            self.errors.entry(field).or_default().extend(msgs);
        }
    }

    pub fn into_orm_error(self) -> OrmError {
        let msg = self
            .errors
            .iter()
            .map(|(f, msgs)| format!("{}: {}", f, msgs.join(", ")))
            .collect::<Vec<_>>()
            .join("; ");
        OrmError::Validation(msg)
    }
}

// ---------------------------------------------------------------------------
// ValidationContext
// ---------------------------------------------------------------------------

/// Контекст, передаваемый в пользовательские методы валидации.
pub struct ValidationContext<'a> {
    errors: &'a mut ValidationErrors,
}

impl<'a> ValidationContext<'a> {
    pub fn new(errors: &'a mut ValidationErrors) -> Self {
        Self { errors }
    }

    /// Добавить ошибку валидации.
    pub fn add_error(&mut self, field: impl Into<String>, message: impl Into<String>) {
        self.errors.add(field, message);
    }

    /// Есть ли ошибки.
    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }
}

// ---------------------------------------------------------------------------
// Validate trait
// ---------------------------------------------------------------------------

/// Трейт синхронной валидации.
pub trait Validate {
    /// Валидация атрибутами (`#[validate(...)]`), генерируется макросом.
    fn validate_attrs(&self) -> ValidationErrors;

    /// Кастомная валидация — реализуется пользователем.
    fn validate_custom(&self, _ctx: &mut ValidationContext) {}

    /// Запускает всю синхронную валидацию.
    fn validate(&self) -> OrmResult<()> {
        let mut errors = self.validate_attrs();
        {
            let mut ctx = ValidationContext::new(&mut errors);
            self.validate_custom(&mut ctx);
        }
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors.into_orm_error())
        }
    }
}

// ---------------------------------------------------------------------------
// Built-in validation helpers
// ---------------------------------------------------------------------------

static EMAIL_RE: once_cell::sync::Lazy<regex::Regex> =
    once_cell::sync::Lazy::new(|| regex::Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap());

static URL_RE: once_cell::sync::Lazy<regex::Regex> =
    once_cell::sync::Lazy::new(|| regex::Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap());

pub fn validate_email(value: &str) -> bool {
    EMAIL_RE.is_match(value)
}

pub fn validate_url(value: &str) -> bool {
    URL_RE.is_match(value)
}

pub fn validate_min_length(value: &str, min: usize) -> bool {
    value.len() >= min
}

pub fn validate_max_length(value: &str, max: usize) -> bool {
    value.len() <= max
}

pub fn validate_regex(value: &str, pattern: &str) -> bool {
    regex::Regex::new(pattern)
        .map(|re| re.is_match(value))
        .unwrap_or(false)
}