#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Assignee(String);
impl Assignee {
pub fn new(s: &str) -> anyhow::Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
anyhow::bail!("assignee cannot be empty");
}
if trimmed.chars().any(|c| c.is_whitespace()) {
anyhow::bail!("assignee '{trimmed}' must not contain whitespace");
}
Ok(Assignee(trimmed.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Assignee {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for Assignee {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Assignee::new(s)
}
}
#[cfg(test)]
pub mod strategy {
use super::*;
use proptest::prelude::*;
pub fn assignee() -> impl Strategy<Value = Assignee> {
"[a-z0-9.@_-]{1,30}".prop_map(|s| Assignee::new(&s).unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn accepts_simple_username() {
assert!(Assignee::new("alice").is_ok());
}
#[test]
fn accepts_email() {
assert!(Assignee::new("alice@example.com").is_ok());
}
#[test]
fn accepts_dotted_username() {
assert!(Assignee::new("alice.bob").is_ok());
}
#[test]
fn rejects_empty() {
assert!(Assignee::new("").is_err());
}
#[test]
fn rejects_blank() {
assert!(Assignee::new(" ").is_err());
}
#[test]
fn rejects_whitespace() {
assert!(Assignee::new("alice bob").is_err());
}
#[test]
fn trims_leading_trailing_whitespace() {
let a = Assignee::new(" alice ").unwrap();
assert_eq!(a.as_str(), "alice");
}
#[test]
fn display_matches_as_str() {
let a = Assignee::new("alice").unwrap();
assert_eq!(a.to_string(), "alice");
}
#[test]
fn from_str_roundtrips() {
let a: Assignee = "alice@example.com".parse().unwrap();
assert_eq!(a.as_str(), "alice@example.com");
}
proptest! {
#[test]
fn prop_clone_equals_original(a in strategy::assignee()) {
prop_assert_eq!(a.clone(), a);
}
#[test]
fn prop_as_str_is_non_empty(a in strategy::assignee()) {
prop_assert!(!a.as_str().is_empty());
}
#[test]
fn prop_no_whitespace(a in strategy::assignee()) {
prop_assert!(!a.as_str().chars().any(|c| c.is_whitespace()));
}
}
}