1use std::collections::HashMap;
2
3use crate::body::Body;
4use crate::value::Value;
5
6#[derive(Debug, Clone)]
8pub struct Message {
9 pub headers: HashMap<String, Value>,
16 pub body: Body,
18}
19
20impl Default for Message {
21 fn default() -> Self {
22 Self {
23 headers: HashMap::new(),
24 body: Body::Empty,
25 }
26 }
27}
28
29impl Message {
30 pub fn new(body: impl Into<Body>) -> Self {
32 Self {
33 headers: HashMap::new(),
34 body: body.into(),
35 }
36 }
37
38 pub fn header(&self, key: &str) -> Option<&Value> {
40 self.headers.get(key)
41 }
42
43 pub fn header_ic(&self, key: &str) -> Option<&Value> {
49 if let Some(value) = self.headers.get(key) {
50 return Some(value);
51 }
52
53 self.headers
54 .iter()
55 .filter(|(k, _)| k.eq_ignore_ascii_case(key))
56 .min_by(|a, b| a.0.cmp(b.0))
57 .map(|(_, v)| v)
58 }
59
60 pub fn set_header(&mut self, key: impl Into<String>, value: impl Into<Value>) {
66 let key = key.into();
67 let value = value.into();
68 self.headers.retain(|k, _| !k.eq_ignore_ascii_case(&key));
69 self.headers.insert(key, value);
70 }
71
72 pub fn set_header_normalized(&mut self, key: impl Into<String>, value: impl Into<Value>) {
77 let key = key.into().to_ascii_lowercase();
78 let value = value.into();
79 self.headers.retain(|k, _| !k.eq_ignore_ascii_case(&key));
80 self.headers.insert(key, value);
81 }
82
83 pub fn remove_header(&mut self, key: &str) -> Option<Value> {
89 let removed = self.headers.remove(key);
90 self.headers.retain(|k, _| !k.eq_ignore_ascii_case(key));
91 removed
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_message_default() {
101 let msg = Message::default();
102 assert!(msg.body.is_empty());
103 assert!(msg.headers.is_empty());
104 }
105
106 #[test]
107 fn test_message_new_with_body() {
108 let msg = Message::new("hello");
109 assert_eq!(msg.body.as_text(), Some("hello"));
110 }
111
112 #[test]
113 fn test_message_headers() {
114 let mut msg = Message::default();
115 msg.set_header("key", Value::String("value".into()));
116 assert_eq!(msg.header("key"), Some(&Value::String("value".into())));
117 assert_eq!(msg.header("missing"), None);
118 }
119
120 #[test]
121 fn test_header_ic_case_insensitive() {
122 let mut msg = Message::default();
123 msg.set_header("Content-Type", Value::String("application/json".into()));
124 assert_eq!(
125 msg.header_ic("content-type"),
126 Some(&Value::String("application/json".into()))
127 );
128 assert_eq!(
129 msg.header_ic("CONTENT-TYPE"),
130 Some(&Value::String("application/json".into()))
131 );
132 }
133
134 #[test]
135 fn test_set_header_normalized_lowercases_key() {
136 let mut msg = Message::default();
137 msg.set_header_normalized("X-Request-Id", Value::String("abc-123".into()));
138
139 assert!(msg.headers.contains_key("x-request-id"));
140 assert_eq!(
141 msg.header("x-request-id"),
142 Some(&Value::String("abc-123".into()))
143 );
144 }
145
146 #[test]
147 fn header_exact_lookup_stays_case_sensitive() {
148 let mut msg = Message::default();
149 msg.set_header("Content-Type", "application/xml");
150
151 assert_eq!(msg.header("content-type"), None);
152 assert_eq!(
153 msg.header("Content-Type"),
154 Some(&Value::String("application/xml".into()))
155 );
156 }
157
158 #[test]
159 fn set_header_case_variants_replace_each_other() {
160 let mut msg = Message::default();
161 msg.set_header("Content-Type", "a/xml");
162 msg.set_header("content-type", "application/json");
163
164 assert_eq!(msg.headers.len(), 1);
165 assert!(msg.headers.contains_key("content-type"));
166 assert_eq!(
167 msg.header_ic("CONTENT-TYPE"),
168 Some(&Value::String("application/json".into()))
169 );
170 }
171
172 #[test]
173 fn set_header_mixed_case_replaces_existing() {
174 let mut msg = Message::default();
175 msg.set_header("content-type", "text/plain");
176 msg.set_header("CoNtEnT-TyPe", "text/csv");
177
178 assert_eq!(msg.headers.len(), 1);
179 assert!(msg.headers.contains_key("CoNtEnT-TyPe"));
180 }
181
182 #[test]
183 fn remove_header_is_case_insensitive() {
184 let mut msg = Message::default();
185 msg.set_header("X-Request-Id", "abc");
186
187 assert_eq!(msg.remove_header("x-request-id"), None);
190 assert_eq!(msg.header("X-Request-Id"), None);
191 assert_eq!(msg.header_ic("x-request-id"), None);
192 }
193
194 #[test]
195 fn remove_header_returns_none_without_exact_case_match() {
196 let mut msg = Message::default();
197 msg.set_header("Other", "keep");
198 msg.headers.insert("X-Dup".into(), 1.into());
199
200 assert_eq!(msg.remove_header("x-dup"), None);
201 assert!(!msg.headers.contains_key("X-Dup"));
202 assert_eq!(msg.header("Other"), Some(&Value::String("keep".into())));
203 }
204
205 #[test]
206 fn remove_header_absent_returns_none_leaves_others() {
207 let mut msg = Message::default();
208 msg.set_header("A", "one");
209 msg.set_header("B", 2);
210
211 assert_eq!(msg.remove_header("x-missing"), None);
212 assert_eq!(msg.headers.len(), 2);
213 assert_eq!(msg.header("A"), Some(&Value::String("one".into())));
214 assert_eq!(msg.header("B"), Some(&Value::from(2)));
215 }
216
217 #[test]
218 fn header_ic_duplicate_spellings_resolve_deterministically() {
219 for i in 0..100 {
220 let mut msg = Message::default();
221 if i % 2 == 0 {
222 msg.headers.insert("content-type".into(), "lower".into());
223 msg.headers.insert("Content-Type".into(), "title".into());
224 msg.headers.insert("x-dup".into(), "lower".into());
225 msg.headers.insert("X-Dup".into(), "upper".into());
226 } else {
227 msg.headers.insert("Content-Type".into(), "title".into());
228 msg.headers.insert("content-type".into(), "lower".into());
229 msg.headers.insert("X-Dup".into(), "upper".into());
230 msg.headers.insert("x-dup".into(), "lower".into());
231 }
232
233 assert_eq!(
234 msg.header_ic("Content-Type"),
235 Some(&Value::String("title".into()))
236 );
237 assert_eq!(
238 msg.header_ic("CONTENT-TYPE"),
239 Some(&Value::String("title".into()))
240 );
241 assert_eq!(msg.header_ic("X-DUP"), Some(&Value::String("upper".into())));
242 assert_eq!(
243 msg.header_ic("content-type"),
244 Some(&Value::String("lower".into()))
245 );
246 assert_eq!(msg.header_ic("x-dup"), Some(&Value::String("lower".into())));
247 }
248 }
249
250 #[test]
251 fn set_header_normalized_deduplicates() {
252 let mut msg = Message::default();
253 msg.set_header("X-Request-Id", "old");
254 msg.set_header_normalized("X-Request-Id", "abc");
255
256 assert_eq!(msg.headers.len(), 1);
257 assert!(msg.headers.contains_key("x-request-id"));
258 assert_eq!(
259 msg.header("x-request-id"),
260 Some(&Value::String("abc".into()))
261 );
262 }
263
264 #[test]
265 fn header_iteration_and_json_preserve_spelling() {
266 let mut msg = Message::default();
267 msg.set_header("X-Request-Id", "abc");
268
269 assert!(msg.headers.keys().any(|k| k == "X-Request-Id"));
270 let json = serde_json::to_value(&msg.headers).expect("headers serialize to object");
271 let obj = json.as_object().expect("headers serialize to JSON object");
272 assert_eq!(obj.get("X-Request-Id"), Some(&Value::String("abc".into())));
273 assert!(!obj.contains_key("x-request-id"));
274 }
275
276 #[test]
277 fn raw_map_write_visible_to_header_ic_only() {
278 let mut msg = Message::default();
279 msg.headers.insert("X-Raw".into(), 1.into());
280
281 assert_eq!(msg.header_ic("x-raw"), Some(&Value::from(1)));
282 assert_eq!(msg.header("x-raw"), None);
283 }
284}