use std::{borrow::Cow, collections::HashMap};
use bytes::Bytes;
use bytes_utils::Str;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HeaderMap {
inner: HashMap<Str, Bytes>,
}
impl HeaderMap {
#[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<Str>, 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_shared(&self, name: &str) -> Option<Bytes> {
let key = normalize_borrowed(name);
self.inner.get(key.as_ref()).cloned()
}
#[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, 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")
}
pub(crate) fn overwrite_with(&mut self, other: Self) {
if self.inner.is_empty() {
*self = other;
} else {
self.inner.extend(other.inner);
}
}
}
impl<K, V> FromIterator<(K, V)> for HeaderMap
where
K: Into<Str>,
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(s: Str) -> Str {
if s.bytes().any(|b| b.is_ascii_uppercase()) {
Str::from(s.to_ascii_lowercase())
} else {
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 = HeaderMap::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 = HeaderMap::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 = HeaderMap::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 = HeaderMap::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: HeaderMap = [("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 overwrite_with_upserts_key_by_key() {
let mut base: HeaderMap = [("tenant", "acme"), ("x-trace", "handle")]
.into_iter()
.collect();
base.overwrite_with(
[("x-trace", "call"), ("x-request-id", "r-1")]
.into_iter()
.collect(),
);
assert_eq!(base.get_str("x-trace"), Some("call"));
assert_eq!(base.get_str("tenant"), Some("acme"));
assert_eq!(base.get_str("x-request-id"), Some("r-1"));
assert_eq!(base.len(), 3);
}
#[test]
fn overwrite_with_moves_the_whole_map_over_an_empty_one() {
let mut call = HeaderMap::new();
call.insert("x-trace", "call");
let mut base = HeaderMap::new();
base.overwrite_with(call);
assert_eq!(base.get_str("x-trace"), Some("call"));
let mut kept = HeaderMap::new();
kept.insert("tenant", "acme");
kept.overwrite_with(HeaderMap::new());
assert_eq!(kept.get_str("tenant"), Some("acme"));
}
#[test]
fn iter_yields_normalized_keys() {
let mut h = HeaderMap::new();
h.insert("Foo", "1");
let pairs: Vec<_> = h.iter().collect();
assert_eq!(pairs, vec![("foo", b"1".as_slice())]);
}
}