use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ValidationError(pub String);
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[non_exhaustive]
pub struct Id(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[non_exhaustive]
pub struct UTCDate(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[non_exhaustive]
pub struct Date(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[non_exhaustive]
pub struct State(String);
macro_rules! impl_string_newtype {
($T:ident) => {
impl fmt::Display for $T {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for $T {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for $T {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl AsRef<str> for $T {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for $T {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for $T {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl std::borrow::Borrow<str> for $T {
fn borrow(&self) -> &str {
&self.0
}
}
impl $T {
pub fn into_inner(self) -> String {
self.0
}
}
};
}
impl_string_newtype!(Id);
impl_string_newtype!(UTCDate);
impl_string_newtype!(Date);
impl_string_newtype!(State);
fn validate_id(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError("Id must not be empty".into()));
}
if s.len() > 255 {
return Err(ValidationError(format!(
"Id exceeds 255 bytes (got {})",
s.len()
)));
}
for ch in s.chars() {
let b = ch as u32;
if !(0x21..=0x7E).contains(&b) || b == 0x22 {
return Err(ValidationError(format!(
"Id contains invalid character {:?} (U+{b:04X})",
ch
)));
}
}
Ok(())
}
fn validate_utcdate(s: &str) -> Result<(), ValidationError> {
if s.len() != 20 {
return Err(ValidationError(format!(
"UTCDate must be exactly 20 characters (YYYY-MM-DDTHH:MM:SSZ), got {:?}",
s
)));
}
let b = s.as_bytes();
if b[4] != b'-'
|| b[7] != b'-'
|| b[10] != b'T'
|| b[13] != b':'
|| b[16] != b':'
|| b[19] != b'Z'
{
return Err(ValidationError(format!(
"UTCDate has wrong structure, expected YYYY-MM-DDTHH:MM:SSZ, got {:?}",
s
)));
}
for &pos in &[0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18] {
if !b[pos].is_ascii_digit() {
return Err(ValidationError(format!(
"UTCDate position {} is not a digit in {:?}",
pos, s
)));
}
}
Ok(())
}
fn validate_state(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError("State must not be empty".into()));
}
Ok(())
}
impl Id {
pub fn new_validated(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_id(&s)?;
Ok(Self(s))
}
}
impl UTCDate {
pub fn new_validated(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_utcdate(&s)?;
Ok(Self(s))
}
}
impl State {
pub fn new_validated(s: impl Into<String>) -> Result<Self, ValidationError> {
let s = s.into();
validate_state(&s)?;
Ok(Self(s))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_serializes_as_plain_string() {
let id = Id("abc123".to_owned());
let json = serde_json::to_string(&id).expect("serialize Id");
assert_eq!(json, "\"abc123\"");
}
#[test]
fn id_deserializes_from_plain_string() {
let id: Id = serde_json::from_str("\"abc123\"").expect("deserialize Id");
assert_eq!(id.as_ref(), "abc123");
}
#[test]
fn utcdate_serializes_as_plain_string() {
let d = UTCDate("2014-10-30T06:12:00Z".to_owned());
let json = serde_json::to_string(&d).expect("serialize UTCDate");
assert_eq!(json, "\"2014-10-30T06:12:00Z\"");
}
#[test]
fn state_serializes_as_plain_string() {
let s = State("75128aab4b1b".to_owned());
let json = serde_json::to_string(&s).expect("serialize State");
assert_eq!(json, "\"75128aab4b1b\"");
}
#[test]
fn id_from_str() {
let id = Id::from("hello");
assert_eq!(id.as_ref(), "hello");
}
#[test]
fn id_display() {
let id = Id("display-test".to_owned());
assert_eq!(id.to_string(), "display-test");
}
#[test]
fn id_as_ref_str() {
let id = Id("ref-test".to_owned());
assert_eq!(id.as_ref(), "ref-test");
}
#[test]
fn state_round_trip() {
let s = State("75128aab4b1b".to_owned());
let json = serde_json::to_string(&s).expect("serialize");
let s2: State = serde_json::from_str(&json).expect("deserialize");
assert_eq!(s, s2);
}
#[test]
fn date_accepts_non_utc_offset() {
let d = Date("2014-10-30T14:12:00+08:00".to_owned());
let json = serde_json::to_string(&d).expect("serialize Date");
assert_eq!(json, "\"2014-10-30T14:12:00+08:00\"");
let d2: Date = serde_json::from_str(&json).expect("deserialize Date");
assert_eq!(d, d2);
}
#[test]
fn id_new_validated_empty_fails() {
let err = Id::new_validated("").unwrap_err();
assert!(err.0.contains("empty"), "error must mention 'empty': {err}");
}
#[test]
fn id_new_validated_space_fails() {
let err = Id::new_validated("has space").unwrap_err();
assert!(err.0.contains("invalid character"), "{err}");
}
#[test]
fn id_new_validated_dquote_fails() {
let err = Id::new_validated("has\"quote").unwrap_err();
assert!(err.0.contains("invalid character"), "{err}");
}
#[test]
fn id_new_validated_control_char_fails() {
let err = Id::new_validated("has\x01ctrl").unwrap_err();
assert!(err.0.contains("invalid character"), "{err}");
}
#[test]
fn id_new_validated_too_long_fails() {
let long = "a".repeat(256);
assert!(Id::new_validated(long).is_err());
}
#[test]
fn id_new_validated_valid_succeeds() {
let id = Id::new_validated("abc123-_ABC").expect("valid Id must succeed");
assert_eq!(id.as_ref(), "abc123-_ABC");
}
#[test]
fn id_new_validated_max_length_succeeds() {
let id255 = "a".repeat(255);
Id::new_validated(id255).expect("255-byte Id must succeed");
}
#[test]
fn utcdate_new_validated_valid_succeeds() {
let d = UTCDate::new_validated("2014-10-30T06:12:00Z").expect("valid UTCDate must succeed");
assert_eq!(d.as_ref(), "2014-10-30T06:12:00Z");
}
#[test]
fn utcdate_new_validated_no_z_fails() {
assert!(UTCDate::new_validated("2014-10-30T06:12:00+00:00").is_err());
}
#[test]
fn utcdate_new_validated_empty_fails() {
assert!(UTCDate::new_validated("").is_err());
}
#[test]
fn utcdate_new_validated_wrong_length_fails() {
assert!(UTCDate::new_validated("2014-10-30").is_err());
assert!(UTCDate::new_validated("2014-10-30T06:12:00.000Z").is_err());
}
#[test]
fn utcdate_new_validated_non_digit_fails() {
assert!(UTCDate::new_validated("XXXX-10-30T06:12:00Z").is_err());
}
#[test]
fn state_new_validated_empty_fails() {
let err = State::new_validated("").unwrap_err();
assert!(err.0.contains("empty"), "{err}");
}
#[test]
fn state_new_validated_valid_succeeds() {
let s = State::new_validated("75128aab4b1b").expect("valid State must succeed");
assert_eq!(s.as_ref(), "75128aab4b1b");
}
#[test]
fn validation_error_implements_error() {
let e = Id::new_validated("").unwrap_err();
let _: &dyn std::error::Error = &e;
assert!(!e.to_string().is_empty(), "error message must not be empty");
assert_eq!(format!("{e}"), e.0, "Display must show the inner message");
}
}