tui-canvas-validation-core 0.8.2

Validation core for the tui-canvas
Documentation
use crate::{ValidationConfig, ValidationMergeError, ValidationSettings};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationRule {
    pub name: String,
    pub description: Option<String>,
    pub settings: ValidationSettings,
}

impl ValidationRule {
    pub fn resolve(&self) -> ValidationConfig {
        self.settings.resolve()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationSet {
    pub name: String,
    pub description: Option<String>,
    pub items: Vec<ValidationSetItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationSetItem {
    GlobalRuleRef(String),
    InlineRule {
        name: Option<String>,
        validation: ValidationSettings,
    },
}

impl ValidationSet {
    pub fn resolve_settings_with_rules<'a>(
        &'a self,
        rules: impl Fn(&str) -> Option<&'a ValidationRule>,
    ) -> Result<ValidationSettings, ValidationSetResolveError> {
        let settings = self.items.iter().map(|item| match item {
            ValidationSetItem::GlobalRuleRef(name) => {
                rules(name).map(|rule| &rule.settings).ok_or_else(|| {
                    ValidationSetResolveError::MissingGlobalRule { name: name.clone() }
                })
            }
            ValidationSetItem::InlineRule { validation, .. } => Ok(validation),
        });

        let settings = settings.collect::<Result<Vec<_>, _>>()?;
        Ok(ValidationSettings::merge_rules(settings)?)
    }

    pub fn resolve_with_rules<'a>(
        &'a self,
        rules: impl Fn(&str) -> Option<&'a ValidationRule>,
    ) -> Result<ValidationConfig, ValidationSetResolveError> {
        Ok(self.resolve_settings_with_rules(rules)?.resolve())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ValidationSetResolveError {
    #[error("validation set references missing global rule '{name}'")]
    MissingGlobalRule { name: String },
    #[error(transparent)]
    Merge(#[from] ValidationMergeError),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedValidation {
    pub set_name: Option<String>,
    pub settings: ValidationSettings,
}

impl AppliedValidation {
    pub fn resolve(&self) -> ValidationConfig {
        self.settings.resolve()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        CharacterFilterSettings, CharacterLimits, PatternSettings, PositionFilterSettings,
        PositionRange,
    };

    #[test]
    fn validation_set_merges_rule_fragments() {
        let set = ValidationSet {
            name: "phone".to_string(),
            description: None,
            items: vec![
                ValidationSetItem::InlineRule {
                    name: Some("phone-length".to_string()),
                    validation: ValidationSettings {
                        character_limits: Some(CharacterLimits::new_range(10, 15)),
                        ..ValidationSettings::default()
                    },
                },
                ValidationSetItem::InlineRule {
                    name: Some("digits-only".to_string()),
                    validation: ValidationSettings {
                        pattern: Some(PatternSettings {
                            filters: vec![PositionFilterSettings {
                                positions: PositionRange::From(0),
                                filter: CharacterFilterSettings::Numeric,
                            }],
                            description: None,
                        }),
                        ..ValidationSettings::default()
                    },
                },
            ],
        };

        let settings = set
            .resolve_settings_with_rules(|_| None)
            .expect("set should resolve");

        assert!(settings.character_limits.is_some());
        assert_eq!(settings.pattern.expect("pattern").filters.len(), 1);
    }

    #[test]
    fn validation_set_rejects_duplicate_singleton_rules() {
        let set = ValidationSet {
            name: "conflict".to_string(),
            description: None,
            items: vec![
                ValidationSetItem::InlineRule {
                    name: Some("short".to_string()),
                    validation: ValidationSettings {
                        character_limits: Some(CharacterLimits::new(10)),
                        ..ValidationSettings::default()
                    },
                },
                ValidationSetItem::InlineRule {
                    name: Some("long".to_string()),
                    validation: ValidationSettings {
                        character_limits: Some(CharacterLimits::new(20)),
                        ..ValidationSettings::default()
                    },
                },
            ],
        };

        assert!(set.resolve_settings_with_rules(|_| None).is_err());
    }
}