tui-canvas-validation-core 0.8.2

Validation core for the tui-canvas
Documentation
use crate::rules::{
    CharacterFilter, CharacterLimits, DisplayMask, PatternFilters, PositionFilter, PositionRange,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllowedValues {
    pub values: Vec<String>,
    pub allow_empty: bool,
    pub case_insensitive: bool,
}

impl AllowedValues {
    pub fn new(values: Vec<String>) -> Self {
        Self {
            values,
            allow_empty: true,
            case_insensitive: false,
        }
    }

    pub fn allow_empty(mut self, allow_empty: bool) -> Self {
        self.allow_empty = allow_empty;
        self
    }

    pub fn case_insensitive(mut self, case_insensitive: bool) -> Self {
        self.case_insensitive = case_insensitive;
        self
    }

    pub fn matches(&self, text: &str) -> bool {
        if self.case_insensitive {
            self.values
                .iter()
                .any(|allowed| allowed.eq_ignore_ascii_case(text))
        } else {
            self.values.iter().any(|allowed| allowed == text)
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CharacterFilterSettings {
    Alphabetic,
    Numeric,
    Alphanumeric,
    Exact(char),
    OneOf(Vec<char>),
    Regex(String),
}

impl CharacterFilterSettings {
    pub fn resolve(&self) -> CharacterFilter {
        match self {
            Self::Alphabetic => CharacterFilter::Alphabetic,
            Self::Numeric => CharacterFilter::Numeric,
            Self::Alphanumeric => CharacterFilter::Alphanumeric,
            Self::Exact(ch) => CharacterFilter::Exact(*ch),
            Self::OneOf(chars) => CharacterFilter::OneOf(chars.clone()),
            Self::Regex(pattern) => {
                #[cfg(feature = "regex")]
                {
                    match regex::Regex::new(pattern) {
                        Ok(regex) => CharacterFilter::Custom(Arc::new(move |ch| {
                            regex.is_match(&ch.to_string())
                        })),
                        Err(_) => CharacterFilter::Custom(Arc::new(|_| false)),
                    }
                }
                #[cfg(not(feature = "regex"))]
                {
                    let _ = pattern;
                    CharacterFilter::Custom(Arc::new(|_| false))
                }
            }
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionFilterSettings {
    pub positions: PositionRange,
    pub filter: CharacterFilterSettings,
}

impl PositionFilterSettings {
    pub fn resolve(&self) -> PositionFilter {
        PositionFilter::new(self.positions.clone(), self.filter.resolve())
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PatternSettings {
    pub filters: Vec<PositionFilterSettings>,
    pub description: Option<String>,
}

impl PatternSettings {
    pub fn resolve(&self) -> PatternFilters {
        PatternFilters::new().add_filters(
            self.filters
                .iter()
                .map(PositionFilterSettings::resolve)
                .collect(),
        )
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidationSettings {
    pub required: bool,
    pub character_limits: Option<CharacterLimits>,
    pub pattern: Option<PatternSettings>,
    pub allowed_values: Option<AllowedValues>,
    pub display_mask: Option<DisplayMask>,
    pub external_validation_enabled: bool,
}

impl ValidationSettings {
    pub fn resolve(&self) -> ValidationConfig {
        ValidationConfig {
            required: self.required,
            character_limits: self.character_limits.clone(),
            pattern_filters: self.pattern.as_ref().map(PatternSettings::resolve),
            allowed_values: self.allowed_values.clone(),
            display_mask: self.display_mask.clone(),
            external_validation_enabled: self.external_validation_enabled,
        }
    }

    pub fn merge_rules<'a>(
        rules: impl IntoIterator<Item = &'a ValidationSettings>,
    ) -> Result<Self, ValidationMergeError> {
        let mut merged = ValidationSettings::default();

        for rule in rules {
            merged.merge_rule(rule)?;
        }

        Ok(merged)
    }

    pub fn merge_rule(&mut self, rule: &ValidationSettings) -> Result<(), ValidationMergeError> {
        self.required |= rule.required;
        self.external_validation_enabled |= rule.external_validation_enabled;

        merge_singleton(
            "character_limits",
            &mut self.character_limits,
            &rule.character_limits,
        )?;
        merge_singleton(
            "allowed_values",
            &mut self.allowed_values,
            &rule.allowed_values,
        )?;
        merge_singleton("display_mask", &mut self.display_mask, &rule.display_mask)?;

        if let Some(pattern) = &rule.pattern {
            match &mut self.pattern {
                Some(existing) => {
                    existing.filters.extend(pattern.filters.clone());
                    if existing.description.is_none() {
                        existing.description = pattern.description.clone();
                    }
                }
                None => self.pattern = Some(pattern.clone()),
            }
        }

        Ok(())
    }
}

fn merge_singleton<T: Clone>(
    field_name: &'static str,
    target: &mut Option<T>,
    source: &Option<T>,
) -> Result<(), ValidationMergeError> {
    if let Some(source) = source {
        if target.is_some() {
            return Err(ValidationMergeError::DuplicateSingleton { field_name });
        }

        *target = Some(source.clone());
    }

    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ValidationMergeError {
    #[error("validation set contains more than one rule configuring {field_name}")]
    DuplicateSingleton { field_name: &'static str },
}

#[derive(Debug, Clone, Default)]
pub struct ValidationConfig {
    pub required: bool,
    pub character_limits: Option<CharacterLimits>,
    pub pattern_filters: Option<PatternFilters>,
    pub allowed_values: Option<AllowedValues>,
    pub display_mask: Option<DisplayMask>,
    pub external_validation_enabled: bool,
}

impl ValidationConfig {
    pub fn validate_content(&self, text: &str) -> ValidationResult {
        if text.is_empty() {
            if self.required {
                return ValidationResult::error("Value required");
            }

            if let Some(allowed_values) = &self.allowed_values {
                if !allowed_values.allow_empty {
                    return ValidationResult::error("Empty value is not allowed");
                }
            }

            return ValidationResult::Valid;
        }

        if let Some(limits) = &self.character_limits {
            if let Some(result) = limits.validate_content(text) {
                if !result.is_acceptable() {
                    return result;
                }
            }
        }

        if let Some(pattern_filters) = &self.pattern_filters {
            if let Err(message) = pattern_filters.validate_text(text) {
                return ValidationResult::error(message);
            }
        }

        if let Some(allowed_values) = &self.allowed_values {
            if !allowed_values.matches(text) {
                return ValidationResult::error("Value must be one of the allowed options");
            }
        }

        ValidationResult::Valid
    }

    pub fn has_validation(&self) -> bool {
        self.required
            || self.character_limits.is_some()
            || self.pattern_filters.is_some()
            || self.allowed_values.is_some()
            || self.display_mask.is_some()
            || self.external_validation_enabled
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationResult {
    Valid,
    Warning { message: String },
    Error { message: String },
}

impl ValidationResult {
    pub fn is_acceptable(&self) -> bool {
        matches!(self, Self::Valid | Self::Warning { .. })
    }

    pub fn is_error(&self) -> bool {
        matches!(self, Self::Error { .. })
    }

    pub fn message(&self) -> Option<&str> {
        match self {
            Self::Valid => None,
            Self::Warning { message } | Self::Error { message } => Some(message),
        }
    }

    pub fn warning(message: impl Into<String>) -> Self {
        Self::Warning {
            message: message.into(),
        }
    }

    pub fn error(message: impl Into<String>) -> Self {
        Self::Error {
            message: message.into(),
        }
    }
}