use std::fmt;
use std::str::FromStr;
use super::super::is_valid_kebab_lowercase;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StatusName(pub(super) String);
impl StatusName {
pub fn new(s: &str) -> anyhow::Result<Self> {
if is_valid_kebab_lowercase(s) {
Ok(StatusName(s.to_string()))
} else {
anyhow::bail!("invalid status '{s}': must match [a-z0-9][a-z0-9-]*")
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for StatusName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for StatusName {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
StatusName::new(s)
}
}
#[cfg(test)]
pub mod strategy {
use super::StatusName;
use proptest::prelude::*;
pub fn status_name() -> impl Strategy<Value = StatusName> {
proptest::string::string_regex("[a-z][a-z0-9-]{0,11}")
.unwrap()
.prop_map(|s| StatusName::new(&s).expect("regex guarantees valid StatusName"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn status_name_accepts_simple() {
assert!(StatusName::new("proposed").is_ok());
}
#[test]
fn status_name_accepts_hyphen() {
assert!(StatusName::new("in-progress").is_ok());
}
#[test]
fn status_name_accepts_digit() {
assert!(StatusName::new("phase2").is_ok());
}
#[test]
fn status_name_rejects_empty() {
assert!(StatusName::new("").is_err());
}
#[test]
fn status_name_rejects_uppercase() {
assert!(StatusName::new("Open").is_err());
}
#[test]
fn status_name_rejects_leading_hyphen() {
assert!(StatusName::new("-open").is_err());
}
#[test]
fn status_name_rejects_spaces() {
assert!(StatusName::new("in progress").is_err());
}
#[test]
fn status_name_rejects_special_chars() {
assert!(StatusName::new("open!").is_err());
}
#[test]
fn status_name_from_str_accepts_valid() {
let s: StatusName = "deprecated".parse().unwrap();
assert_eq!(s.as_str(), "deprecated");
}
#[test]
fn status_name_from_str_rejects_invalid() {
assert!("".parse::<StatusName>().is_err());
assert!("Bad".parse::<StatusName>().is_err());
}
proptest! {
#[test]
fn prop_status_name_strategy_valid(n in strategy::status_name()) {
prop_assert!(StatusName::new(n.as_str()).is_ok());
}
#[test]
fn prop_status_name_chars_are_valid(n in strategy::status_name()) {
let text = n.as_str();
prop_assert!(!text.is_empty());
prop_assert!(text.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'));
}
}
}