use crate::domain::KeyDomain;
use crate::error::KeyParseError;
use crate::key::Key;
#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::fmt::Write;
const fn is_allowed_key_byte_default(b: u8) -> bool {
matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.')
}
const fn is_separator_byte_default(b: u8) -> bool {
matches!(b, b'_' | b'-' | b'.')
}
const fn is_allowed_end_byte_default(b: u8) -> bool {
matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z')
}
#[must_use]
pub const fn is_valid_key_default(s: &str, max_length: usize) -> bool {
let bytes = s.as_bytes();
let len = bytes.len();
if len == 0 {
return false;
}
if len > max_length {
return false;
}
let mut i = 0;
while i < len {
let b = bytes[i];
if !is_allowed_key_byte_default(b) {
return false;
}
if i > 0 && is_separator_byte_default(b) && bytes[i - 1] == b {
return false;
}
i += 1;
}
if !is_allowed_end_byte_default(bytes[len - 1]) {
return false;
}
true
}
#[inline]
#[must_use]
pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
validate_key::<T>(key).is_ok()
}
pub fn validate_key<T: KeyDomain>(key: &str) -> Result<(), KeyParseError> {
Key::<T>::new(key).map(|_| ())
}
#[must_use]
pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
T::validation_help()
}
#[must_use]
pub fn validation_info<T: KeyDomain>() -> String {
let mut info = format!("Domain: {}\n", T::DOMAIN_NAME);
writeln!(info, "Max length: {}", T::MAX_LENGTH).unwrap();
writeln!(info, "Min length: {}", T::min_length()).unwrap();
writeln!(info, "Expected length: {}", T::EXPECTED_LENGTH).unwrap();
writeln!(info, "Case insensitive: {}", T::CASE_INSENSITIVE).unwrap();
writeln!(info, "Custom validation: {}", T::HAS_CUSTOM_VALIDATION).unwrap();
writeln!(
info,
"Custom normalization: {}",
T::HAS_CUSTOM_NORMALIZATION,
)
.unwrap();
writeln!(info, "Default separator: '{}'", T::default_separator()).unwrap();
if let Some(help) = T::validation_help() {
info.push_str("Help: ");
info.push_str(help);
info.push('\n');
}
let examples = T::examples();
if !examples.is_empty() {
info.push_str("Examples: ");
for (i, example) in examples.iter().enumerate() {
if i > 0 {
info.push_str(", ");
}
info.push_str(example);
}
info.push('\n');
}
info
}
pub fn validate_batch<T: KeyDomain, I>(keys: I) -> (Vec<String>, Vec<(String, KeyParseError)>)
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let mut valid = Vec::new();
let mut invalid = Vec::new();
for key in keys {
let key_str = key.as_ref();
match validate_key::<T>(key_str) {
Ok(()) => valid.push(key_str.to_string()),
Err(e) => invalid.push((key_str.to_string(), e)),
}
}
(valid, invalid)
}
pub fn filter_valid<T: KeyDomain, I>(keys: I) -> impl Iterator<Item = I::Item>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
keys.into_iter()
.filter(|key| is_valid_key::<T>(key.as_ref()))
}
pub fn count_valid<T: KeyDomain, I>(keys: I) -> usize
where
I: IntoIterator,
I::Item: AsRef<str>,
{
keys.into_iter()
.filter(|key| is_valid_key::<T>(key.as_ref()))
.count()
}
pub fn all_valid<T: KeyDomain, I>(keys: I) -> bool
where
I: IntoIterator,
I::Item: AsRef<str>,
{
keys.into_iter().all(|key| is_valid_key::<T>(key.as_ref()))
}
pub fn any_valid<T: KeyDomain, I>(keys: I) -> bool
where
I: IntoIterator,
I::Item: AsRef<str>,
{
keys.into_iter().any(|key| is_valid_key::<T>(key.as_ref()))
}
pub trait IntoKey<T: KeyDomain> {
fn into_key(self) -> Result<Key<T>, KeyParseError>;
fn try_into_key(self) -> Option<Key<T>>;
}
impl<T: KeyDomain> IntoKey<T> for &str {
#[inline]
fn into_key(self) -> Result<Key<T>, KeyParseError> {
Key::new(self)
}
#[inline]
fn try_into_key(self) -> Option<Key<T>> {
Key::try_new(self)
}
}
impl<T: KeyDomain> IntoKey<T> for String {
#[inline]
fn into_key(self) -> Result<Key<T>, KeyParseError> {
Key::from_string(self)
}
#[inline]
fn try_into_key(self) -> Option<Key<T>> {
Key::from_string(self).ok()
}
}
impl<T: KeyDomain> IntoKey<T> for &String {
#[inline]
fn into_key(self) -> Result<Key<T>, KeyParseError> {
Key::new(self)
}
#[inline]
fn try_into_key(self) -> Option<Key<T>> {
Key::try_new(self)
}
}
type ValidatorFunction = fn(&str) -> Result<(), KeyParseError>;
#[derive(Debug)]
pub struct ValidationBuilder<T: KeyDomain> {
allow_empty_collection: bool,
max_failures: Option<usize>,
stop_on_first_error: bool,
custom_validator: Option<ValidatorFunction>,
_phantom: core::marker::PhantomData<T>,
}
impl<T: KeyDomain> Default for ValidationBuilder<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: KeyDomain> ValidationBuilder<T> {
#[must_use]
pub fn new() -> Self {
Self {
allow_empty_collection: false,
max_failures: None,
stop_on_first_error: false,
custom_validator: None,
_phantom: core::marker::PhantomData,
}
}
#[must_use]
pub fn allow_empty_collection(mut self, allow: bool) -> Self {
self.allow_empty_collection = allow;
self
}
#[must_use]
pub fn max_failures(mut self, max: usize) -> Self {
self.max_failures = Some(max);
self
}
#[must_use]
pub fn stop_on_first_error(mut self, stop: bool) -> Self {
self.stop_on_first_error = stop;
self
}
#[must_use]
pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
self.custom_validator = Some(validator);
self
}
pub fn validate<I>(&self, keys: I) -> ValidationResult
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let mut valid = Vec::new();
let mut errors = Vec::new();
let mut keys = keys.into_iter().peekable();
if keys.peek().is_none() && !self.allow_empty_collection {
return ValidationResult {
valid,
errors: vec![(String::new(), KeyParseError::Empty)],
total_processed: 0,
};
}
for key in keys {
let key_str = key.as_ref();
if let Some(max) = self.max_failures {
if errors.len() >= max {
break;
}
}
if self.stop_on_first_error && !errors.is_empty() {
break;
}
match validate_key::<T>(key_str) {
Ok(()) => {
let normalized = Key::<T>::normalize(key_str);
if let Some(custom) = self.custom_validator {
match custom(&normalized) {
Ok(()) => valid.push(normalized.into_owned()),
Err(e) => errors.push((normalized.into_owned(), e)),
}
} else {
valid.push(normalized.into_owned());
}
}
Err(e) => errors.push((key_str.to_string(), e)),
}
}
ValidationResult {
total_processed: valid.len() + errors.len(),
valid,
errors,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationResult {
pub total_processed: usize,
pub valid: Vec<String>,
pub errors: Vec<(String, KeyParseError)>,
}
impl ValidationResult {
#[inline]
#[must_use]
pub fn is_success(&self) -> bool {
self.errors.is_empty()
}
#[inline]
#[must_use]
pub fn valid_count(&self) -> usize {
self.valid.len()
}
#[inline]
#[must_use]
pub fn error_count(&self) -> usize {
self.errors.len()
}
#[must_use]
pub fn success_rate(&self) -> f64 {
if self.total_processed == 0 {
0.0
} else {
#[expect(
clippy::cast_precision_loss,
reason = "total_processed fits comfortably in f64; precision loss only occurs above 2^53 items"
)]
let valid_ratio = self.valid.len() as f64 / self.total_processed as f64;
valid_ratio * 100.0
}
}
pub fn into_keys<T: KeyDomain>(self) -> Result<Vec<Key<T>>, KeyParseError> {
self.valid
.into_iter()
.map(|s| Key::from_string(s))
.collect()
}
#[must_use]
pub fn try_into_keys<T: KeyDomain>(self) -> Vec<Key<T>> {
self.valid
.into_iter()
.filter_map(|s| Key::from_string(s).ok())
.collect()
}
}
#[must_use]
pub fn strict_validator<T: KeyDomain>() -> ValidationBuilder<T> {
ValidationBuilder::new()
.stop_on_first_error(true)
.allow_empty_collection(false)
}
#[must_use]
pub fn lenient_validator<T: KeyDomain>() -> ValidationBuilder<T> {
ValidationBuilder::new()
.stop_on_first_error(false)
.allow_empty_collection(true)
}
pub fn quick_convert<T: KeyDomain, I>(keys: I) -> Result<Vec<Key<T>>, Vec<(String, KeyParseError)>>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let mut valid = Vec::new();
let mut errors = Vec::new();
for key in keys {
let key_str = key.as_ref().to_string();
match Key::<T>::from_string(key_str.clone()) {
Ok(k) => valid.push(k),
Err(e) => errors.push((key_str, e)),
}
}
if errors.is_empty() {
Ok(valid)
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TestDomain;
impl crate::Domain for TestDomain {
const DOMAIN_NAME: &'static str = "test";
}
impl KeyDomain for TestDomain {
const MAX_LENGTH: usize = 32;
fn validation_help() -> Option<&'static str> {
Some("Test domain help")
}
fn examples() -> &'static [&'static str] {
&["example1", "example2"]
}
}
#[test]
fn is_valid_key_accepts_good_rejects_bad() {
assert!(is_valid_key::<TestDomain>("valid_key"));
assert!(!is_valid_key::<TestDomain>(""));
assert!(!is_valid_key::<TestDomain>("a".repeat(50).as_str()));
}
#[test]
fn validate_key_returns_error_for_empty() {
assert!(validate_key::<TestDomain>("valid_key").is_ok());
assert!(validate_key::<TestDomain>("").is_err());
}
#[test]
fn validation_info_contains_domain_details() {
let info = validation_info::<TestDomain>();
assert!(info.contains("Domain: test"));
assert!(info.contains("Max length: 32"));
assert!(info.contains("Help: Test domain help"));
assert!(info.contains("Examples: example1, example2"));
}
#[test]
fn validate_batch_separates_valid_and_invalid() {
let keys = vec!["valid1", "", "valid2", "bad key"];
let (valid, invalid) = validate_batch::<TestDomain, _>(&keys);
assert_eq!(valid.len(), 2);
assert_eq!(invalid.len(), 2);
assert!(valid.contains(&"valid1".to_string()));
assert!(valid.contains(&"valid2".to_string()));
}
#[test]
fn filter_valid_removes_bad_keys() {
let keys = vec!["valid1", "", "valid2", "bad key"];
let valid: Vec<_> = filter_valid::<TestDomain, _>(&keys).collect();
assert_eq!(valid.len(), 2);
assert!(valid.contains(&&"valid1"));
assert!(valid.contains(&&"valid2"));
}
#[test]
fn count_valid_matches_filter_length() {
let keys = vec!["valid1", "", "valid2", "bad key"];
let count = count_valid::<TestDomain, _>(&keys);
assert_eq!(count, 2);
}
#[test]
fn all_valid_true_only_when_all_pass() {
let all_valid_keys = vec!["valid1", "valid2"];
let mixed = vec!["valid1", "", "valid2"];
assert!(all_valid::<TestDomain, _>(&all_valid_keys));
assert!(!all_valid::<TestDomain, _>(&mixed));
}
#[test]
fn any_valid_true_when_at_least_one_passes() {
let mixed = vec!["", "valid1", ""];
let all_invalid = vec!["", ""];
assert!(any_valid::<TestDomain, _>(&mixed));
assert!(!any_valid::<TestDomain, _>(&all_invalid));
}
#[test]
fn into_key_converts_str_and_string() {
let key1: Key<TestDomain> = "test_key".into_key().unwrap();
let key2: Key<TestDomain> = "another_key".to_string().into_key().unwrap();
assert_eq!(key1.as_str(), "test_key");
assert_eq!(key2.as_str(), "another_key");
let invalid: Option<Key<TestDomain>> = "".try_into_key();
assert!(invalid.is_none());
}
#[test]
fn builder_respects_max_failures_limit() {
let builder = ValidationBuilder::<TestDomain>::new()
.allow_empty_collection(true)
.max_failures(2)
.stop_on_first_error(false);
let keys = vec!["valid1", "", "valid2", "", "valid3"];
let result = builder.validate(&keys);
#[cfg(feature = "std")]
{
println!("Total processed: {}", result.total_processed);
println!("Valid count: {}", result.valid_count());
println!("Error count: {}", result.error_count());
println!("Valid keys: {:?}", result.valid);
println!("Errors: {:?}", result.errors);
}
assert_eq!(result.valid_count(), 2); assert_eq!(result.error_count(), 2); assert!(!result.is_success()); assert_eq!(result.total_processed, 4); assert!(result.success_rate() > 40.0 && result.success_rate() <= 60.0); }
#[test]
fn builder_stops_on_first_error_when_configured() {
let builder = ValidationBuilder::<TestDomain>::new()
.stop_on_first_error(true)
.allow_empty_collection(false);
let keys = vec!["valid", "", "another"];
let result = builder.validate(&keys);
assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
assert_eq!(result.error_count(), 1);
}
#[test]
fn builder_processes_all_when_not_stopping_on_error() {
let builder = ValidationBuilder::<TestDomain>::new()
.stop_on_first_error(false)
.allow_empty_collection(true);
let keys = vec!["valid", "", "another"];
let result = builder.validate(&keys);
assert_eq!(result.total_processed, 3);
assert_eq!(result.valid_count(), 2);
assert_eq!(result.error_count(), 1);
}
#[test]
fn validation_result_computes_success_rate() {
const EPSILON: f64 = 1e-10;
let keys = vec!["valid1", "valid2"];
let (valid, errors) = validate_batch::<TestDomain, _>(keys);
let result = ValidationResult {
total_processed: valid.len() + errors.len(),
valid,
errors,
};
assert!(result.is_success());
assert_eq!(result.valid_count(), 2);
assert_eq!(result.error_count(), 0);
assert!((result.success_rate() - 100.0).abs() < EPSILON);
let keys = result.try_into_keys::<TestDomain>();
assert_eq!(keys.len(), 2);
}
#[test]
fn strict_validator_stops_on_first_error() {
let validator = strict_validator::<TestDomain>();
let keys = vec!["valid", "", "another"];
let result = validator.validate(&keys);
assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
assert_eq!(result.error_count(), 1);
}
#[test]
fn lenient_validator_processes_all_items() {
let validator = lenient_validator::<TestDomain>();
let keys = vec!["valid", "", "another"];
let result = validator.validate(&keys);
assert_eq!(result.total_processed, 3);
assert_eq!(result.valid_count(), 2);
assert_eq!(result.error_count(), 1);
}
#[test]
fn quick_convert_succeeds_or_returns_errors() {
let strings = vec!["key1", "key2", "key3"];
let keys = quick_convert::<TestDomain, _>(&strings).unwrap();
assert_eq!(keys.len(), 3);
let mixed = vec!["key1", "", "key2"];
let result = quick_convert::<TestDomain, _>(&mixed);
assert!(result.is_err());
}
#[test]
fn custom_validator_applies_extra_check() {
fn custom_check(key: &str) -> Result<(), KeyParseError> {
if key.starts_with("custom_") {
Ok(())
} else {
Err(KeyParseError::custom(9999, "Must start with custom_"))
}
}
let validator = ValidationBuilder::<TestDomain>::new().custom_validator(custom_check);
let keys = vec!["custom_key", "invalid_key"];
let result = validator.validate(&keys);
assert_eq!(result.valid_count(), 1);
assert_eq!(result.error_count(), 1);
}
const _GOOD: () = assert!(is_valid_key_default("user_123", 64));
const _EMPTY: () = assert!(!is_valid_key_default("", 64));
const _TOO_LONG: () = assert!(!is_valid_key_default("abcdefgh", 4));
const _TRAILING_SEP: () = assert!(!is_valid_key_default("foo_", 64));
const _CONSECUTIVE: () = assert!(!is_valid_key_default("a__b", 64));
const _WITH_SPACE: () = assert!(!is_valid_key_default("hello world", 64));
const _LEADING_SEP: () = assert!(is_valid_key_default("_foo", 64));
const _HYPHEN_MID: () = assert!(is_valid_key_default("foo-bar", 64));
const _DOT_MID: () = assert!(is_valid_key_default("foo.bar", 64));
const _CONSEC_HYPHEN: () = assert!(!is_valid_key_default("foo--bar", 64));
const _CONSEC_DOT: () = assert!(!is_valid_key_default("foo..bar", 64));
const _NON_ASCII: () = assert!(!is_valid_key_default("héllo", 64));
const _UPPERCASE: () = assert!(is_valid_key_default("FooBar", 64));
const _DIGITS_ONLY: () = assert!(is_valid_key_default("12345", 64));
const _EXACT_MAX: () = assert!(is_valid_key_default("ab", 2));
const _OVER_MAX: () = assert!(!is_valid_key_default("abc", 2));
#[test]
fn is_valid_key_default_matches_runtime_for_valid_keys() {
assert!(is_valid_key_default("hello", 64));
assert!(is_valid_key_default("user_name", 64));
assert!(is_valid_key_default("foo-bar.baz", 64));
assert!(is_valid_key_default("ABC123", 64));
}
#[test]
fn is_valid_key_default_rejects_all_bad_patterns() {
assert!(!is_valid_key_default("", 64));
assert!(!is_valid_key_default("trailing_", 64));
assert!(!is_valid_key_default("trailing-", 64));
assert!(!is_valid_key_default("trailing.", 64));
assert!(!is_valid_key_default("a__b", 64));
assert!(!is_valid_key_default("a--b", 64));
assert!(!is_valid_key_default("a..b", 64));
assert!(!is_valid_key_default("has space", 64));
assert!(!is_valid_key_default("has\ttab", 64));
assert!(!is_valid_key_default("a@b", 64));
assert!(!is_valid_key_default("a!b", 64));
}
#[test]
fn is_valid_key_default_respects_max_length() {
let exactly_max = "a".repeat(32);
let over_max = "a".repeat(33);
assert!(is_valid_key_default(&exactly_max, 32));
assert!(!is_valid_key_default(&over_max, 32));
}
}