use std::{borrow::Cow, collections::HashMap};
use bytes::Bytes;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Headers {
inner: HashMap<String, Bytes>,
}
impl Headers {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(cap: usize) -> Self {
Self {
inner: HashMap::with_capacity(cap),
}
}
pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Bytes>) -> Option<Bytes> {
let key = normalize_owned(name.into());
self.inner.insert(key, value.into())
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&[u8]> {
let key = normalize_borrowed(name);
self.inner.get(key.as_ref()).map(Bytes::as_ref)
}
#[must_use]
pub fn get_str(&self, name: &str) -> Option<&str> {
self.get(name).and_then(|raw| std::str::from_utf8(raw).ok())
}
pub fn remove(&mut self, name: &str) -> Option<Bytes> {
let key = normalize_borrowed(name);
self.inner.remove(key.as_ref())
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
let key = normalize_borrowed(name);
self.inner.contains_key(key.as_ref())
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
self.inner.iter().map(|(k, v)| (k.as_str(), v.as_ref()))
}
#[must_use]
pub fn content_type(&self) -> Option<&str> {
self.get_str("content-type")
}
#[must_use]
pub fn correlation_id(&self) -> Option<&str> {
self.get_str("correlation-id")
}
#[must_use]
pub fn reply_to(&self) -> Option<&str> {
self.get_str("reply-to")
}
#[must_use]
pub fn message_id(&self) -> Option<&str> {
self.get_str("message-id")
}
}
impl<K, V> FromIterator<(K, V)> for Headers
where
K: Into<String>,
V: Into<Bytes>,
{
fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let mut headers = Self::with_capacity(lower);
for (k, v) in iter {
headers.insert(k, v);
}
headers
}
}
fn normalize_owned(mut s: String) -> String {
if s.bytes().any(|b| b.is_ascii_uppercase()) {
s.make_ascii_lowercase();
}
s
}
fn normalize_borrowed(s: &str) -> Cow<'_, str> {
if s.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(s.to_ascii_lowercase())
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_and_get_case_insensitive() {
let mut h = Headers::new();
h.insert("Content-Type", "application/json");
assert_eq!(h.get("content-type"), Some(b"application/json".as_slice()));
assert_eq!(h.get("CONTENT-TYPE"), Some(b"application/json".as_slice()));
assert_eq!(h.get_str("Content-Type"), Some("application/json"));
}
#[test]
fn typed_accessors_return_values() {
let mut h = Headers::new();
h.insert("Content-Type", "application/json");
h.insert("Correlation-Id", "abc-123");
h.insert("Reply-To", "responses.inbox");
h.insert("Message-Id", "msg-1");
assert_eq!(h.content_type(), Some("application/json"));
assert_eq!(h.correlation_id(), Some("abc-123"));
assert_eq!(h.reply_to(), Some("responses.inbox"));
assert_eq!(h.message_id(), Some("msg-1"));
}
#[test]
fn typed_accessor_returns_none_for_non_utf8() {
let mut h = Headers::new();
h.insert("Content-Type", Bytes::from_static(&[0xff, 0xfe]));
assert_eq!(h.content_type(), None);
assert_eq!(h.get("content-type"), Some([0xff, 0xfe].as_slice()));
}
#[test]
fn remove_and_contains() {
let mut h = Headers::new();
h.insert("X-Tenant", "acme");
assert!(h.contains("x-tenant"));
assert_eq!(h.remove("X-TENANT"), Some(Bytes::from_static(b"acme")));
assert!(!h.contains("x-tenant"));
}
#[test]
fn collect_via_from_iterator() {
let h: Headers = [("Foo", "1"), ("Bar", "2")].into_iter().collect();
assert_eq!(h.len(), 2);
assert_eq!(h.get_str("foo"), Some("1"));
assert_eq!(h.get_str("bar"), Some("2"));
}
#[test]
fn iter_yields_normalized_keys() {
let mut h = Headers::new();
h.insert("Foo", "1");
let pairs: Vec<_> = h.iter().collect();
assert_eq!(pairs, vec![("foo", b"1".as_slice())]);
}
}