use core::ops::RangeBounds;
#[cfg(feature = "alloc")]
extern crate alloc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationResult {
Valid,
Invalid(&'static str),
}
impl ValidationResult {
#[inline]
pub fn is_valid(&self) -> bool {
matches!(self, ValidationResult::Valid)
}
#[inline]
pub fn is_invalid(&self) -> bool {
matches!(self, ValidationResult::Invalid(_))
}
pub fn error_message(&self) -> Option<&'static str> {
match self {
ValidationResult::Invalid(msg) => Some(msg),
ValidationResult::Valid => None,
}
}
}
pub trait Constraint<T: ?Sized> {
fn validate(&self, value: &T) -> ValidationResult;
fn description(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy)]
pub struct MaxLength {
max: usize,
}
impl MaxLength {
pub const fn new(max: usize) -> Self {
Self { max }
}
}
impl Constraint<str> for MaxLength {
fn validate(&self, value: &str) -> ValidationResult {
if value.len() <= self.max {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string exceeds maximum length")
}
}
fn description(&self) -> &'static str {
"maximum length constraint"
}
}
#[cfg(feature = "alloc")]
impl Constraint<alloc::string::String> for MaxLength {
fn validate(&self, value: &alloc::string::String) -> ValidationResult {
if value.len() <= self.max {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string exceeds maximum length")
}
}
fn description(&self) -> &'static str {
"maximum length constraint"
}
}
impl<T> Constraint<[T]> for MaxLength {
fn validate(&self, value: &[T]) -> ValidationResult {
if value.len() <= self.max {
ValidationResult::Valid
} else {
ValidationResult::Invalid("collection exceeds maximum length")
}
}
fn description(&self) -> &'static str {
"maximum length constraint"
}
}
#[cfg(feature = "alloc")]
impl<T> Constraint<alloc::vec::Vec<T>> for MaxLength {
fn validate(&self, value: &alloc::vec::Vec<T>) -> ValidationResult {
if value.len() <= self.max {
ValidationResult::Valid
} else {
ValidationResult::Invalid("collection exceeds maximum length")
}
}
fn description(&self) -> &'static str {
"maximum length constraint"
}
}
#[derive(Debug, Clone, Copy)]
pub struct MinLength {
min: usize,
}
impl MinLength {
pub const fn new(min: usize) -> Self {
Self { min }
}
}
impl Constraint<str> for MinLength {
fn validate(&self, value: &str) -> ValidationResult {
if value.len() >= self.min {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string below minimum length")
}
}
fn description(&self) -> &'static str {
"minimum length constraint"
}
}
#[cfg(feature = "alloc")]
impl Constraint<alloc::string::String> for MinLength {
fn validate(&self, value: &alloc::string::String) -> ValidationResult {
if value.len() >= self.min {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string below minimum length")
}
}
fn description(&self) -> &'static str {
"minimum length constraint"
}
}
impl<T> Constraint<[T]> for MinLength {
fn validate(&self, value: &[T]) -> ValidationResult {
if value.len() >= self.min {
ValidationResult::Valid
} else {
ValidationResult::Invalid("collection below minimum length")
}
}
fn description(&self) -> &'static str {
"minimum length constraint"
}
}
#[cfg(feature = "alloc")]
impl<T> Constraint<alloc::vec::Vec<T>> for MinLength {
fn validate(&self, value: &alloc::vec::Vec<T>) -> ValidationResult {
if value.len() >= self.min {
ValidationResult::Valid
} else {
ValidationResult::Invalid("collection below minimum length")
}
}
fn description(&self) -> &'static str {
"minimum length constraint"
}
}
#[derive(Debug, Clone)]
pub struct Range<T> {
min: Option<T>,
max: Option<T>,
}
impl<T: PartialOrd + Clone> Range<T> {
pub fn new(min: Option<T>, max: Option<T>) -> Self {
Self { min, max }
}
pub fn from_bounds<R: RangeBounds<T>>(bounds: &R) -> Self
where
T: Clone,
{
use core::ops::Bound;
let min = match bounds.start_bound() {
Bound::Included(v) => Some(v.clone()),
Bound::Excluded(_) => None, Bound::Unbounded => None,
};
let max = match bounds.end_bound() {
Bound::Included(v) => Some(v.clone()),
Bound::Excluded(_) => None,
Bound::Unbounded => None,
};
Self { min, max }
}
}
impl<T: PartialOrd> Constraint<T> for Range<T> {
fn validate(&self, value: &T) -> ValidationResult {
if let Some(ref min) = self.min {
if value < min {
return ValidationResult::Invalid("value below minimum");
}
}
if let Some(ref max) = self.max {
if value > max {
return ValidationResult::Invalid("value above maximum");
}
}
ValidationResult::Valid
}
fn description(&self) -> &'static str {
"range constraint"
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NonEmpty;
impl NonEmpty {
pub const fn new() -> Self {
Self
}
}
impl Constraint<str> for NonEmpty {
fn validate(&self, value: &str) -> ValidationResult {
if !value.is_empty() {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string must not be empty")
}
}
fn description(&self) -> &'static str {
"non-empty constraint"
}
}
#[cfg(feature = "alloc")]
impl Constraint<alloc::string::String> for NonEmpty {
fn validate(&self, value: &alloc::string::String) -> ValidationResult {
if !value.is_empty() {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string must not be empty")
}
}
fn description(&self) -> &'static str {
"non-empty constraint"
}
}
impl<T> Constraint<[T]> for NonEmpty {
fn validate(&self, value: &[T]) -> ValidationResult {
if !value.is_empty() {
ValidationResult::Valid
} else {
ValidationResult::Invalid("collection must not be empty")
}
}
fn description(&self) -> &'static str {
"non-empty constraint"
}
}
#[cfg(feature = "alloc")]
impl<T> Constraint<alloc::vec::Vec<T>> for NonEmpty {
fn validate(&self, value: &alloc::vec::Vec<T>) -> ValidationResult {
if !value.is_empty() {
ValidationResult::Valid
} else {
ValidationResult::Invalid("collection must not be empty")
}
}
fn description(&self) -> &'static str {
"non-empty constraint"
}
}
#[derive(Debug, Clone, Copy)]
pub struct AsciiOnly;
impl AsciiOnly {
pub const fn new() -> Self {
Self
}
}
impl Default for AsciiOnly {
fn default() -> Self {
Self::new()
}
}
impl Constraint<str> for AsciiOnly {
fn validate(&self, value: &str) -> ValidationResult {
if value.is_ascii() {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string must contain only ASCII characters")
}
}
fn description(&self) -> &'static str {
"ASCII-only constraint"
}
}
#[cfg(feature = "alloc")]
impl Constraint<alloc::string::String> for AsciiOnly {
fn validate(&self, value: &alloc::string::String) -> ValidationResult {
if value.is_ascii() {
ValidationResult::Valid
} else {
ValidationResult::Invalid("string must contain only ASCII characters")
}
}
fn description(&self) -> &'static str {
"ASCII-only constraint"
}
}
pub struct CustomValidator<T, F>
where
F: Fn(&T) -> bool,
{
validator: F,
error_message: &'static str,
description: &'static str,
_phantom: core::marker::PhantomData<T>,
}
impl<T, F> CustomValidator<T, F>
where
F: Fn(&T) -> bool,
{
pub const fn new(validator: F, error_message: &'static str, description: &'static str) -> Self {
Self {
validator,
error_message,
description,
_phantom: core::marker::PhantomData,
}
}
}
impl<T, F> Constraint<T> for CustomValidator<T, F>
where
F: Fn(&T) -> bool,
{
fn validate(&self, value: &T) -> ValidationResult {
if (self.validator)(value) {
ValidationResult::Valid
} else {
ValidationResult::Invalid(self.error_message)
}
}
fn description(&self) -> &'static str {
self.description
}
}
pub struct Constraints;
impl Constraints {
pub const fn max_len(max: usize) -> MaxLength {
MaxLength::new(max)
}
pub const fn min_len(min: usize) -> MinLength {
MinLength::new(min)
}
pub const fn non_empty() -> NonEmpty {
NonEmpty::new()
}
pub const fn ascii_only() -> AsciiOnly {
AsciiOnly::new()
}
pub fn range<T: PartialOrd + Clone>(min: Option<T>, max: Option<T>) -> Range<T> {
Range::new(min, max)
}
pub const fn custom<T, F: Fn(&T) -> bool>(
validator: F,
error_message: &'static str,
description: &'static str,
) -> CustomValidator<T, F> {
CustomValidator::new(validator, error_message, description)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_max_length_str() {
let constraint = MaxLength::new(10);
assert!(constraint.validate("hello").is_valid());
assert!(constraint.validate("0123456789").is_valid());
assert!(constraint.validate("01234567890").is_invalid());
}
#[test]
fn test_min_length_str() {
let constraint = MinLength::new(3);
assert!(constraint.validate("abc").is_valid());
assert!(constraint.validate("abcd").is_valid());
assert!(constraint.validate("ab").is_invalid());
}
#[test]
fn test_range_i32() {
let constraint = Range::new(Some(0i32), Some(100i32));
assert!(constraint.validate(&0).is_valid());
assert!(constraint.validate(&50).is_valid());
assert!(constraint.validate(&100).is_valid());
assert!(constraint.validate(&-1).is_invalid());
assert!(constraint.validate(&101).is_invalid());
}
#[test]
fn test_non_empty() {
let constraint = NonEmpty::new();
assert!(constraint.validate("hello").is_valid());
assert!(constraint.validate("").is_invalid());
}
#[test]
fn test_ascii_only() {
let constraint = AsciiOnly::new();
assert!(constraint.validate("hello world").is_valid());
assert!(constraint.validate("hello 世界").is_invalid());
}
#[test]
fn test_custom_validator() {
let is_even = Constraints::custom(
|x: &i32| x % 2 == 0,
"value must be even",
"even number constraint",
);
assert!(is_even.validate(&2).is_valid());
assert!(is_even.validate(&4).is_valid());
assert!(is_even.validate(&3).is_invalid());
}
#[test]
fn test_validation_result() {
let valid = ValidationResult::Valid;
let invalid = ValidationResult::Invalid("test error");
assert!(valid.is_valid());
assert!(!valid.is_invalid());
assert!(valid.error_message().is_none());
assert!(!invalid.is_valid());
assert!(invalid.is_invalid());
assert_eq!(invalid.error_message(), Some("test error"));
}
#[cfg(feature = "alloc")]
#[test]
fn test_vec_constraints() {
let max_len = MaxLength::new(5);
let min_len = MinLength::new(2);
let non_empty = NonEmpty::new();
let vec_short: alloc::vec::Vec<i32> = alloc::vec![1, 2];
let vec_long: alloc::vec::Vec<i32> = alloc::vec![1, 2, 3, 4, 5, 6];
let vec_empty: alloc::vec::Vec<i32> = alloc::vec![];
assert!(max_len.validate(&vec_short).is_valid());
assert!(max_len.validate(&vec_long).is_invalid());
assert!(min_len.validate(&vec_short).is_valid());
assert!(min_len.validate(&vec_empty).is_invalid());
assert!(non_empty.validate(&vec_short).is_valid());
assert!(non_empty.validate(&vec_empty).is_invalid());
}
#[test]
fn test_constraints_builder() {
let max = Constraints::max_len(100);
let min = Constraints::min_len(1);
let non_empty = Constraints::non_empty();
let ascii = Constraints::ascii_only();
let range = Constraints::range(Some(0u8), Some(255u8));
assert!(max.validate("hello").is_valid());
assert!(min.validate("h").is_valid());
assert!(non_empty.validate("x").is_valid());
assert!(ascii.validate("hello").is_valid());
assert!(range.validate(&100u8).is_valid());
}
}