async_nats/subject.rs
1use bytes::Bytes;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::ops::Deref;
5use std::str::{from_utf8, Utf8Error};
6
7/// Error type for subject validation failures.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum SubjectError {
10 /// The subject format is invalid (contains whitespace, starts/ends with `.`,
11 /// has consecutive dots, or is empty).
12 InvalidFormat,
13}
14
15impl fmt::Display for SubjectError {
16 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
17 match self {
18 SubjectError::InvalidFormat => write!(f, "invalid subject format"),
19 }
20 }
21}
22
23impl std::error::Error for SubjectError {}
24
25/// A `Subject` is an immutable string type that guarantees valid UTF-8 contents.
26#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
27pub struct Subject {
28 bytes: Bytes,
29}
30
31impl Subject {
32 /// Creates a new `Subject` from Bytes.
33 ///
34 /// # Safety
35 /// Function is unsafe because it does not check if the bytes are valid UTF-8.
36 ///
37 /// # Examples
38 ///
39 /// ```
40 /// use async_nats::Subject;
41 /// use bytes::Bytes;
42 ///
43 /// let bytes = Bytes::from_static(b"Static string");
44 ///
45 /// let subject = unsafe { Subject::from_bytes_unchecked(bytes) };
46 /// assert_eq!(subject.as_str(), "Static string");
47 /// ```
48 pub const unsafe fn from_bytes_unchecked(bytes: Bytes) -> Self {
49 Subject { bytes }
50 }
51
52 /// Creates a new `Subject` from a static string.
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// use async_nats::Subject;
58 ///
59 /// let subject = Subject::from_static("Static string");
60 /// assert_eq!(subject.as_str(), "Static string");
61 /// ```
62 pub const fn from_static(input: &'static str) -> Self {
63 Subject {
64 bytes: Bytes::from_static(input.as_bytes()),
65 }
66 }
67
68 /// Creates a new `Subject` from a UTF-8 encoded byte vector.
69 ///
70 /// Returns an error if the input is not valid UTF-8.
71 ///
72 /// # Examples
73 ///
74 /// ```
75 /// use async_nats::Subject;
76 ///
77 /// let utf8_input = vec![72, 101, 108, 108, 111]; // "Hello" in UTF-8
78 /// let subject = Subject::from_utf8(utf8_input).unwrap();
79 /// assert_eq!(subject.as_ref(), "Hello");
80 /// ```
81 pub fn from_utf8<T>(input: T) -> Result<Self, Utf8Error>
82 where
83 T: Into<Bytes>,
84 {
85 let bytes = input.into();
86 from_utf8(bytes.as_ref())?;
87
88 Ok(Subject { bytes })
89 }
90
91 /// Extracts a string slice containing the entire `Subject`.
92 ///
93 /// # Examples
94 ///
95 /// Basic usage:
96 ///
97 /// ```
98 /// use async_nats::Subject;
99 ///
100 /// let s = Subject::from("foo");
101 /// assert_eq!("foo", s.as_str());
102 /// ```
103 #[inline]
104 pub fn as_str(&self) -> &str {
105 self
106 }
107
108 /// Turns the `Subject` into a `String`, consuming it.
109 ///
110 /// Note that this function is not implemented as `From<Subject> for String` as the conversion
111 /// from the underlying type could involve an allocation. If the `Subject` is owned data, it
112 /// will not allocate, but if it was constructed from borrowed data, it will.
113 ///
114 /// # Examples
115 ///
116 /// ```
117 /// use async_nats::Subject;
118 ///
119 /// let s = Subject::from("foo");
120 /// let sub = s.into_string();
121 /// ```
122 pub fn into_string(self) -> String {
123 // SAFETY: We have guaranteed that the bytes in the `Subject` struct are valid UTF-8.
124 unsafe { String::from_utf8_unchecked(self.bytes.into()) }
125 }
126
127 /// Returns `true` if this subject follows NATS subject rules.
128 ///
129 /// A valid subject must:
130 /// - Not be empty
131 /// - Not start or end with `.`
132 /// - Not contain consecutive dots (`..`)
133 /// - Not contain whitespace (space, tab, CR, LF)
134 #[inline]
135 pub fn is_valid(&self) -> bool {
136 crate::is_valid_subject(self)
137 }
138
139 /// Creates a new `Subject` from a string, validating it follows NATS subject rules.
140 ///
141 /// Returns an error if the subject is invalid.
142 ///
143 /// # Examples
144 ///
145 /// ```
146 /// use async_nats::Subject;
147 ///
148 /// let subject = Subject::validated("events.data").unwrap();
149 /// assert_eq!(subject.as_str(), "events.data");
150 ///
151 /// assert!(Subject::validated("invalid subject").is_err());
152 /// assert!(Subject::validated("").is_err());
153 /// assert!(Subject::validated(".invalid").is_err());
154 /// ```
155 pub fn validated(s: impl AsRef<str>) -> Result<Subject, SubjectError> {
156 let s = s.as_ref();
157 if !crate::is_valid_subject(s) {
158 return Err(SubjectError::InvalidFormat);
159 }
160 Ok(Subject::from(s))
161 }
162
163 /// Creates a new validated `Subject` from a static string with compile-time validation.
164 ///
165 /// This function validates the subject at compile time using const panics.
166 /// Invalid subjects will cause a compilation error.
167 ///
168 /// # Examples
169 ///
170 /// ```
171 /// use async_nats::Subject;
172 ///
173 /// const SUBJECT: Subject = Subject::from_static_validated("events.data");
174 /// ```
175 ///
176 /// The following will fail to compile:
177 /// ```compile_fail
178 /// use async_nats::Subject;
179 /// const INVALID: Subject = Subject::from_static_validated("invalid subject");
180 /// ```
181 pub const fn from_static_validated(subject: &'static str) -> Self {
182 let bytes = subject.as_bytes();
183 let len = bytes.len();
184
185 if len == 0 {
186 panic!("subject cannot be empty");
187 }
188
189 if bytes[0] == b'.' {
190 panic!("subject cannot start with '.'");
191 }
192
193 if bytes[len - 1] == b'.' {
194 panic!("subject cannot end with '.'");
195 }
196
197 let mut i = 0;
198 while i < len {
199 let c = bytes[i];
200 if c == b' ' || c == b'\t' || c == b'\r' || c == b'\n' {
201 panic!("subject cannot contain whitespace (space, tab, CR, LF)");
202 }
203 if c == b'.' && i + 1 < len && bytes[i + 1] == b'.' {
204 panic!("subject cannot contain consecutive dots");
205 }
206 i += 1;
207 }
208
209 Subject::from_static(subject)
210 }
211}
212
213impl<'a> From<&'a str> for Subject {
214 fn from(s: &'a str) -> Self {
215 // Since &str is guaranteed to be valid UTF-8, we can create the Subject instance by copying the contents of the &str
216 Subject {
217 bytes: Bytes::copy_from_slice(s.as_bytes()),
218 }
219 }
220}
221
222impl From<String> for Subject {
223 fn from(s: String) -> Self {
224 // Since the input `String` is guaranteed to be valid UTF-8, we can
225 // safely transmute the internal Vec<u8> to a Bytes value.
226 let bytes = Bytes::from(s.into_bytes());
227 Subject { bytes }
228 }
229}
230
231impl TryFrom<Bytes> for Subject {
232 type Error = Utf8Error;
233
234 fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
235 from_utf8(bytes.as_ref())?;
236 Ok(Subject { bytes })
237 }
238}
239
240impl AsRef<str> for Subject {
241 fn as_ref(&self) -> &str {
242 self
243 }
244}
245
246impl Deref for Subject {
247 type Target = str;
248
249 fn deref(&self) -> &Self::Target {
250 // SAFETY: It is safe to perform an unchecked conversion from bytes to a string slice
251 // here because we guarantee that the bytes in the `Subject` struct are valid UTF-8.
252 // This is enforced during the construction of `Subject` through the `from_static`,
253 // and `from_utf8` methods. In both cases, the input is either checked for UTF-8 validity or
254 // known to be valid UTF-8 as a static string.
255 unsafe { std::str::from_utf8_unchecked(&self.bytes) }
256 }
257}
258
259impl fmt::Display for Subject {
260 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
261 write!(f, "{}", self.as_str())
262 }
263}
264
265pub trait ToSubject {
266 fn to_subject(&self) -> Subject;
267}
268
269impl ToSubject for Subject {
270 fn to_subject(&self) -> Subject {
271 self.to_owned()
272 }
273}
274
275impl ToSubject for &'static str {
276 fn to_subject(&self) -> Subject {
277 Subject::from_static(self)
278 }
279}
280
281impl ToSubject for String {
282 fn to_subject(&self) -> Subject {
283 Subject::from(self.as_str())
284 }
285}
286
287impl Serialize for Subject {
288 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
289 where
290 S: serde::Serializer,
291 {
292 serializer.serialize_str(self.as_str())
293 }
294}
295
296impl<'de> Deserialize<'de> for Subject {
297 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
298 where
299 D: serde::Deserializer<'de>,
300 {
301 Ok(String::deserialize(deserializer)?.into())
302 }
303}