use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RecordKind(String);
impl serde::Serialize for RecordKind {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
impl RecordKind {
pub fn new(s: &str) -> anyhow::Result<Self> {
if super::is_valid_kebab_lowercase(s) {
Ok(RecordKind(s.to_string()))
} else {
anyhow::bail!("invalid record kind '{s}': must match [a-z0-9][a-z0-9-]*")
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RecordKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for RecordKind {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
RecordKind::new(s)
}
}
#[cfg(test)]
pub mod strategy {
use super::*;
use proptest::prelude::*;
pub fn record_kind() -> impl Strategy<Value = RecordKind> {
proptest::string::string_regex("[a-z][a-z0-9-]{1,7}")
.unwrap()
.prop_map(|s| RecordKind::new(&s).expect("regex guarantees valid RecordKind"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn new_accepts_simple_kind() {
assert!(RecordKind::new("adr").is_ok());
}
#[test]
fn new_accepts_kind_with_hyphen() {
assert!(RecordKind::new("game-ddr").is_ok());
}
#[test]
fn new_accepts_kind_with_digits() {
assert!(RecordKind::new("adr2").is_ok());
}
#[test]
fn new_rejects_empty_string() {
assert!(RecordKind::new("").is_err());
}
#[test]
fn new_rejects_uppercase() {
assert!(RecordKind::new("ADR").is_err());
}
#[test]
fn new_rejects_leading_hyphen() {
assert!(RecordKind::new("-adr").is_err());
}
#[test]
fn new_rejects_spaces() {
assert!(RecordKind::new("my kind").is_err());
}
#[test]
fn new_rejects_special_chars() {
assert!(RecordKind::new("adr!").is_err());
}
#[test]
fn display_roundtrips() {
let k = RecordKind::new("adr").unwrap();
assert_eq!(k.to_string(), "adr");
}
#[test]
fn as_str_returns_inner() {
let k = RecordKind::new("ddr").unwrap();
assert_eq!(k.as_str(), "ddr");
}
#[test]
fn from_str_accepts_valid_kind() {
let k: RecordKind = "gddr".parse().unwrap();
assert_eq!(k.as_str(), "gddr");
}
#[test]
fn from_str_rejects_invalid_kind() {
assert!("".parse::<RecordKind>().is_err());
assert!("ADR".parse::<RecordKind>().is_err());
}
#[test]
fn equality_holds_for_same_kind() {
let a = RecordKind::new("adr").unwrap();
let b = RecordKind::new("adr").unwrap();
assert_eq!(a, b);
}
#[test]
fn ordering_is_lexicographic() {
let adr = RecordKind::new("adr").unwrap();
let ddr = RecordKind::new("ddr").unwrap();
assert!(adr < ddr);
}
proptest! {
#[test]
fn prop_strategy_always_produces_valid_kinds(k in strategy::record_kind()) {
prop_assert!(RecordKind::new(k.as_str()).is_ok());
}
#[test]
fn prop_display_roundtrips(k in strategy::record_kind()) {
let s = k.to_string();
let parsed: RecordKind = s.parse().unwrap();
prop_assert_eq!(k, parsed);
}
#[test]
fn prop_kind_chars_are_valid(k in strategy::record_kind()) {
let s = k.as_str();
prop_assert!(!s.is_empty());
prop_assert!(s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'));
}
}
}