use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::util::rng::{self, RngError};
pub const MAX_LENGTH: usize = 64;
#[derive(Debug, Clone, Copy, Error)]
#[non_exhaustive]
pub enum SubscriptionIdError {
#[error("subscription id must not be empty")]
Empty,
#[error("subscription id too long: {0} characters (max {MAX_LENGTH})")]
TooLong(usize),
#[error("failed to generate subscription id: {0}")]
Rng(#[from] RngError),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubscriptionId(String);
impl SubscriptionId {
pub fn new<S>(value: S) -> Result<Self, SubscriptionIdError>
where
S: Into<String>,
{
let value = value.into();
Self::validate(&value)?;
Ok(Self(value))
}
pub fn generate() -> Result<Self, SubscriptionIdError> {
let id = rng::random_hex_string::<16>()?;
Ok(Self(id))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
fn validate(value: &str) -> Result<(), SubscriptionIdError> {
if value.is_empty() {
return Err(SubscriptionIdError::Empty);
}
let chars = value.chars().count();
if chars > MAX_LENGTH {
return Err(SubscriptionIdError::TooLong(chars));
}
Ok(())
}
}
impl fmt::Display for SubscriptionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for SubscriptionId {
type Err = SubscriptionIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_owned())
}
}
impl AsRef<str> for SubscriptionId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Serialize for SubscriptionId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for SubscriptionId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Self::new(raw).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_round_trip() {
let id = SubscriptionId::new("abcdef").unwrap();
assert_eq!(id.as_str(), "abcdef");
}
#[test]
fn empty_is_rejected() {
let err = SubscriptionId::new("").unwrap_err();
assert!(matches!(err, SubscriptionIdError::Empty));
}
#[test]
fn too_long_is_rejected() {
let value = "a".repeat(MAX_LENGTH + 1);
let err = SubscriptionId::new(value).unwrap_err();
assert!(matches!(err, SubscriptionIdError::TooLong(_)));
}
#[test]
fn generate_unique() {
let lhs = SubscriptionId::generate().unwrap();
let rhs = SubscriptionId::generate().unwrap();
assert_ne!(lhs, rhs);
assert_eq!(lhs.as_str().len(), 32);
}
#[test]
fn serde_round_trip() {
let id = SubscriptionId::new("query-1").unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, r#""query-1""#);
let parsed: SubscriptionId = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, id);
}
#[test]
fn from_str_works() {
let id: SubscriptionId = "x".parse().unwrap();
assert_eq!(id.as_str(), "x");
}
#[test]
fn accepts_multi_byte_chars_at_limit() {
let value = "ñ".repeat(MAX_LENGTH);
assert_eq!(value.chars().count(), MAX_LENGTH);
assert!(value.len() > MAX_LENGTH, "byte length must exceed cap");
let id = SubscriptionId::new(value.clone()).unwrap();
assert_eq!(id.as_str(), value);
}
#[test]
fn rejects_one_char_above_limit_in_chars() {
let value = "x".repeat(MAX_LENGTH + 1);
let err = SubscriptionId::new(value).unwrap_err();
assert!(matches!(err, SubscriptionIdError::TooLong(n) if n == MAX_LENGTH + 1));
}
}