use alloc::string::String;
use core::{
fmt::{self, Display},
ops::Deref,
str::FromStr,
};
use unicase::UniCase;
use bytestring::ByteString;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct HeaderName(UniCase<ByteString>);
impl HeaderName {
pub const MESSAGE_ID: Self = Self::new_internal("Nats-Msg-Id");
pub const EXPECTED_STREAM: Self = Self::new_internal("Nats-Expected-Stream");
pub const EXPECTED_LAST_MESSAGE_ID: Self = Self::new_internal("Nats-Expected-Last-Msg-Id");
pub const EXPECTED_LAST_SEQUENCE: Self = Self::new_internal("Nats-Expected-Last-Sequence");
pub const ROLLUP: Self = Self::new_internal("Nats-Rollup");
pub const STREAM: Self = Self::new_internal("Nats-Stream");
pub const SUBJECT: Self = Self::new_internal("Nats-Subject");
pub const SEQUENCE: Self = Self::new_internal("Nats-Sequence");
pub const LAST_SEQUENCE: Self = Self::new_internal("Nats-Last-Sequence");
pub const TIMESTAMP: Self = Self::new_internal("Nats-Time-Stamp");
pub const STREAM_SOURCE: Self = Self::new_internal("Nats-Stream-Source");
pub const MESSAGE_SIZE: Self = Self::new_internal("Nats-Msg-Size");
#[must_use]
pub fn from_static(value: &'static str) -> Self {
Self::try_from(ByteString::from_static(value)).expect("invalid HeaderName")
}
#[expect(
clippy::missing_panics_doc,
reason = "The header validation is only made in debug"
)]
#[must_use]
pub fn from_dangerous_value(value: ByteString) -> Self {
if cfg!(debug_assertions)
&& let Err(err) = validate_header_name(&value)
{
panic!("HeaderName {value:?} isn't valid {err:?}");
}
Self(UniCase::new(value))
}
const fn new_internal(value: &'static str) -> Self {
if value.is_ascii() {
Self(UniCase::ascii(ByteString::from_static(value)))
} else {
Self(UniCase::unicode(ByteString::from_static(value)))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for HeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl TryFrom<ByteString> for HeaderName {
type Error = HeaderNameValidateError;
fn try_from(value: ByteString) -> Result<Self, Self::Error> {
validate_header_name(&value)?;
Ok(Self::from_dangerous_value(value))
}
}
impl FromStr for HeaderName {
type Err = HeaderNameValidateError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
validate_header_name(value)?;
Ok(Self::from_dangerous_value(value.into()))
}
}
impl TryFrom<String> for HeaderName {
type Error = HeaderNameValidateError;
fn try_from(value: String) -> Result<Self, Self::Error> {
validate_header_name(&value)?;
Ok(Self::from_dangerous_value(value.into()))
}
}
impl From<HeaderName> for ByteString {
fn from(value: HeaderName) -> Self {
value.0.into_inner()
}
}
impl AsRef<[u8]> for HeaderName {
fn as_ref(&self) -> &[u8] {
self.as_str().as_bytes()
}
}
impl AsRef<str> for HeaderName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Deref for HeaderName {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[derive(Debug, thiserror::Error)]
pub enum HeaderNameValidateError {
#[error("HeaderName is empty")]
Empty,
#[error("HeaderName contained an illegal character")]
IllegalCharacter,
}
fn validate_header_name(header_name: &str) -> Result<(), HeaderNameValidateError> {
if header_name.is_empty() {
return Err(HeaderNameValidateError::Empty);
}
for b in header_name.bytes() {
match b {
b if b >= 0x80 => return Err(HeaderNameValidateError::IllegalCharacter),
b'\r' | b'\n' | b':' => return Err(HeaderNameValidateError::IllegalCharacter),
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use core::cmp::Ordering;
use core::str::FromStr;
use claims::assert_matches;
use super::{HeaderName, HeaderNameValidateError};
#[test]
fn eq() {
let cased = HeaderName::from_static("Nats-Message-Id");
let lowercase = HeaderName::from_static("nats-message-id");
assert_eq!(cased, lowercase);
assert_eq!(cased.cmp(&lowercase), Ordering::Equal);
}
#[test]
fn valid_header_names() {
let names = ["Nats-Msg-Id", "a", "X-Custom-Header", "Header Name"];
for name in names {
let header_name = HeaderName::from_str(name).unwrap();
assert_eq!(header_name.as_str(), name);
}
}
#[test]
fn invalid_header_names() {
assert_matches!(
HeaderName::from_str(""),
Err(HeaderNameValidateError::Empty)
);
let names = [":", "name:", "na:me", "name\r", "na\nme", "nàme"];
for name in names {
assert_matches!(
HeaderName::from_str(name),
Err(HeaderNameValidateError::IllegalCharacter),
"{name:?}"
);
}
}
}