#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct Description(String);
impl Description {
pub const MAX_LENGTH: usize = 50;
pub fn parse(value: impl Into<String>) -> Result<Self, DescriptionError> {
let value = value.into().trim().to_owned();
if value.is_empty() {
Err(DescriptionError::Empty)
} else {
Ok(Self(value))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.0.chars().count()
}
}
impl AsRef<str> for Description {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Description {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DescriptionError {
#[error("Description cannot be empty")]
Empty,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_description_accepted() {
let result = Description::parse("add new feature");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "add new feature");
}
#[test]
fn single_character_description_accepted() {
let result = Description::parse("a");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "a");
}
#[test]
fn description_with_numbers_accepted() {
let result = Description::parse("fix issue #123");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "fix issue #123");
}
#[test]
fn description_with_special_chars_accepted() {
let result = Description::parse("add @decorator support (beta)");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "add @decorator support (beta)");
}
#[test]
fn description_with_punctuation_accepted() {
let result = Description::parse("fix: handle edge case!");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "fix: handle edge case!");
}
#[test]
fn empty_string_rejected() {
let result = Description::parse("");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DescriptionError::Empty);
}
#[test]
fn whitespace_only_rejected() {
let result = Description::parse(" ");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DescriptionError::Empty);
}
#[test]
fn tabs_only_rejected() {
let result = Description::parse("\t\t");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DescriptionError::Empty);
}
#[test]
fn mixed_whitespace_rejected() {
let result = Description::parse(" \t \n ");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DescriptionError::Empty);
}
#[test]
fn newline_only_rejected() {
let result = Description::parse("\n");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DescriptionError::Empty);
}
#[test]
fn leading_whitespace_trimmed() {
let result = Description::parse(" add feature");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "add feature");
}
#[test]
fn trailing_whitespace_trimmed() {
let result = Description::parse("add feature ");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "add feature");
}
#[test]
fn leading_and_trailing_whitespace_trimmed() {
let result = Description::parse(" add feature ");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "add feature");
}
#[test]
fn internal_whitespace_preserved() {
let result = Description::parse("add multiple spaces");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "add multiple spaces");
}
#[test]
fn description_over_soft_limit_accepted() {
let desc_51 = "a".repeat(51);
let result = Description::parse(&desc_51);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 51);
let desc_72 = "a".repeat(72);
let result = Description::parse(&desc_72);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 72);
}
#[test]
fn length_checked_after_trimming() {
let desc_with_spaces = format!(" {} ", "a".repeat(50));
let result = Description::parse(&desc_with_spaces);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str().len(), 50);
}
#[test]
fn fifty_characters_accepted() {
let desc_50 = "a".repeat(50);
let result = Description::parse(&desc_50);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str().len(), 50);
}
#[test]
fn max_length_constant_is_50() {
assert_eq!(Description::MAX_LENGTH, 50);
}
#[test]
fn as_str_returns_inner_string() {
let desc = Description::parse("my description").unwrap();
assert_eq!(desc.as_str(), "my description");
}
#[test]
fn len_returns_correct_length() {
let desc = Description::parse("hello").unwrap();
assert_eq!(desc.len(), 5);
}
#[test]
fn len_counts_unicode_chars_not_bytes() {
let desc = Description::parse("café").unwrap();
assert_eq!(desc.len(), 4);
let desc = Description::parse("fix 🐛").unwrap();
assert_eq!(desc.len(), 5);
}
#[test]
fn display_outputs_inner_string() {
let desc = Description::parse("add feature").unwrap();
assert_eq!(format!("{}", desc), "add feature");
}
#[test]
fn description_is_cloneable() {
let original = Description::parse("add feature").unwrap();
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn description_equality() {
let desc1 = Description::parse("add feature").unwrap();
let desc2 = Description::parse("add feature").unwrap();
let desc3 = Description::parse("fix bug").unwrap();
assert_eq!(desc1, desc2);
assert_ne!(desc1, desc3);
}
#[test]
fn description_has_debug() {
let desc = Description::parse("add feature").unwrap();
let debug_output = format!("{:?}", desc);
assert!(debug_output.contains("Description"));
assert!(debug_output.contains("add feature"));
}
#[test]
fn description_as_ref_str() {
let desc = Description::parse("add feature").unwrap();
let s: &str = desc.as_ref();
assert_eq!(s, "add feature");
}
#[test]
fn empty_error_display() {
let err = DescriptionError::Empty;
let msg = format!("{}", err);
assert!(msg.contains("cannot be empty"));
}
#[test]
fn whitespace_after_trim_is_empty() {
let whitespace_variants = [" ", " ", "\t", "\n", "\r\n", " \t \n "];
for ws in whitespace_variants {
let result = Description::parse(ws);
assert!(result.is_err(), "Expected error for whitespace: {:?}", ws);
assert_eq!(
result.unwrap_err(),
DescriptionError::Empty,
"Expected Empty error for whitespace: {:?}",
ws
);
}
}
#[test]
fn boundary_length_after_trim() {
let desc = format!(" {} ", "x".repeat(50));
let result = Description::parse(&desc);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 50);
}
#[test]
fn over_soft_limit_after_trim_accepted() {
let desc = format!(" {} ", "x".repeat(51));
let result = Description::parse(&desc);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 51);
}
}