Skip to main content

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}