use crate::validation::{ExternalValidationState, ValidationConfig, ValidationResult};
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ValidationState {
field_configs: HashMap<usize, ValidationConfig>,
field_results: HashMap<usize, ValidationResult>,
validated_fields: std::collections::HashSet<usize>,
enabled: bool,
external_results: HashMap<usize, ExternalValidationState>,
last_switch_block: Option<String>,
}
impl ValidationState {
pub fn new() -> Self {
Self {
field_configs: HashMap::new(),
field_results: HashMap::new(),
validated_fields: std::collections::HashSet::new(),
enabled: true,
external_results: HashMap::new(),
last_switch_block: None,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.field_results.clear();
self.validated_fields.clear();
self.external_results.clear(); }
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_field_config(&mut self, field_index: usize, config: ValidationConfig) {
if config.has_validation() || config.external_validation_enabled {
self.field_configs.insert(field_index, config);
} else {
self.field_configs.remove(&field_index);
self.field_results.remove(&field_index);
self.validated_fields.remove(&field_index);
self.external_results.remove(&field_index);
}
}
pub fn get_field_config(&self, field_index: usize) -> Option<&ValidationConfig> {
self.field_configs.get(&field_index)
}
pub fn remove_field_config(&mut self, field_index: usize) {
self.field_configs.remove(&field_index);
self.field_results.remove(&field_index);
self.validated_fields.remove(&field_index);
self.external_results.remove(&field_index);
}
pub fn set_external_validation(&mut self, field_index: usize, state: ExternalValidationState) {
self.external_results.insert(field_index, state);
}
pub fn get_external_validation(&self, field_index: usize) -> ExternalValidationState {
self.external_results
.get(&field_index)
.cloned()
.unwrap_or(ExternalValidationState::NotValidated)
}
pub fn clear_external_validation(&mut self, field_index: usize) {
self.external_results.remove(&field_index);
}
pub fn clear_all_external_validation(&mut self) {
self.external_results.clear();
}
pub fn validate_char_insertion(
&mut self,
field_index: usize,
current_text: &str,
position: usize,
character: char,
) -> ValidationResult {
if !self.enabled {
return ValidationResult::Valid;
}
if let Some(config) = self.field_configs.get(&field_index) {
let result = config.validate_char_insertion(current_text, position, character);
self.field_results.insert(field_index, result.clone());
self.validated_fields.insert(field_index);
result
} else {
ValidationResult::Valid
}
}
pub fn validate_field_content(&mut self, field_index: usize, text: &str) -> ValidationResult {
if !self.enabled {
return ValidationResult::Valid;
}
if let Some(config) = self.field_configs.get(&field_index) {
let result = config.validate_content(text);
self.field_results.insert(field_index, result.clone());
self.validated_fields.insert(field_index);
result
} else {
ValidationResult::Valid
}
}
pub fn get_field_result(&self, field_index: usize) -> Option<&ValidationResult> {
self.field_results.get(&field_index)
}
#[cfg(feature = "validation")]
pub fn formatted_for(
&self,
field_index: usize,
raw: &str,
) -> Option<(
String,
std::sync::Arc<dyn crate::validation::PositionMapper>,
Option<String>,
)> {
let config = self.field_configs.get(&field_index)?;
config.run_custom_formatter(raw)
}
pub fn is_field_validated(&self, field_index: usize) -> bool {
self.validated_fields.contains(&field_index)
}
pub fn clear_field_result(&mut self, field_index: usize) {
self.field_results.remove(&field_index);
self.validated_fields.remove(&field_index);
}
pub fn clear_all_results(&mut self) {
self.field_results.clear();
self.validated_fields.clear();
}
pub fn validated_field_indices(&self) -> impl Iterator<Item = usize> + '_ {
self.field_configs.keys().copied()
}
pub fn fields_with_errors(&self) -> impl Iterator<Item = usize> + '_ {
self.field_results
.iter()
.filter(|(_, result)| result.is_error())
.map(|(index, _)| *index)
}
pub fn fields_with_warnings(&self) -> impl Iterator<Item = usize> + '_ {
self.field_results
.iter()
.filter(|(_, result)| matches!(result, ValidationResult::Warning { .. }))
.map(|(index, _)| *index)
}
pub fn has_errors(&self) -> bool {
self.field_results.values().any(|result| result.is_error())
}
pub fn has_warnings(&self) -> bool {
self.field_results
.values()
.any(|result| matches!(result, ValidationResult::Warning { .. }))
}
pub fn validated_field_count(&self) -> usize {
self.field_configs.len()
}
pub fn allows_field_switch(&self, field_index: usize, text: &str) -> bool {
if !self.enabled {
return true;
}
if let Some(config) = self.field_configs.get(&field_index) {
config.allows_field_switch(text)
} else {
true }
}
pub fn field_switch_block_reason(&self, field_index: usize, text: &str) -> Option<String> {
if !self.enabled {
return None;
}
if let Some(config) = self.field_configs.get(&field_index) {
config.field_switch_block_reason(text)
} else {
None }
}
pub fn summary(&self) -> ValidationSummary {
let total_validated = self.validated_fields.len();
let errors = self.fields_with_errors().count();
let warnings = self.fields_with_warnings().count();
let valid = total_validated - errors - warnings;
ValidationSummary {
total_fields: self.field_configs.len(),
validated_fields: total_validated,
valid_fields: valid,
warning_fields: warnings,
error_fields: errors,
}
}
pub fn set_last_switch_block<S: Into<String>>(&mut self, reason: S) {
self.last_switch_block = Some(reason.into());
}
pub fn clear_last_switch_block(&mut self) {
self.last_switch_block = None;
}
pub fn last_switch_block(&self) -> Option<&str> {
self.last_switch_block.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationSummary {
pub total_fields: usize,
pub validated_fields: usize,
pub valid_fields: usize,
pub warning_fields: usize,
pub error_fields: usize,
}
impl ValidationSummary {
pub fn is_all_valid(&self) -> bool {
self.error_fields == 0 && self.validated_fields == self.total_fields
}
pub fn has_errors(&self) -> bool {
self.error_fields > 0
}
pub fn has_warnings(&self) -> bool {
self.warning_fields > 0
}
pub fn completion_percentage(&self) -> f32 {
if self.total_fields == 0 {
1.0
} else {
self.validated_fields as f32 / self.total_fields as f32
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::{CharacterLimits, ValidationConfigBuilder};
#[test]
fn test_validation_state_creation() {
let state = ValidationState::new();
assert!(state.is_enabled());
assert_eq!(state.validated_field_count(), 0);
}
#[test]
fn test_enable_disable() {
let mut state = ValidationState::new();
let config = ValidationConfigBuilder::new().with_max_length(10).build();
state.set_field_config(0, config);
let result = state.validate_field_content(0, "test");
assert!(result.is_acceptable());
assert!(state.is_field_validated(0));
state.set_enabled(false);
assert!(!state.is_enabled());
assert!(!state.is_field_validated(0));
let result = state.validate_field_content(0, "this is way too long for the limit");
assert!(result.is_acceptable());
}
#[test]
fn test_field_config_management() {
let mut state = ValidationState::new();
let config = ValidationConfigBuilder::new().with_max_length(5).build();
state.set_field_config(0, config);
assert_eq!(state.validated_field_count(), 1);
assert!(state.get_field_config(0).is_some());
state.remove_field_config(0);
assert_eq!(state.validated_field_count(), 0);
assert!(state.get_field_config(0).is_none());
}
#[test]
fn test_character_insertion_validation() {
let mut state = ValidationState::new();
let config = ValidationConfigBuilder::new().with_max_length(5).build();
state.set_field_config(0, config);
let result = state.validate_char_insertion(0, "test", 4, 'x');
assert!(result.is_acceptable());
let result = state.validate_char_insertion(0, "tests", 5, 'x');
assert!(!result.is_acceptable());
assert!(state.is_field_validated(0));
let stored_result = state.get_field_result(0);
assert!(stored_result.is_some());
assert!(!stored_result.unwrap().is_acceptable());
}
#[test]
fn test_validation_summary() {
let mut state = ValidationState::new();
let config1 = ValidationConfigBuilder::new().with_max_length(5).build();
let config2 = ValidationConfigBuilder::new().with_max_length(10).build();
state.set_field_config(0, config1);
state.set_field_config(1, config2);
state.validate_field_content(0, "test");
state.validate_field_content(1, "this is too long");
let summary = state.summary();
assert_eq!(summary.total_fields, 2);
assert_eq!(summary.validated_fields, 2);
assert_eq!(summary.valid_fields, 1);
assert_eq!(summary.error_fields, 1);
assert_eq!(summary.warning_fields, 0);
assert!(!summary.is_all_valid());
assert!(summary.has_errors());
assert!(!summary.has_warnings());
assert_eq!(summary.completion_percentage(), 1.0);
}
#[test]
fn test_error_and_warning_tracking() {
let mut state = ValidationState::new();
let config = ValidationConfigBuilder::new()
.with_character_limits(CharacterLimits::new_range(3, 10).with_warning_threshold(8))
.build();
state.set_field_config(0, config);
state.validate_field_content(0, "hi");
assert!(state.has_warnings());
assert!(!state.has_errors());
state.validate_field_content(0, "hello");
assert!(!state.has_warnings());
assert!(!state.has_errors());
state.validate_field_content(0, "hello world!");
assert!(!state.has_warnings());
assert!(state.has_errors());
}
}