use core::fmt;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Smit(String);
impl Smit {
pub const MAX_CHARS: usize = 48;
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Smit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl core::str::FromStr for Smit {
type Err = SmitError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
if text.trim().is_empty() {
return Err(SmitError::Empty);
}
let chars = text.chars().count();
if chars > Self::MAX_CHARS {
return Err(SmitError::TooLong { chars });
}
if text.chars().any(char::is_control) {
return Err(SmitError::Unprintable);
}
Ok(Self(text.to_string()))
}
}
impl<'de> Deserialize<'de> for Smit {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SmitError {
TooLong {
chars: usize,
},
Unprintable,
Empty,
}
impl fmt::Display for SmitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLong { chars } => write!(
f,
"a smit is at most {} characters; this one is {chars}",
Smit::MAX_CHARS
),
Self::Unprintable => {
f.write_str("a smit may not contain a control character, an escape included")
}
Self::Empty => f.write_str("a smit may not be empty"),
}
}
}
impl core::error::Error for SmitError {}
#[cfg(test)]
mod tests {
use super::super::{Envelope, Request, SelectorSpec};
use super::*;
#[test]
fn a_smit_is_validated_when_it_is_deserialized_not_only_when_parsed() {
for bad in [
r#""\u001b[2Jgone""#.to_string(), r#""a\nb""#.to_string(), r#""""#.to_string(), r#"" ""#.to_string(), format!(r#""{}""#, "x".repeat(Smit::MAX_CHARS + 1)), ] {
assert!(
serde_json::from_str::<Smit>(&bad).is_err(),
"a daemon must refuse this on the wire: {bad}"
);
}
assert!(serde_json::from_str::<Smit>(r#""\u25b2 main@a1b2c3""#).is_ok());
}
#[test]
fn a_smit_travels_as_a_bare_string() {
let smit: Smit = "\u{25b2} main@a1b2c3".parse().expect("valid");
let json = serde_json::to_string(&smit).unwrap();
assert_eq!(json, "\"\u{25b2} main@a1b2c3\"");
assert_eq!(serde_json::from_str::<Smit>(&json).unwrap(), smit);
}
#[test]
fn a_smit_is_capped_in_characters_not_bytes() {
let cjk = "\u{7f8a}".repeat(Smit::MAX_CHARS);
assert_eq!(cjk.len(), Smit::MAX_CHARS * 3);
assert!(cjk.parse::<Smit>().is_ok(), "{cjk}");
assert_eq!(
"x".repeat(Smit::MAX_CHARS + 1).parse::<Smit>(),
Err(SmitError::TooLong {
chars: Smit::MAX_CHARS + 1
})
);
}
#[test]
fn a_smit_is_stored_exactly_as_it_arrived() {
let padded: Smit = " main@a1b2c3 ".parse().expect("valid");
assert_eq!(padded.as_str(), " main@a1b2c3 ");
assert_eq!(padded.to_string(), " main@a1b2c3 ");
}
#[test]
fn v1_fixture_still_deserializes() {
let fixture = r#"{"id":7,"deadline_ms":null,"body":{"kind":"stop","selector":{"kind":"name","value":"web"}}}"#;
let env: Envelope = serde_json::from_str(fixture).unwrap();
assert_eq!(env.id, 7);
assert!(matches!(
env.body,
Request::Stop { selector: SelectorSpec::Name(ref n) } if n == "web"
));
}
}