use crate::errors::ValidationError;
use crate::traits::{PrimitiveValue, ValueObject};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct BoundedString<const MIN: usize, const MAX: usize>(String);
impl<const MIN: usize, const MAX: usize> ValueObject for BoundedString<MIN, MAX> {
type Input = String;
type Error = ValidationError;
fn new(value: Self::Input) -> Result<Self, Self::Error> {
if MIN > MAX {
return Err(ValidationError::Custom {
type_name: "BoundedString",
message: format!("MIN ({MIN}) must be <= MAX ({MAX})"),
});
}
let trimmed = value.trim().to_owned();
let len = trimmed.chars().count();
if len < MIN || len > MAX {
return Err(ValidationError::OutOfRange {
type_name: "BoundedString",
min: MIN.to_string(),
max: MAX.to_string(),
actual: len.to_string(),
});
}
Ok(Self(trimmed))
}
fn into_inner(self) -> Self::Input {
self.0
}
}
impl<const MIN: usize, const MAX: usize> PrimitiveValue for BoundedString<MIN, MAX> {
type Primitive = String;
fn value(&self) -> &String {
&self.0
}
}
impl<const MIN: usize, const MAX: usize> TryFrom<String> for BoundedString<MIN, MAX> {
type Error = ValidationError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[cfg(feature = "serde")]
impl<const MIN: usize, const MAX: usize> From<BoundedString<MIN, MAX>> for String {
fn from(v: BoundedString<MIN, MAX>) -> String {
v.0
}
}
impl<const MIN: usize, const MAX: usize> TryFrom<&str> for BoundedString<MIN, MAX> {
type Error = ValidationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value.to_owned())
}
}
impl<const MIN: usize, const MAX: usize> std::fmt::Display for BoundedString<MIN, MAX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_string_within_bounds() {
let s: BoundedString<2, 10> = BoundedString::new("hello".into()).unwrap();
assert_eq!(s.value(), "hello");
}
#[test]
fn trims_surrounding_whitespace() {
let s: BoundedString<1, 10> = BoundedString::new(" hi ".into()).unwrap();
assert_eq!(s.value(), "hi");
}
#[test]
fn rejects_too_short() {
assert!(BoundedString::<3, 10>::new("ab".into()).is_err());
}
#[test]
fn rejects_too_long() {
assert!(BoundedString::<1, 3>::new("toolong".into()).is_err());
}
#[test]
fn accepts_exact_min() {
let s: BoundedString<3, 10> = BoundedString::new("abc".into()).unwrap();
assert_eq!(s.value(), "abc");
}
#[test]
fn accepts_exact_max() {
let s: BoundedString<1, 5> = BoundedString::new("hello".into()).unwrap();
assert_eq!(s.value(), "hello");
}
#[test]
fn counts_unicode_chars_not_bytes() {
let s: BoundedString<1, 4> = BoundedString::new("café".into()).unwrap();
assert_eq!(s.value(), "café");
}
#[test]
fn try_from_str() {
let s: BoundedString<1, 10> = "test".try_into().unwrap();
assert_eq!(s.value(), "test");
}
}