#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Description(String);
impl serde::Serialize for Description {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
impl Description {
pub fn new(s: impl Into<String>) -> Result<Self, &'static str> {
let raw = s.into();
if raw.contains('\n') {
return Err("description must fit on a single line");
}
let trimmed = raw.trim().to_string();
if trimmed.is_empty() {
return Err("description must not be empty");
}
Ok(Description(trimmed))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Description {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl TryFrom<String> for Description {
type Error = &'static str;
fn try_from(s: String) -> Result<Self, Self::Error> {
Description::new(s)
}
}
impl TryFrom<&str> for Description {
type Error = &'static str;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Description::new(s)
}
}
#[cfg(test)]
pub mod strategy {
use super::*;
use proptest::prelude::*;
pub fn arb_description() -> impl Strategy<Value = Description> {
proptest::string::string_regex("[A-Za-z0-9][A-Za-z0-9 .,:;!?-]{0,79}")
.unwrap()
.prop_map(|s| Description::new(s).expect("strategy always produces valid descriptions"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_accepts_one_line_text() {
let d = Description::new("A short summary").unwrap();
assert_eq!(d.as_str(), "A short summary");
}
#[test]
fn new_rejects_empty_string() {
assert!(Description::new("").is_err());
}
#[test]
fn new_rejects_whitespace_only() {
assert!(Description::new(" ").is_err());
assert!(Description::new("\t").is_err());
}
#[test]
fn new_rejects_multiline_value() {
assert!(Description::new("first line\nsecond line").is_err());
}
#[test]
fn new_trims_padding() {
let d = Description::new(" hello ").unwrap();
assert_eq!(d.as_str(), "hello");
}
#[test]
fn display_roundtrips() {
let d = Description::new("summary").unwrap();
assert_eq!(d.to_string(), "summary");
}
#[test]
fn equality_holds_for_same_value() {
assert_eq!(
Description::new("abc").unwrap(),
Description::new("abc").unwrap()
);
}
#[test]
fn equality_normalises_whitespace_padding() {
assert_eq!(
Description::new(" abc ").unwrap(),
Description::new("abc").unwrap()
);
}
proptest::proptest! {
#[test]
fn prop_trim_is_idempotent(s in "[A-Za-z0-9][A-Za-z0-9 ]{0,79}") {
let d = Description::new(&s).unwrap();
assert_eq!(Description::new(d.as_str()).unwrap(), d);
}
}
}