armature_core/
headers.rs

1//! SmallVec-Based HTTP Header Storage
2//!
3//! This module provides a stack-allocated header map for typical HTTP requests.
4//! Most requests have fewer than 8-12 headers, so storing them inline avoids
5//! heap allocations entirely.
6//!
7//! ## Performance
8//!
9//! - **Inline storage**: Up to 12 headers stored on stack (no allocation)
10//! - **O(n) lookup**: Linear scan, but N is typically small (<20)
11//! - **Cache-friendly**: Contiguous memory layout
12//! - **Zero heap alloc**: For typical requests
13//!
14//! ## Comparison
15//!
16//! | Operation | HashMap | HeaderMap (SmallVec) |
17//! |-----------|---------|---------------------|
18//! | Insert (first 12) | Heap alloc | Stack only |
19//! | Lookup | O(1) hash | O(n) linear |
20//! | Memory | ~48 bytes min + heap | ~384 bytes inline |
21//! | Cache | Pointer chasing | Contiguous |
22//!
23//! For typical HTTP workloads with <12 headers, SmallVec is faster due to
24//! avoiding allocator overhead and better cache locality.
25
26use smallvec::SmallVec;
27use std::collections::HashMap;
28use std::fmt;
29
30/// Number of headers to store inline (on stack).
31/// 12 headers × 32 bytes = 384 bytes, reasonable stack footprint.
32/// Most HTTP requests have 5-10 headers.
33pub const INLINE_HEADERS: usize = 12;
34
35/// A header name-value pair.
36#[derive(Clone, PartialEq, Eq)]
37pub struct Header {
38    /// Header name (case-insensitive for lookup)
39    pub name: String,
40    /// Header value
41    pub value: String,
42}
43
44impl Header {
45    /// Create a new header
46    #[inline]
47    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
48        Self {
49            name: name.into(),
50            value: value.into(),
51        }
52    }
53
54    /// Check if name matches (case-insensitive)
55    #[inline]
56    pub fn name_eq(&self, name: &str) -> bool {
57        self.name.eq_ignore_ascii_case(name)
58    }
59}
60
61impl fmt::Debug for Header {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "{}: {}", self.name, self.value)
64    }
65}
66
67/// A compact header map using SmallVec for inline storage.
68///
69/// Stores up to 12 headers inline (on the stack), only allocating
70/// on the heap if more headers are added.
71///
72/// # Example
73///
74/// ```rust
75/// use armature_core::headers::HeaderMap;
76///
77/// let mut headers = HeaderMap::new();
78/// headers.insert("Content-Type", "application/json");
79/// headers.insert("Accept", "text/html");
80///
81/// assert_eq!(headers.get("content-type"), Some(&"application/json".to_string()));
82/// assert!(headers.is_inline()); // Still on stack
83/// ```
84#[derive(Clone, Default)]
85pub struct HeaderMap {
86    inner: SmallVec<[Header; INLINE_HEADERS]>,
87}
88
89impl HeaderMap {
90    /// Create a new empty header map.
91    #[inline]
92    pub const fn new() -> Self {
93        Self {
94            inner: SmallVec::new_const(),
95        }
96    }
97
98    /// Create with pre-allocated capacity.
99    ///
100    /// If capacity <= INLINE_HEADERS, no heap allocation occurs.
101    #[inline]
102    pub fn with_capacity(capacity: usize) -> Self {
103        Self {
104            inner: SmallVec::with_capacity(capacity),
105        }
106    }
107
108    /// Check if storage is inline (no heap allocation).
109    #[inline]
110    pub fn is_inline(&self) -> bool {
111        !self.inner.spilled()
112    }
113
114    /// Get the number of headers.
115    #[inline]
116    pub fn len(&self) -> usize {
117        self.inner.len()
118    }
119
120    /// Check if empty.
121    #[inline]
122    pub fn is_empty(&self) -> bool {
123        self.inner.is_empty()
124    }
125
126    /// Get header value by name (case-insensitive).
127    #[inline]
128    pub fn get(&self, name: &str) -> Option<&String> {
129        self.inner
130            .iter()
131            .find(|h| h.name_eq(name))
132            .map(|h| &h.value)
133    }
134
135    /// Get header value by name, with lowercase fallback.
136    ///
137    /// First tries exact match, then lowercase. This handles
138    /// the common case where headers might be stored in different cases.
139    #[inline]
140    pub fn get_ignore_case(&self, name: &str) -> Option<&String> {
141        self.get(name)
142    }
143
144    /// Check if header exists (case-insensitive).
145    #[inline]
146    pub fn contains(&self, name: &str) -> bool {
147        self.inner.iter().any(|h| h.name_eq(name))
148    }
149
150    /// Insert a header, replacing any existing header with same name.
151    ///
152    /// Returns the old value if replaced.
153    #[inline]
154    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) -> Option<String> {
155        let name = name.into();
156        let value = value.into();
157
158        // Check if header exists
159        for h in &mut self.inner {
160            if h.name_eq(&name) {
161                let old = std::mem::replace(&mut h.value, value);
162                return Some(old);
163            }
164        }
165
166        // New header
167        self.inner.push(Header { name, value });
168        None
169    }
170
171    /// Append a header (allows duplicates).
172    ///
173    /// Unlike `insert`, this doesn't replace existing headers.
174    /// Use for headers that can have multiple values (e.g., Set-Cookie).
175    #[inline]
176    pub fn append(&mut self, name: impl Into<String>, value: impl Into<String>) {
177        self.inner.push(Header {
178            name: name.into(),
179            value: value.into(),
180        });
181    }
182
183    /// Remove a header by name (case-insensitive).
184    ///
185    /// Returns the removed value if found.
186    #[inline]
187    pub fn remove(&mut self, name: &str) -> Option<String> {
188        if let Some(pos) = self.inner.iter().position(|h| h.name_eq(name)) {
189            Some(self.inner.remove(pos).value)
190        } else {
191            None
192        }
193    }
194
195    /// Remove all headers with given name (case-insensitive).
196    ///
197    /// Returns number of headers removed.
198    #[inline]
199    pub fn remove_all(&mut self, name: &str) -> usize {
200        let before = self.inner.len();
201        self.inner.retain(|h| !h.name_eq(name));
202        before - self.inner.len()
203    }
204
205    /// Iterate over all headers.
206    #[inline]
207    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
208        self.inner.iter().map(|h| (&h.name, &h.value))
209    }
210
211    /// Iterate over header names.
212    #[inline]
213    pub fn names(&self) -> impl Iterator<Item = &String> {
214        self.inner.iter().map(|h| &h.name)
215    }
216
217    /// Iterate over header values.
218    #[inline]
219    pub fn values(&self) -> impl Iterator<Item = &String> {
220        self.inner.iter().map(|h| &h.value)
221    }
222
223    /// Get all values for a header name (for multi-value headers).
224    #[inline]
225    pub fn get_all(&self, name: &str) -> Vec<&String> {
226        self.inner
227            .iter()
228            .filter(|h| h.name_eq(name))
229            .map(|h| &h.value)
230            .collect()
231    }
232
233    /// Clear all headers.
234    #[inline]
235    pub fn clear(&mut self) {
236        self.inner.clear();
237    }
238
239    /// Extend with headers from iterator.
240    #[inline]
241    pub fn extend<I, K, V>(&mut self, iter: I)
242    where
243        I: IntoIterator<Item = (K, V)>,
244        K: Into<String>,
245        V: Into<String>,
246    {
247        for (k, v) in iter {
248            self.insert(k, v);
249        }
250    }
251
252    /// Convert to HashMap (for compatibility).
253    #[inline]
254    pub fn to_hash_map(&self) -> HashMap<String, String> {
255        self.inner
256            .iter()
257            .map(|h| (h.name.clone(), h.value.clone()))
258            .collect()
259    }
260
261    /// Create from HashMap.
262    #[inline]
263    pub fn from_hash_map(map: HashMap<String, String>) -> Self {
264        let mut headers = Self::with_capacity(map.len());
265        for (k, v) in map {
266            headers.inner.push(Header { name: k, value: v });
267        }
268        headers
269    }
270
271    // ========================================================================
272    // Common Header Accessors
273    // ========================================================================
274
275    /// Get Content-Type header.
276    #[inline]
277    pub fn content_type(&self) -> Option<&String> {
278        self.get("Content-Type")
279    }
280
281    /// Get Content-Length header as usize.
282    #[inline]
283    pub fn content_length(&self) -> Option<usize> {
284        self.get("Content-Length")?.parse().ok()
285    }
286
287    /// Get Accept header.
288    #[inline]
289    pub fn accept(&self) -> Option<&String> {
290        self.get("Accept")
291    }
292
293    /// Get Authorization header.
294    #[inline]
295    pub fn authorization(&self) -> Option<&String> {
296        self.get("Authorization")
297    }
298
299    /// Get User-Agent header.
300    #[inline]
301    pub fn user_agent(&self) -> Option<&String> {
302        self.get("User-Agent")
303    }
304
305    /// Get Host header.
306    #[inline]
307    pub fn host(&self) -> Option<&String> {
308        self.get("Host")
309    }
310
311    /// Get Cookie header.
312    #[inline]
313    pub fn cookie(&self) -> Option<&String> {
314        self.get("Cookie")
315    }
316
317    /// Check if Keep-Alive connection.
318    #[inline]
319    pub fn is_keep_alive(&self) -> bool {
320        self.get("Connection")
321            .map(|v| v.eq_ignore_ascii_case("keep-alive"))
322            .unwrap_or(true) // HTTP/1.1 default is keep-alive
323    }
324
325    /// Check if chunked transfer encoding.
326    #[inline]
327    pub fn is_chunked(&self) -> bool {
328        self.get("Transfer-Encoding")
329            .map(|v| v.contains("chunked"))
330            .unwrap_or(false)
331    }
332
333    /// Set Content-Type header.
334    #[inline]
335    pub fn set_content_type(&mut self, value: impl Into<String>) {
336        self.insert("Content-Type", value);
337    }
338
339    /// Set Content-Length header.
340    #[inline]
341    pub fn set_content_length(&mut self, len: usize) {
342        self.insert("Content-Length", len.to_string());
343    }
344}
345
346impl fmt::Debug for HeaderMap {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        f.debug_map()
349            .entries(self.inner.iter().map(|h| (&h.name, &h.value)))
350            .finish()
351    }
352}
353
354impl<K, V> FromIterator<(K, V)> for HeaderMap
355where
356    K: Into<String>,
357    V: Into<String>,
358{
359    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
360        let iter = iter.into_iter();
361        let (min, max) = iter.size_hint();
362        let mut map = HeaderMap::with_capacity(max.unwrap_or(min));
363        for (k, v) in iter {
364            map.insert(k, v);
365        }
366        map
367    }
368}
369
370impl<'a> IntoIterator for &'a HeaderMap {
371    type Item = (&'a String, &'a String);
372    type IntoIter =
373        std::iter::Map<std::slice::Iter<'a, Header>, fn(&'a Header) -> (&'a String, &'a String)>;
374
375    fn into_iter(self) -> Self::IntoIter {
376        self.inner.iter().map(|h| (&h.name, &h.value))
377    }
378}
379
380impl IntoIterator for HeaderMap {
381    type Item = (String, String);
382    type IntoIter = std::iter::Map<
383        smallvec::IntoIter<[Header; INLINE_HEADERS]>,
384        fn(Header) -> (String, String),
385    >;
386
387    fn into_iter(self) -> Self::IntoIter {
388        self.inner.into_iter().map(|h| (h.name, h.value))
389    }
390}
391
392// Allow HashMap-like indexing
393impl std::ops::Index<&str> for HeaderMap {
394    type Output = String;
395
396    fn index(&self, name: &str) -> &Self::Output {
397        self.get(name).expect("header not found")
398    }
399}
400
401// ============================================================================
402// Conversion from/to HashMap for backwards compatibility
403// ============================================================================
404
405impl From<HashMap<String, String>> for HeaderMap {
406    fn from(map: HashMap<String, String>) -> Self {
407        Self::from_hash_map(map)
408    }
409}
410
411impl From<HeaderMap> for HashMap<String, String> {
412    fn from(map: HeaderMap) -> Self {
413        map.to_hash_map()
414    }
415}
416
417// ============================================================================
418// Tests
419// ============================================================================
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_new_is_inline() {
427        let headers = HeaderMap::new();
428        assert!(headers.is_inline());
429        assert!(headers.is_empty());
430    }
431
432    #[test]
433    fn test_insert_and_get() {
434        let mut headers = HeaderMap::new();
435        headers.insert("Content-Type", "application/json");
436        headers.insert("Accept", "text/html");
437
438        assert_eq!(headers.len(), 2);
439        assert_eq!(
440            headers.get("Content-Type"),
441            Some(&"application/json".to_string())
442        );
443        assert_eq!(
444            headers.get("content-type"),
445            Some(&"application/json".to_string())
446        ); // case insensitive
447    }
448
449    #[test]
450    fn test_insert_replaces() {
451        let mut headers = HeaderMap::new();
452        headers.insert("Content-Type", "text/plain");
453        let old = headers.insert("Content-Type", "application/json");
454
455        assert_eq!(old, Some("text/plain".to_string()));
456        assert_eq!(headers.len(), 1);
457        assert_eq!(
458            headers.get("Content-Type"),
459            Some(&"application/json".to_string())
460        );
461    }
462
463    #[test]
464    fn test_append_duplicates() {
465        let mut headers = HeaderMap::new();
466        headers.append("Set-Cookie", "session=abc");
467        headers.append("Set-Cookie", "user=123");
468
469        assert_eq!(headers.len(), 2);
470        let cookies = headers.get_all("Set-Cookie");
471        assert_eq!(cookies.len(), 2);
472    }
473
474    #[test]
475    fn test_remove() {
476        let mut headers = HeaderMap::new();
477        headers.insert("Content-Type", "application/json");
478        headers.insert("Accept", "text/html");
479
480        let removed = headers.remove("Content-Type");
481        assert_eq!(removed, Some("application/json".to_string()));
482        assert_eq!(headers.len(), 1);
483        assert!(!headers.contains("Content-Type"));
484    }
485
486    #[test]
487    fn test_inline_capacity() {
488        let mut headers = HeaderMap::new();
489
490        // Add INLINE_HEADERS headers
491        for i in 0..INLINE_HEADERS {
492            headers.insert(format!("Header-{}", i), format!("Value-{}", i));
493        }
494
495        assert!(headers.is_inline()); // Still inline
496
497        // Add one more
498        headers.insert("Extra-Header", "Extra-Value");
499
500        // Now it should have spilled to heap
501        assert!(!headers.is_inline());
502    }
503
504    #[test]
505    fn test_iter() {
506        let mut headers = HeaderMap::new();
507        headers.insert("A", "1");
508        headers.insert("B", "2");
509
510        let pairs: Vec<_> = headers.iter().collect();
511        assert_eq!(pairs.len(), 2);
512    }
513
514    #[test]
515    fn test_common_accessors() {
516        let mut headers = HeaderMap::new();
517        headers.insert("Content-Type", "application/json");
518        headers.insert("Content-Length", "100");
519        headers.insert("Connection", "keep-alive");
520        headers.insert("Transfer-Encoding", "chunked");
521
522        assert_eq!(
523            headers.content_type(),
524            Some(&"application/json".to_string())
525        );
526        assert_eq!(headers.content_length(), Some(100));
527        assert!(headers.is_keep_alive());
528        assert!(headers.is_chunked());
529    }
530
531    #[test]
532    fn test_from_hash_map() {
533        let mut map = HashMap::new();
534        map.insert("Content-Type".to_string(), "application/json".to_string());
535        map.insert("Accept".to_string(), "text/html".to_string());
536
537        let headers = HeaderMap::from_hash_map(map);
538        assert_eq!(headers.len(), 2);
539        assert!(headers.contains("Content-Type"));
540    }
541
542    #[test]
543    fn test_to_hash_map() {
544        let mut headers = HeaderMap::new();
545        headers.insert("Content-Type", "application/json");
546
547        let map = headers.to_hash_map();
548        assert_eq!(
549            map.get("Content-Type"),
550            Some(&"application/json".to_string())
551        );
552    }
553
554    #[test]
555    fn test_from_iterator() {
556        let headers: HeaderMap = [
557            ("Content-Type", "application/json"),
558            ("Accept", "text/html"),
559        ]
560        .into_iter()
561        .collect();
562
563        assert_eq!(headers.len(), 2);
564    }
565
566    #[test]
567    fn test_indexing() {
568        let mut headers = HeaderMap::new();
569        headers.insert("Content-Type", "application/json");
570
571        assert_eq!(&headers["Content-Type"], "application/json");
572    }
573}