nula_core/message/
subscription_id.rs1use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use thiserror::Error;
13
14use crate::util::rng::{self, RngError};
15
16pub const MAX_LENGTH: usize = 64;
19
20#[derive(Debug, Clone, Copy, Error)]
22#[non_exhaustive]
23pub enum SubscriptionIdError {
24 #[error("subscription id must not be empty")]
26 Empty,
27 #[error("subscription id too long: {0} characters (max {MAX_LENGTH})")]
29 TooLong(usize),
30 #[error("failed to generate subscription id: {0}")]
32 Rng(#[from] RngError),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct SubscriptionId(String);
40
41impl SubscriptionId {
42 pub fn new<S>(value: S) -> Result<Self, SubscriptionIdError>
49 where
50 S: Into<String>,
51 {
52 let value = value.into();
53 Self::validate(&value)?;
54 Ok(Self(value))
55 }
56
57 pub fn generate() -> Result<Self, SubscriptionIdError> {
63 let id = rng::random_hex_string::<16>()?;
64 Ok(Self(id))
65 }
66
67 #[must_use]
69 pub fn as_str(&self) -> &str {
70 &self.0
71 }
72
73 #[must_use]
75 pub fn into_string(self) -> String {
76 self.0
77 }
78
79 fn validate(value: &str) -> Result<(), SubscriptionIdError> {
80 if value.is_empty() {
81 return Err(SubscriptionIdError::Empty);
82 }
83 let chars = value.chars().count();
87 if chars > MAX_LENGTH {
88 return Err(SubscriptionIdError::TooLong(chars));
89 }
90 Ok(())
91 }
92}
93
94impl fmt::Display for SubscriptionId {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(&self.0)
97 }
98}
99
100impl FromStr for SubscriptionId {
101 type Err = SubscriptionIdError;
102
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 Self::new(s.to_owned())
105 }
106}
107
108impl AsRef<str> for SubscriptionId {
109 fn as_ref(&self) -> &str {
110 &self.0
111 }
112}
113
114impl Serialize for SubscriptionId {
115 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
116 where
117 S: Serializer,
118 {
119 serializer.serialize_str(&self.0)
120 }
121}
122
123impl<'de> Deserialize<'de> for SubscriptionId {
124 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125 where
126 D: Deserializer<'de>,
127 {
128 let raw = String::deserialize(deserializer)?;
129 Self::new(raw).map_err(serde::de::Error::custom)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn new_round_trip() {
139 let id = SubscriptionId::new("abcdef").unwrap();
140 assert_eq!(id.as_str(), "abcdef");
141 }
142
143 #[test]
144 fn empty_is_rejected() {
145 let err = SubscriptionId::new("").unwrap_err();
146 assert!(matches!(err, SubscriptionIdError::Empty));
147 }
148
149 #[test]
150 fn too_long_is_rejected() {
151 let value = "a".repeat(MAX_LENGTH + 1);
152 let err = SubscriptionId::new(value).unwrap_err();
153 assert!(matches!(err, SubscriptionIdError::TooLong(_)));
154 }
155
156 #[test]
157 fn generate_unique() {
158 let lhs = SubscriptionId::generate().unwrap();
159 let rhs = SubscriptionId::generate().unwrap();
160 assert_ne!(lhs, rhs);
161 assert_eq!(lhs.as_str().len(), 32);
162 }
163
164 #[test]
165 fn serde_round_trip() {
166 let id = SubscriptionId::new("query-1").unwrap();
167 let json = serde_json::to_string(&id).unwrap();
168 assert_eq!(json, r#""query-1""#);
169 let parsed: SubscriptionId = serde_json::from_str(&json).unwrap();
170 assert_eq!(parsed, id);
171 }
172
173 #[test]
174 fn from_str_works() {
175 let id: SubscriptionId = "x".parse().unwrap();
176 assert_eq!(id.as_str(), "x");
177 }
178
179 #[test]
180 fn accepts_multi_byte_chars_at_limit() {
181 let value = "ñ".repeat(MAX_LENGTH);
184 assert_eq!(value.chars().count(), MAX_LENGTH);
185 assert!(value.len() > MAX_LENGTH, "byte length must exceed cap");
186 let id = SubscriptionId::new(value.clone()).unwrap();
187 assert_eq!(id.as_str(), value);
188 }
189
190 #[test]
191 fn rejects_one_char_above_limit_in_chars() {
192 let value = "x".repeat(MAX_LENGTH + 1);
193 let err = SubscriptionId::new(value).unwrap_err();
194 assert!(matches!(err, SubscriptionIdError::TooLong(n) if n == MAX_LENGTH + 1));
195 }
196}