validation of exact strings
This commit is contained in:
@@ -6,6 +6,49 @@ use crate::validation::{CharacterLimits, PatternFilters, DisplayMask};
|
|||||||
use crate::validation::{CustomFormatter, FormattingResult, PositionMapper};
|
use crate::validation::{CustomFormatter, FormattingResult, PositionMapper};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Whitelist of allowed exact values for a field.
|
||||||
|
/// If configured, the field is valid when it is empty (by default) or when the
|
||||||
|
/// content exactly matches one of the allowed values. This does not block field
|
||||||
|
/// switching (unlike minimum length in CharacterLimits).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AllowedValues {
|
||||||
|
allowed: Vec<String>,
|
||||||
|
allow_empty: bool,
|
||||||
|
case_insensitive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllowedValues {
|
||||||
|
pub fn new(allowed: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
allowed,
|
||||||
|
allow_empty: true,
|
||||||
|
case_insensitive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allow or disallow empty value to be considered valid (default: true).
|
||||||
|
pub fn allow_empty(mut self, allow: bool) -> Self {
|
||||||
|
self.allow_empty = allow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable/disable ASCII case-insensitive matching (default: false).
|
||||||
|
pub fn case_insensitive(mut self, ci: bool) -> Self {
|
||||||
|
self.case_insensitive = ci;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, text: &str) -> bool {
|
||||||
|
if self.case_insensitive {
|
||||||
|
self.allowed
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.eq_ignore_ascii_case(text))
|
||||||
|
} else {
|
||||||
|
self.allowed.iter().any(|s| s == text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Main validation configuration for a field
|
/// Main validation configuration for a field
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ValidationConfig {
|
pub struct ValidationConfig {
|
||||||
@@ -22,6 +65,9 @@ pub struct ValidationConfig {
|
|||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
|
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
|
||||||
|
|
||||||
|
/// Optional: restrict the field to one of exact allowed values (or empty)
|
||||||
|
pub allowed_values: Option<AllowedValues>,
|
||||||
|
|
||||||
/// Enable external validation indicator UI (feature 5)
|
/// Enable external validation indicator UI (feature 5)
|
||||||
pub external_validation_enabled: bool,
|
pub external_validation_enabled: bool,
|
||||||
|
|
||||||
@@ -50,6 +96,7 @@ impl std::fmt::Debug for ValidationConfig {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.field("allowed_values", &self.allowed_values)
|
||||||
.field("external_validation_enabled", &self.external_validation_enabled)
|
.field("external_validation_enabled", &self.external_validation_enabled)
|
||||||
.field("external_validation", &self.external_validation)
|
.field("external_validation", &self.external_validation)
|
||||||
.finish()
|
.finish()
|
||||||
@@ -167,6 +214,18 @@ impl ValidationConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allowed values (whitelist) validation
|
||||||
|
if let Some(ref allowed) = self.allowed_values {
|
||||||
|
// Empty value is allowed (default) or required (if allow_empty is false)
|
||||||
|
if text.is_empty() {
|
||||||
|
if !allowed.allow_empty {
|
||||||
|
return ValidationResult::warning("Value required");
|
||||||
|
}
|
||||||
|
} else if !allowed.matches(text) {
|
||||||
|
return ValidationResult::error("Value must be one of the allowed options");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Future: Add other validation types here
|
// Future: Add other validation types here
|
||||||
|
|
||||||
ValidationResult::Valid
|
ValidationResult::Valid
|
||||||
@@ -183,6 +242,12 @@ impl ValidationConfig {
|
|||||||
#[cfg(not(feature = "validation"))]
|
#[cfg(not(feature = "validation"))]
|
||||||
{ false }
|
{ false }
|
||||||
}
|
}
|
||||||
|
|| self.allowed_values.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if whitelist is configured
|
||||||
|
pub fn has_allowed_values(&self) -> bool {
|
||||||
|
self.allowed_values.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allows_field_switch(&self, text: &str) -> bool {
|
pub fn allows_field_switch(&self, text: &str) -> bool {
|
||||||
@@ -289,6 +354,41 @@ impl ValidationConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restrict content to one of the provided exact values (or empty).
|
||||||
|
/// - Empty is considered valid by default.
|
||||||
|
/// - Matching is case-sensitive by default.
|
||||||
|
pub fn with_allowed_values<S>(mut self, values: Vec<S>) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let vals: Vec<String> = values.into_iter().map(Into::into).collect();
|
||||||
|
self.config.allowed_values = Some(AllowedValues::new(vals));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as with_allowed_values, but case-insensitive (ASCII).
|
||||||
|
pub fn with_allowed_values_ci<S>(mut self, values: Vec<S>) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let vals: Vec<String> = values.into_iter().map(Into::into).collect();
|
||||||
|
self.config.allowed_values = Some(AllowedValues::new(vals).case_insensitive(true));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure whether empty value should be allowed when using AllowedValues.
|
||||||
|
pub fn with_allowed_values_allow_empty(mut self, allow_empty: bool) -> Self {
|
||||||
|
if let Some(av) = self.config.allowed_values.take() {
|
||||||
|
self.config.allowed_values = Some(AllowedValues {
|
||||||
|
allow_empty,
|
||||||
|
..av
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.config.allowed_values = Some(AllowedValues::new(vec![]).allow_empty(allow_empty));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Enable or disable external validation indicator UI (feature 5)
|
/// Enable or disable external validation indicator UI (feature 5)
|
||||||
pub fn with_external_validation_enabled(mut self, enabled: bool) -> Self {
|
pub fn with_external_validation_enabled(mut self, enabled: bool) -> Self {
|
||||||
self.config.external_validation_enabled = enabled;
|
self.config.external_validation_enabled = enabled;
|
||||||
@@ -391,6 +491,47 @@ mod tests {
|
|||||||
assert!(config.display_mask.is_some());
|
assert!(config.display_mask.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_allowed_values() {
|
||||||
|
let config = ValidationConfigBuilder::new()
|
||||||
|
.with_allowed_values(vec!["alpha", "beta", "gamma", "delta", "epsilon"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Empty should be valid by default
|
||||||
|
let result = config.validate_content("");
|
||||||
|
assert!(result.is_acceptable());
|
||||||
|
|
||||||
|
// Exact allowed values are valid
|
||||||
|
assert!(config.validate_content("alpha").is_acceptable());
|
||||||
|
assert!(config.validate_content("beta").is_acceptable());
|
||||||
|
|
||||||
|
// Anything else is an error
|
||||||
|
let res = config.validate_content("alph");
|
||||||
|
assert!(res.is_error());
|
||||||
|
let res = config.validate_content("ALPHA");
|
||||||
|
assert!(res.is_error()); // case-sensitive by default
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_allowed_values_case_insensitive_and_required() {
|
||||||
|
let config = ValidationConfigBuilder::new()
|
||||||
|
.with_allowed_values_ci(vec!["Yes", "No"])
|
||||||
|
.with_allowed_values_allow_empty(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Empty is not allowed now (warning so it's still acceptable for typing)
|
||||||
|
let res = config.validate_content("");
|
||||||
|
assert!(res.is_acceptable());
|
||||||
|
|
||||||
|
// Case-insensitive matches
|
||||||
|
assert!(config.validate_content("yes").is_acceptable());
|
||||||
|
assert!(config.validate_content("NO").is_acceptable());
|
||||||
|
|
||||||
|
// Random text is an error
|
||||||
|
let res = config.validate_content("maybe");
|
||||||
|
assert!(res.is_error());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validation_result() {
|
fn test_validation_result() {
|
||||||
let valid = ValidationResult::Valid;
|
let valid = ValidationResult::Valid;
|
||||||
|
|||||||
Reference in New Issue
Block a user