Skip to main content

camel_api/
message.rs

1use std::collections::HashMap;
2
3use crate::body::Body;
4use crate::value::Value;
5
6/// A message flowing through the Camel framework.
7#[derive(Debug, Clone)]
8pub struct Message {
9    /// Raw message headers (metadata) — a raw escape hatch.
10    ///
11    /// Direct writes to this map bypass header-key normalization, so the map
12    /// may contain several keys that differ only by ASCII case. Readers that
13    /// need case folding MUST use [`Message::header_ic`] instead of indexing
14    /// this map directly.
15    pub headers: HashMap<String, Value>,
16    /// Message body (payload).
17    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    /// Create a new message with the given body.
31    pub fn new(body: impl Into<Body>) -> Self {
32        Self {
33            headers: HashMap::new(),
34            body: body.into(),
35        }
36    }
37
38    /// Get a header value by key, matching the stored key exactly.
39    pub fn header(&self, key: &str) -> Option<&Value> {
40        self.headers.get(key)
41    }
42
43    /// Get a header value, matching case-insensitively.
44    ///
45    /// An exact-case entry wins. Without one, the value whose stored key sorts
46    /// first by byte order among the ASCII case-insensitive matches is
47    /// returned, so the result does not depend on `HashMap` iteration order.
48    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    /// Set a header value, replacing every ASCII case-insensitive equivalent.
61    ///
62    /// The caller's spelling is inserted verbatim, so iteration and
63    /// serialization expose that spelling. This is O(n) in the number of
64    /// headers.
65    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    /// Set a header value with a normalized lowercase key.
73    ///
74    /// Every ASCII case-insensitive duplicate is removed before the lowercase
75    /// key is inserted.
76    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    /// Remove a header, matching case-insensitively.
84    ///
85    /// Every entry whose key equals `key` ASCII case-insensitively is removed.
86    /// Returns the exact-case entry's value when that entry existed, and
87    /// `None` otherwise — including when only case variants were removed.
88    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        // Spec: only an exact-case entry's value is returned, so a
188        // case-insensitive-only removal yields `None`; the entry is still gone.
189        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}