use super::super::{PartialValidationResult, Priority, ValidationResult, Validator};
use std::collections::HashSet;
#[derive(Debug)]
pub struct ChoiceValidator {
choices: Vec<String>,
case_sensitive: bool,
min_choices: usize,
max_choices: usize,
priority: Priority,
custom_message: Option<String>,
selection_separator: String,
}
impl ChoiceValidator {
pub fn new(choices: Vec<String>) -> Self {
Self {
choices,
case_sensitive: false,
min_choices: 1,
max_choices: 1,
priority: Priority::High,
custom_message: None,
selection_separator: ",".to_string(),
}
}
pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
self.case_sensitive = case_sensitive;
self
}
pub fn min_choices(mut self, min: usize) -> Self {
self.min_choices = min;
self
}
pub fn max_choices(mut self, max: usize) -> Self {
self.max_choices = max;
self
}
pub fn with_priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.custom_message = Some(message.into());
self
}
pub fn selection_separator(mut self, separator: impl Into<String>) -> Self {
self.selection_separator = separator.into();
self
}
fn parse_input(&self, input: &str) -> Vec<String> {
if self.max_choices == 1 {
vec![input.trim().to_string()]
} else {
input
.split(&self.selection_separator)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
}
fn is_valid_choice(&self, choice: &str) -> bool {
if self.case_sensitive {
self.choices.contains(&choice.to_string())
} else {
let choice_lower = choice.to_lowercase();
self.choices
.iter()
.any(|c| c.to_lowercase() == choice_lower)
}
}
fn get_canonical_choice(&self, choice: &str) -> Option<String> {
if self.case_sensitive {
if self.choices.contains(&choice.to_string()) {
Some(choice.to_string())
} else {
None
}
} else {
let choice_lower = choice.to_lowercase();
self.choices
.iter()
.find(|c| c.to_lowercase() == choice_lower)
.cloned()
}
}
}
impl Validator for ChoiceValidator {
fn validate(&self, input: &str) -> ValidationResult {
let parsed_choices = self.parse_input(input);
if parsed_choices.len() < self.min_choices {
let message = if let Some(msg) = &self.custom_message {
msg.clone()
} else {
format!("At least {} choice(s) required", self.min_choices)
};
return ValidationResult::failure("choice", self.priority, &message);
}
if parsed_choices.len() > self.max_choices {
let message = if let Some(msg) = &self.custom_message {
msg.clone()
} else {
format!("At most {} choice(s) allowed", self.max_choices)
};
return ValidationResult::failure("choice", self.priority, &message);
}
let mut seen = HashSet::new();
let mut duplicates = Vec::new();
for choice in &parsed_choices {
if let Some(canonical) = self.get_canonical_choice(choice) {
if !seen.insert(canonical.clone()) {
duplicates.push(canonical);
}
}
}
if !duplicates.is_empty() {
let message = if let Some(msg) = &self.custom_message {
msg.clone()
} else {
format!("Duplicate choices not allowed: {}", duplicates.join(", "))
};
return ValidationResult::failure("choice", self.priority, &message);
}
let mut invalid_choices = Vec::new();
for choice in &parsed_choices {
if !self.is_valid_choice(choice) {
invalid_choices.push(choice.clone());
}
}
if !invalid_choices.is_empty() {
let message = if let Some(msg) = &self.custom_message {
msg.clone()
} else {
let valid_choices_str = self.choices.join(", ");
format!(
"Invalid choice(s): {}. Valid options: {}",
invalid_choices.join(", "),
valid_choices_str
)
};
return ValidationResult::failure("choice", self.priority, &message);
}
ValidationResult::success("choice")
}
fn partial_validate(&self, input: &str, _cursor_pos: usize) -> PartialValidationResult {
if input.is_empty() {
return PartialValidationResult::valid();
}
if self.max_choices == 1 {
let has_partial_match = self.choices.iter().any(|choice| {
if self.case_sensitive {
choice.starts_with(input)
} else {
choice.to_lowercase().starts_with(&input.to_lowercase())
}
});
if !has_partial_match {
for (i, _ch) in input.char_indices() {
let partial = &input[..=i];
let has_match = self.choices.iter().any(|choice| {
if self.case_sensitive {
choice.starts_with(partial)
} else {
choice.to_lowercase().starts_with(&partial.to_lowercase())
}
});
if !has_match {
return PartialValidationResult::error_at(i);
}
}
}
} else {
let parts: Vec<&str> = input.split(',').collect();
if let Some(current_choice) = parts.last() {
let current_choice = current_choice.trim();
if !current_choice.is_empty() {
let has_partial_match = self.choices.iter().any(|choice| {
if self.case_sensitive {
choice.starts_with(current_choice)
} else {
choice
.to_lowercase()
.starts_with(¤t_choice.to_lowercase())
}
});
if !has_partial_match {
let prefix_len: usize = parts[..parts.len() - 1]
.iter()
.map(|p| p.len() + 1) .sum();
for (i, _ch) in current_choice.char_indices() {
let partial = current_choice[..=i].trim();
let has_match = self.choices.iter().any(|choice| {
if self.case_sensitive {
choice.starts_with(partial)
} else {
choice.to_lowercase().starts_with(&partial.to_lowercase())
}
});
if !has_match {
return PartialValidationResult::error_at(prefix_len + i);
}
}
}
}
}
}
PartialValidationResult::valid()
}
fn priority(&self) -> Priority {
self.priority
}
fn name(&self) -> &str {
"choice"
}
}