1use serde::{Deserialize, Deserializer, Serialize, Serializer};
18use smol_str::SmolStr;
19use std::fmt;
20use std::hash::{Hash, Hasher};
21
22#[derive(Debug, Clone)]
25pub struct HeaderName {
26 original: SmolStr,
27 lower: SmolStr,
28}
29
30impl Serialize for HeaderName {
31 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
32 serializer.serialize_str(&self.original)
33 }
34}
35
36impl<'de> Deserialize<'de> for HeaderName {
37 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
38 let s = SmolStr::deserialize(deserializer)?;
39 Ok(HeaderName::new(s))
40 }
41}
42
43impl HeaderName {
44 pub const fn new_static(original: &'static str, lower: &'static str) -> Self {
49 Self {
50 original: SmolStr::new_static(original),
51 lower: SmolStr::new_static(lower),
52 }
53 }
54
55 pub fn new(name: impl Into<SmolStr>) -> Self {
58 let original: SmolStr = name.into();
59 let lower = if original.bytes().all(|b| !b.is_ascii_uppercase()) {
60 original.clone()
61 } else {
62 SmolStr::from(original.to_ascii_lowercase())
63 };
64 Self { original, lower }
65 }
66
67 pub fn original(&self) -> &str {
69 &self.original
70 }
71
72 pub fn as_str(&self) -> &str {
74 &self.lower
75 }
76}
77
78impl PartialEq for HeaderName {
79 fn eq(&self, other: &Self) -> bool {
80 self.lower == other.lower
81 }
82}
83
84impl Eq for HeaderName {}
85
86impl Hash for HeaderName {
87 fn hash<H: Hasher>(&self, state: &mut H) {
88 self.lower.hash(state);
89 }
90}
91
92impl fmt::Display for HeaderName {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str(&self.original)
95 }
96}
97
98impl From<&str> for HeaderName {
99 fn from(value: &str) -> Self {
100 Self::new(value)
101 }
102}
103
104impl From<String> for HeaderName {
105 fn from(value: String) -> Self {
106 Self::new(SmolStr::from(value))
107 }
108}
109
110impl From<SmolStr> for HeaderName {
111 fn from(value: SmolStr) -> Self {
112 Self::new(value)
113 }
114}
115
116impl From<&HeaderName> for HeaderName {
117 fn from(value: &HeaderName) -> Self {
118 value.clone()
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn case_insensitive_equality_preserves_original() {
128 let a = HeaderName::new("Authorization");
129 let b = HeaderName::new("authorization");
130 let c = HeaderName::new("AUTHORIZATION");
131 assert_eq!(a, b);
132 assert_eq!(b, c);
133 assert_eq!(a.original(), "Authorization");
134 assert_eq!(b.original(), "authorization");
135 assert_eq!(c.original(), "AUTHORIZATION");
136 }
137
138 #[test]
139 fn lowercase_input_avoids_extra_allocation() {
140 let n = HeaderName::new("content-type");
141 assert_eq!(n.original(), "content-type");
142 assert_eq!(n.as_str(), "content-type");
143 }
144
145 #[test]
146 fn display_uses_original_case() {
147 let n = HeaderName::new("X-Session-Id");
148 assert_eq!(format!("{n}"), "X-Session-Id");
149 }
150
151 #[test]
152 fn hash_matches_equality_under_case_change() {
153 use std::collections::hash_map::DefaultHasher;
154 fn h(n: &HeaderName) -> u64 {
155 let mut s = DefaultHasher::new();
156 n.hash(&mut s);
157 s.finish()
158 }
159 assert_eq!(h(&HeaderName::new("Foo-Bar")), h(&HeaderName::new("foo-bar")));
160 }
161
162 #[test]
163 fn new_static_round_trips_both_cases() {
164 const N: HeaderName = HeaderName::new_static("X-Request-Id", "x-request-id");
165 assert_eq!(N.original(), "X-Request-Id");
166 assert_eq!(N.as_str(), "x-request-id");
167 assert_eq!(N, HeaderName::new("X-REQUEST-ID"));
168 }
169}