watermelon_proto/headers/
name.rs1use alloc::string::String;
2use core::{
3 fmt::{self, Display},
4 ops::Deref,
5 str::FromStr,
6};
7use unicase::UniCase;
8
9use bytestring::ByteString;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
25pub struct HeaderName(UniCase<ByteString>);
26
27impl HeaderName {
28 pub const MESSAGE_ID: Self = Self::new_internal("Nats-Msg-Id");
30 pub const EXPECTED_STREAM: Self = Self::new_internal("Nats-Expected-Stream");
32 pub const EXPECTED_LAST_MESSAGE_ID: Self = Self::new_internal("Nats-Expected-Last-Msg-Id");
34 pub const EXPECTED_LAST_SEQUENCE: Self = Self::new_internal("Nats-Expected-Last-Sequence");
36 pub const ROLLUP: Self = Self::new_internal("Nats-Rollup");
38
39 pub const STREAM: Self = Self::new_internal("Nats-Stream");
41 pub const SUBJECT: Self = Self::new_internal("Nats-Subject");
43 pub const SEQUENCE: Self = Self::new_internal("Nats-Sequence");
45 pub const LAST_SEQUENCE: Self = Self::new_internal("Nats-Last-Sequence");
47 pub const TIMESTAMP: Self = Self::new_internal("Nats-Time-Stamp");
49
50 pub const STREAM_SOURCE: Self = Self::new_internal("Nats-Stream-Source");
52
53 pub const MESSAGE_SIZE: Self = Self::new_internal("Nats-Msg-Size");
55
56 #[must_use]
62 pub fn from_static(value: &'static str) -> Self {
63 Self::try_from(ByteString::from_static(value)).expect("invalid HeaderName")
64 }
65
66 #[expect(
79 clippy::missing_panics_doc,
80 reason = "The header validation is only made in debug"
81 )]
82 #[must_use]
83 pub fn from_dangerous_value(value: ByteString) -> Self {
84 if cfg!(debug_assertions)
85 && let Err(err) = validate_header_name(&value)
86 {
87 panic!("HeaderName {value:?} isn't valid {err:?}");
88 }
89 Self(UniCase::new(value))
90 }
91
92 const fn new_internal(value: &'static str) -> Self {
93 if value.is_ascii() {
94 Self(UniCase::ascii(ByteString::from_static(value)))
95 } else {
96 Self(UniCase::unicode(ByteString::from_static(value)))
97 }
98 }
99
100 #[must_use]
101 pub fn as_str(&self) -> &str {
102 &self.0
103 }
104}
105
106impl Display for HeaderName {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 Display::fmt(&self.0, f)
109 }
110}
111
112impl TryFrom<ByteString> for HeaderName {
113 type Error = HeaderNameValidateError;
114
115 fn try_from(value: ByteString) -> Result<Self, Self::Error> {
116 validate_header_name(&value)?;
117 Ok(Self::from_dangerous_value(value))
118 }
119}
120
121impl FromStr for HeaderName {
122 type Err = HeaderNameValidateError;
123
124 fn from_str(value: &str) -> Result<Self, Self::Err> {
125 validate_header_name(value)?;
126 Ok(Self::from_dangerous_value(value.into()))
127 }
128}
129
130impl TryFrom<String> for HeaderName {
131 type Error = HeaderNameValidateError;
132
133 fn try_from(value: String) -> Result<Self, Self::Error> {
134 validate_header_name(&value)?;
135 Ok(Self::from_dangerous_value(value.into()))
136 }
137}
138
139impl From<HeaderName> for ByteString {
140 fn from(value: HeaderName) -> Self {
141 value.0.into_inner()
142 }
143}
144
145impl AsRef<[u8]> for HeaderName {
146 fn as_ref(&self) -> &[u8] {
147 self.as_str().as_bytes()
148 }
149}
150
151impl AsRef<str> for HeaderName {
152 fn as_ref(&self) -> &str {
153 self.as_str()
154 }
155}
156
157impl Deref for HeaderName {
158 type Target = str;
159
160 fn deref(&self) -> &Self::Target {
161 self.as_str()
162 }
163}
164
165#[derive(Debug, thiserror::Error)]
167pub enum HeaderNameValidateError {
168 #[error("HeaderName is empty")]
170 Empty,
171 #[error("HeaderName contained an illegal character")]
173 IllegalCharacter,
174}
175
176fn validate_header_name(header_name: &str) -> Result<(), HeaderNameValidateError> {
177 if header_name.is_empty() {
178 return Err(HeaderNameValidateError::Empty);
179 }
180
181 for b in header_name.bytes() {
182 match b {
183 b if b >= 0x80 => return Err(HeaderNameValidateError::IllegalCharacter),
189 b'\r' | b'\n' | b':' => return Err(HeaderNameValidateError::IllegalCharacter),
191 _ => {}
192 }
193 }
194
195 Ok(())
196}
197
198#[cfg(test)]
199mod tests {
200 use core::cmp::Ordering;
201 use core::str::FromStr;
202
203 use claims::assert_matches;
204
205 use super::{HeaderName, HeaderNameValidateError};
206
207 #[test]
208 fn eq() {
209 let cased = HeaderName::from_static("Nats-Message-Id");
210 let lowercase = HeaderName::from_static("nats-message-id");
211 assert_eq!(cased, lowercase);
212 assert_eq!(cased.cmp(&lowercase), Ordering::Equal);
213 }
214
215 #[test]
216 fn valid_header_names() {
217 let names = ["Nats-Msg-Id", "a", "X-Custom-Header", "Header Name"];
218 for name in names {
219 let header_name = HeaderName::from_str(name).unwrap();
220 assert_eq!(header_name.as_str(), name);
221 }
222 }
223
224 #[test]
225 fn invalid_header_names() {
226 assert_matches!(
227 HeaderName::from_str(""),
228 Err(HeaderNameValidateError::Empty)
229 );
230
231 let names = [":", "name:", "na:me", "name\r", "na\nme", "nàme"];
232 for name in names {
233 assert_matches!(
234 HeaderName::from_str(name),
235 Err(HeaderNameValidateError::IllegalCharacter),
236 "{name:?}"
237 );
238 }
239 }
240}