use crate::{Rule, RuleContext, ValidationError};
use std::collections::HashSet;
use std::hash::Hash;
pub fn min_items<T: 'static>(min: usize) -> Rule<[T]> {
Rule::new(move |value: &[T], ctx: &RuleContext| {
let count = value.len();
if count < min {
let mut err = ValidationError::single(
ctx.full_path(),
"too_few_items",
format!("Must have at least {} items", min),
);
err.violations[0].meta.insert("min", min.to_string());
err.violations[0].meta.insert("actual", count.to_string());
err
} else {
ValidationError::default()
}
})
}
pub fn max_items<T: 'static>(max: usize) -> Rule<[T]> {
Rule::new(move |value: &[T], ctx: &RuleContext| {
let count = value.len();
if count > max {
let mut err = ValidationError::single(
ctx.full_path(),
"too_many_items",
format!("Must have at most {} items", max),
);
err.violations[0].meta.insert("max", max.to_string());
err.violations[0].meta.insert("actual", count.to_string());
err
} else {
ValidationError::default()
}
})
}
pub fn unique<T>() -> Rule<[T]>
where
T: Eq + Hash + 'static,
{
Rule::new(|value: &[T], ctx: &RuleContext| {
let mut seen = HashSet::new();
let mut duplicate_count = 0;
for item in value {
if !seen.insert(item) {
duplicate_count += 1;
}
}
if duplicate_count > 0 {
let mut err = ValidationError::single(
ctx.full_path(),
"duplicate_items",
format!(
"All items must be unique (found {} duplicates)",
duplicate_count
),
);
err.violations[0]
.meta
.insert("duplicates", duplicate_count.to_string());
err
} else {
ValidationError::default()
}
})
}
pub fn non_empty_items() -> Rule<[String]> {
Rule::new(|value: &[String], ctx: &RuleContext| {
let mut empty_indices = Vec::new();
for (i, item) in value.iter().enumerate() {
if item.is_empty() {
empty_indices.push(i);
}
}
if !empty_indices.is_empty() {
let count = empty_indices.len();
let mut err = ValidationError::single(
ctx.full_path(),
"empty_item",
format!("All items must be non-empty (found {} empty items)", count),
);
err.violations[0]
.meta
.insert("empty_count", count.to_string());
err.violations[0].meta.insert(
"indices",
format!(
"[{}]",
empty_indices
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(", ")
),
);
err
} else {
ValidationError::default()
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_min_items_valid() {
let rule: Rule<[i32]> = min_items(2);
assert!(rule.apply(&[1, 2, 3]).is_empty());
assert!(rule.apply(&[1, 2]).is_empty());
let rule_str: Rule<[&str]> = min_items(2);
assert!(rule_str.apply(&["a", "b", "c"]).is_empty());
}
#[test]
fn test_min_items_invalid() {
let rule = min_items(2);
let result = rule.apply(&[1]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_few_items");
assert_eq!(result.violations[0].meta.get("min"), Some("2"));
assert_eq!(result.violations[0].meta.get("actual"), Some("1"));
let result = rule.apply(&Vec::<i32>::new());
assert!(!result.is_empty());
assert_eq!(result.violations[0].meta.get("actual"), Some("0"));
}
#[test]
fn test_max_items_valid() {
let rule = max_items(3);
assert!(rule.apply(&[1, 2]).is_empty());
assert!(rule.apply(&[1, 2, 3]).is_empty()); assert!(rule.apply(&Vec::<i32>::new()).is_empty()); }
#[test]
fn test_max_items_invalid() {
let rule = max_items(3);
let result = rule.apply(&[1, 2, 3, 4]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_many_items");
assert_eq!(result.violations[0].meta.get("max"), Some("3"));
assert_eq!(result.violations[0].meta.get("actual"), Some("4"));
let result = rule.apply(&[1, 2, 3, 4, 5]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].meta.get("actual"), Some("5"));
}
#[test]
fn test_min_max_composition() {
let rule = min_items(2).and(max_items(4));
assert!(rule.apply(&[1, 2]).is_empty()); assert!(rule.apply(&[1, 2, 3]).is_empty()); assert!(rule.apply(&[1, 2, 3, 4]).is_empty());
let result = rule.apply(&[1]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_few_items");
let result = rule.apply(&[1, 2, 3, 4, 5]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_many_items");
}
#[test]
fn test_unique_valid() {
let rule: Rule<[i32]> = unique();
assert!(rule.apply(&[1, 2, 3, 4]).is_empty());
assert!(rule.apply(&Vec::<i32>::new()).is_empty()); assert!(rule.apply(&[42]).is_empty());
let rule_str: Rule<[&str]> = unique();
assert!(rule_str.apply(&["a", "b", "c"]).is_empty());
}
#[test]
fn test_unique_invalid_numbers() {
let rule = unique();
let result = rule.apply(&[1, 2, 2, 3]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "duplicate_items");
assert_eq!(result.violations[0].meta.get("duplicates"), Some("1"));
let result = rule.apply(&[1, 1, 2, 2, 3, 3]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].meta.get("duplicates"), Some("3"));
}
#[test]
fn test_unique_invalid_strings() {
let rule = unique();
let result = rule.apply(&["a", "b", "a", "c"]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "duplicate_items");
}
#[test]
fn test_unique_all_duplicates() {
let rule = unique();
let result = rule.apply(&[1, 1, 1, 1]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].meta.get("duplicates"), Some("3"));
}
#[test]
fn test_collection_with_min_and_unique() {
let rule = min_items(1).and(unique());
assert!(rule.apply(&["rust", "validation"]).is_empty());
let result = rule.apply(&Vec::<&str>::new());
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_few_items");
let result = rule.apply(&["rust", "rust"]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "duplicate_items");
}
#[test]
fn test_non_empty_items_valid() {
let rule = non_empty_items();
let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()];
assert!(rule.apply(&tags).is_empty());
let tags = vec!["a".to_string()];
assert!(rule.apply(&tags).is_empty());
let tags = vec!["rust".to_string(), "validation".to_string()];
assert!(rule.apply(&tags).is_empty());
assert!(rule.apply(&Vec::<String>::new()).is_empty());
}
#[test]
fn test_non_empty_items_invalid() {
let rule = non_empty_items();
let tags = vec!["tag1".to_string(), "".to_string(), "tag3".to_string()];
let result = rule.apply(&tags);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "empty_item");
assert_eq!(result.violations[0].meta.get("empty_count"), Some("1"));
assert!(result.violations[0]
.meta
.get("indices")
.unwrap()
.contains("1"));
let tags = vec![
"".to_string(),
"tag2".to_string(),
"".to_string(),
"tag4".to_string(),
];
let result = rule.apply(&tags);
assert!(!result.is_empty());
assert_eq!(result.violations[0].meta.get("empty_count"), Some("2"));
let tags = vec!["".to_string(), "".to_string(), "".to_string()];
let result = rule.apply(&tags);
assert!(!result.is_empty());
assert_eq!(result.violations[0].meta.get("empty_count"), Some("3"));
let invalid_tags = vec!["rust".to_string(), "".to_string()];
let result = rule.apply(&invalid_tags);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "empty_item");
}
#[test]
fn test_non_empty_items_composition() {
let rule = min_items(1).and(unique()).and(non_empty_items());
let tags = vec!["rust".to_string(), "validation".to_string()];
assert!(rule.apply(&tags).is_empty());
let result = rule.apply(&Vec::<String>::new());
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_few_items");
let tags = vec!["rust".to_string(), "rust".to_string()];
let result = rule.apply(&tags);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "duplicate_items");
let tags = vec!["rust".to_string(), "".to_string()];
let result = rule.apply(&tags);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "empty_item");
}
#[test]
fn test_min_items_zero() {
let rule: Rule<[i32]> = min_items(0);
assert!(rule.apply(&[]).is_empty());
assert!(rule.apply(&[1]).is_empty());
assert!(rule.apply(&[1, 2, 3]).is_empty());
}
#[test]
fn test_max_items_zero() {
let rule: Rule<[i32]> = max_items(0);
assert!(rule.apply(&[]).is_empty());
let result = rule.apply(&[1]);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "too_many_items");
assert_eq!(result.violations[0].meta.get("max"), Some("0"));
}
#[test]
fn test_min_equals_max() {
let rule = min_items(3).and(max_items(3));
assert!(!rule.apply(&[1, 2]).is_empty());
assert!(rule.apply(&[1, 2, 3]).is_empty());
assert!(!rule.apply(&[1, 2, 3, 4]).is_empty());
}
#[test]
fn test_min_items_large() {
let rule: Rule<[i32]> = min_items(1000);
let large: Vec<i32> = (0..1000).collect();
assert!(rule.apply(&large).is_empty());
let large: Vec<i32> = (0..999).collect();
assert!(!rule.apply(&large).is_empty());
}
#[test]
fn test_max_items_large() {
let rule: Rule<[i32]> = max_items(1000);
let large: Vec<i32> = (0..1000).collect();
assert!(rule.apply(&large).is_empty());
let large: Vec<i32> = (0..1001).collect();
assert!(!rule.apply(&large).is_empty());
}
#[test]
fn test_unique_large_collection() {
let rule: Rule<[i32]> = unique();
let large: Vec<i32> = (0..1000).collect();
assert!(rule.apply(&large).is_empty());
let mut with_dup: Vec<i32> = (0..1000).collect();
with_dup.push(0);
assert!(!rule.apply(&with_dup).is_empty());
}
#[test]
fn test_unique_counts_duplicates_correctly() {
let rule: Rule<[i32]> = unique();
let result = rule.apply(&[1, 1]);
assert_eq!(result.violations[0].meta.get("duplicates"), Some("1"));
let result = rule.apply(&[1, 1, 1]);
assert_eq!(result.violations[0].meta.get("duplicates"), Some("2"));
let result = rule.apply(&[1, 2, 1, 2]);
assert_eq!(result.violations[0].meta.get("duplicates"), Some("2"));
}
#[test]
fn test_non_empty_items_indices_format() {
let rule = non_empty_items();
let tags = vec![
"".to_string(),
"valid".to_string(),
"".to_string(),
"also_valid".to_string(),
"".to_string(),
];
let result = rule.apply(&tags);
let indices = result.violations[0].meta.get("indices").unwrap();
assert!(indices.contains("0"));
assert!(indices.contains("2"));
assert!(indices.contains("4"));
}
#[test]
fn test_non_empty_items_single_empty() {
let rule = non_empty_items();
let tags = vec!["".to_string()];
let result = rule.apply(&tags);
assert_eq!(result.violations[0].meta.get("empty_count"), Some("1"));
assert!(result.violations[0]
.meta
.get("indices")
.unwrap()
.contains("0"));
}
#[test]
fn test_unique_with_strings() {
let rule: Rule<[String]> = unique();
let strings: Vec<String> = vec!["a".to_string(), "b".to_string(), "c".to_string()];
assert!(rule.apply(&strings).is_empty());
let with_dup: Vec<String> = vec!["a".to_string(), "b".to_string(), "a".to_string()];
assert!(!rule.apply(&with_dup).is_empty());
}
#[test]
fn test_unique_with_tuples() {
let rule: Rule<[(i32, i32)]> = unique();
assert!(rule.apply(&[(1, 2), (3, 4), (5, 6)]).is_empty());
assert!(!rule.apply(&[(1, 2), (3, 4), (1, 2)]).is_empty());
}
#[test]
fn test_min_items_message_format() {
let rule: Rule<[i32]> = min_items(5);
let result = rule.apply(&[1, 2]);
assert_eq!(result.violations[0].message, "Must have at least 5 items");
}
#[test]
fn test_max_items_message_format() {
let rule: Rule<[i32]> = max_items(2);
let result = rule.apply(&[1, 2, 3, 4, 5]);
assert_eq!(result.violations[0].message, "Must have at most 2 items");
}
#[test]
fn test_unique_message_format() {
let rule: Rule<[i32]> = unique();
let result = rule.apply(&[1, 1, 2, 2]);
assert!(result.violations[0].message.contains("found 2 duplicates"));
}
#[test]
fn test_non_empty_items_message_format() {
let rule = non_empty_items();
let tags = vec!["".to_string(), "".to_string()];
let result = rule.apply(&tags);
assert!(result.violations[0].message.contains("found 2 empty items"));
}
#[test]
fn test_non_empty_items_whitespace_is_valid() {
let rule = non_empty_items();
let tags = vec![" ".to_string(), "\t".to_string(), "\n".to_string()];
assert!(rule.apply(&tags).is_empty());
}
}