use std::fmt;
use crate::domain::model::is_valid_kebab_lowercase;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct QueryIdentifier(String);
impl QueryIdentifier {
pub fn new(s: &str) -> anyhow::Result<Self> {
if is_valid_kebab_lowercase(s) {
Ok(Self(s.to_owned()))
} else {
anyhow::bail!("invalid query name '{s}': must match [a-z0-9][a-z0-9-]*")
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for QueryIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
pub mod strategy {
use super::QueryIdentifier;
use proptest::prelude::*;
pub fn query_identifier() -> impl Strategy<Value = QueryIdentifier> {
"[a-z0-9][a-z0-9-]{0,30}".prop_map(|s| QueryIdentifier::new(&s).unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn as_str_roundtrips_through_new(id in strategy::query_identifier()) {
let s = id.as_str().to_string();
let again = QueryIdentifier::new(&s).unwrap();
prop_assert_eq!(again.as_str().to_string(), s);
}
}
#[test]
fn accepts_kebab_lowercase() {
assert_eq!(
QueryIdentifier::new("count-issues").unwrap().as_str(),
"count-issues"
);
assert_eq!(QueryIdentifier::new("a").unwrap().as_str(), "a");
assert_eq!(
QueryIdentifier::new("0-leading-digit").unwrap().as_str(),
"0-leading-digit"
);
}
#[test]
fn rejects_empty() {
assert!(QueryIdentifier::new("").is_err());
}
#[test]
fn rejects_uppercase_and_separators() {
assert!(QueryIdentifier::new("CountIssues").is_err());
assert!(QueryIdentifier::new("count_issues").is_err());
assert!(QueryIdentifier::new("count issues").is_err());
assert!(QueryIdentifier::new("-leading-hyphen").is_err());
}
}