watermelon_proto/headers/
name.rs1use alloc::string::String;
2use core::{
3 fmt::{self, Display},
4 ops::Deref,
5};
6use unicase::UniCase;
7
8use bytestring::ByteString;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub struct HeaderName(UniCase<ByteString>);
27
28impl HeaderName {
29 pub const MESSAGE_ID: Self = Self::new_internal("Nats-Msg-Id");
31 pub const EXPECTED_STREAM: Self = Self::new_internal("Nats-Expected-Stream");
33 pub const EXPECTED_LAST_MESSAGE_ID: Self = Self::new_internal("Nats-Expected-Last-Msg-Id");
35 pub const EXPECTED_LAST_SEQUENCE: Self = Self::new_internal("Nats-Expected-Last-Sequence");
37 pub const ROLLUP: Self = Self::new_internal("Nats-Rollup");
39
40 pub const STREAM: Self = Self::new_internal("Nats-Stream");
42 pub const SUBJECT: Self = Self::new_internal("Nats-Subject");
44 pub const SEQUENCE: Self = Self::new_internal("Nats-Sequence");
46 pub const LAST_SEQUENCE: Self = Self::new_internal("Nats-Last-Sequence");
48 pub const TIMESTAMP: Self = Self::new_internal("Nats-Time-Stamp");
50
51 pub const STREAM_SOURCE: Self = Self::new_internal("Nats-Stream-Source");
53
54 pub const MESSAGE_SIZE: Self = Self::new_internal("Nats-Msg-Size");
56
57 #[must_use]
63 pub fn from_static(value: &'static str) -> Self {
64 Self::try_from(ByteString::from_static(value)).expect("invalid HeaderName")
65 }
66
67 #[expect(
80 clippy::missing_panics_doc,
81 reason = "The header validation is only made in debug"
82 )]
83 #[must_use]
84 pub fn from_dangerous_value(value: ByteString) -> Self {
85 if cfg!(debug_assertions) {
86 if let Err(err) = validate_header_name(&value) {
87 panic!("HeaderName {value:?} isn't valid {err:?}");
88 }
89 }
90 Self(UniCase::new(value))
91 }
92
93 const fn new_internal(value: &'static str) -> Self {
94 if value.is_ascii() {
95 Self(UniCase::ascii(ByteString::from_static(value)))
96 } else {
97 Self(UniCase::unicode(ByteString::from_static(value)))
98 }
99 }
100
101 #[must_use]
102 pub fn as_str(&self) -> &str {
103 &self.0
104 }
105}
106
107impl Display for HeaderName {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 Display::fmt(&self.0, f)
110 }
111}
112
113impl TryFrom<ByteString> for HeaderName {
114 type Error = HeaderNameValidateError;
115
116 fn try_from(value: ByteString) -> Result<Self, Self::Error> {
117 validate_header_name(&value)?;
118 Ok(Self::from_dangerous_value(value))
119 }
120}
121
122impl TryFrom<String> for HeaderName {
123 type Error = HeaderNameValidateError;
124
125 fn try_from(value: String) -> Result<Self, Self::Error> {
126 validate_header_name(&value)?;
127 Ok(Self::from_dangerous_value(value.into()))
128 }
129}
130
131impl From<HeaderName> for ByteString {
132 fn from(value: HeaderName) -> Self {
133 value.0.into_inner()
134 }
135}
136
137impl AsRef<[u8]> for HeaderName {
138 fn as_ref(&self) -> &[u8] {
139 self.as_str().as_bytes()
140 }
141}
142
143impl AsRef<str> for HeaderName {
144 fn as_ref(&self) -> &str {
145 self.as_str()
146 }
147}
148
149impl Deref for HeaderName {
150 type Target = str;
151
152 fn deref(&self) -> &Self::Target {
153 self.as_str()
154 }
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum HeaderNameValidateError {
160 #[error("HeaderName is empty")]
162 Empty,
163 #[error("HeaderName is too long")]
165 TooLong,
166 #[error("HeaderName contained an illegal whitespace character")]
168 IllegalCharacter,
169}
170
171fn validate_header_name(header_name: &str) -> Result<(), HeaderNameValidateError> {
172 if header_name.is_empty() {
173 return Err(HeaderNameValidateError::Empty);
174 }
175
176 if header_name.len() > 64 {
177 return Err(HeaderNameValidateError::TooLong);
179 }
180
181 if header_name.chars().any(|c| c.is_whitespace() || c == ':') {
182 return Err(HeaderNameValidateError::IllegalCharacter);
185 }
186
187 Ok(())
188}
189
190#[cfg(test)]
191mod tests {
192 use core::cmp::Ordering;
193
194 use super::HeaderName;
195
196 #[test]
197 fn eq() {
198 let cased = HeaderName::from_static("Nats-Message-Id");
199 let lowercase = HeaderName::from_static("nats-message-id");
200 assert_eq!(cased, lowercase);
201 assert_eq!(cased.cmp(&lowercase), Ordering::Equal);
202 }
203}