use crate::domain::ValidationError;
use anyhow::Context;
use std::fmt;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]
#[sqlx(transparent)]
pub struct ValidName(String);
impl ValidName {
pub fn parse(s: String) -> Result<ValidName, ValidationError> {
let is_empty_or_whitespace = s.trim().is_empty();
let is_too_long = s.graphemes(true).count() > 256;
if is_empty_or_whitespace || is_too_long {
Err(ValidationError(format!("Invalid Name: {s}")))
} else {
Ok(Self(s))
}
}
}
impl AsRef<str> for ValidName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl serde::Serialize for ValidName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for ValidName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let buf = String::deserialize(deserializer)?;
ValidName::parse(buf.clone())
.with_context(|| format!("Parsing '{buf}' failed"))
.map_err(serde::de::Error::custom)
}
}
impl fmt::Display for ValidName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use crate::domain::ValidName;
use claim::{assert_err, assert_ok};
use fake::{Fake, StringFaker};
#[derive(Debug, Clone)]
struct ValidNameString(pub String);
impl quickcheck::Arbitrary for ValidNameString {
fn arbitrary(_g: &mut quickcheck::Gen) -> Self {
let name = StringFaker::with(
String::from(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*&^%$#@!~",
)
.into_bytes(),
1..256,
)
.fake();
Self(name)
}
}
#[test]
fn a_256_grapheme_long_name_is_valid() {
let name = "ё".repeat(256);
assert_ok!(ValidName::parse(name));
}
#[test]
fn a_name_longer_than_256_graphemes_is_rejected() {
let name = "a".repeat(257);
assert_err!(ValidName::parse(name));
}
#[test]
fn whitespace_only_names_are_rejected() {
let name = " ".to_string();
assert_err!(ValidName::parse(name));
}
#[test]
fn empty_string_is_rejected() {
let name = "".to_string();
assert_err!(ValidName::parse(name));
}
#[quickcheck]
fn a_valid_name_is_parsed_successfully(name: ValidNameString) {
assert_ok!(ValidName::parse(name.0));
}
}