use std::fmt;
use serde::{Deserialize, Serialize};
use crate::domain::error::{DomainError, WireResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct Slot(String);
impl Slot {
pub fn new(value: impl Into<String>) -> WireResult<Self> {
let s = value.into();
validate(&s)?;
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
fn validate(s: &str) -> WireResult<()> {
if s.is_empty() {
return Err(DomainError::InvalidMetadata("slot must not be empty".into()).into());
}
if s.contains('.') {
return Err(DomainError::InvalidMetadata(format!("slot must not contain '.': {s}")).into());
}
Ok(())
}
impl fmt::Display for Slot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Slot {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<Slot> for String {
fn from(value: Slot) -> Self {
value.0
}
}
impl TryFrom<String> for Slot {
type Error = crate::domain::error::WireError;
fn try_from(value: String) -> WireResult<Self> {
Self::new(value)
}
}
impl TryFrom<&str> for Slot {
type Error = crate::domain::error::WireError;
fn try_from(value: &str) -> WireResult<Self> {
Self::new(value.to_owned())
}
}
impl<'de> Deserialize<'de> for Slot {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Self::new(raw).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::error::WireError;
#[test]
fn new_accepts_valid_slot() {
let s = Slot::new("mailbox").expect("valid slot");
assert_eq!(s.as_str(), "mailbox");
assert_eq!(s.to_string(), "mailbox");
}
#[test]
fn new_rejects_empty() {
let err = Slot::new("").expect_err("empty must reject");
assert!(matches!(
err,
WireError::Domain(DomainError::InvalidMetadata(_))
));
}
#[test]
fn new_rejects_dot() {
let err = Slot::new("foo.bar").expect_err("dot must reject");
assert!(matches!(
err,
WireError::Domain(DomainError::InvalidMetadata(_))
));
}
#[test]
fn try_from_str_and_string_roundtrip() {
let from_str: Slot = "news".try_into().expect("ok");
let from_string: Slot = String::from("news").try_into().expect("ok");
assert_eq!(from_str, from_string);
let back: String = from_str.into();
assert_eq!(back, "news");
}
#[test]
fn serde_roundtrip() {
let s = Slot::new("priorities").unwrap();
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, "\"priorities\"");
let parsed: Slot = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, s);
}
#[test]
fn serde_rejects_empty() {
let err = serde_json::from_str::<Slot>("\"\"").expect_err("empty must reject");
assert!(err.to_string().contains("slot must not be empty"));
}
#[test]
fn serde_rejects_dot() {
let err = serde_json::from_str::<Slot>("\"a.b\"").expect_err("dot must reject");
assert!(err.to_string().contains("must not contain '.'"));
}
#[test]
fn as_ref_str_returns_underlying() {
let s = Slot::new("inbox").unwrap();
assert_eq!(<Slot as AsRef<str>>::as_ref(&s), "inbox");
}
}