Skip to main content

chopin_core/
headers.rs

1// src/headers.rs
2//
3// Zero-allocation response headers using fixed-size inline storage.
4//
5// Design: up to MAX_INLINE_HEADERS header values are stored in a
6// stack-allocated `ArrayVec` (the "slab"). When the slab is full, additional
7// headers spill into an optional `Vec` that is lazily created on first
8// overflow. Header values ≤ MAX_INLINE_VALUE bytes are stored ‘inline’ in an
9// `ArrayString`; longer values fall back to a heap `String`.
10
11use arrayvec::{ArrayString, ArrayVec};
12use std::fmt::Write;
13
14// ── constants ────────────────────────────────────────────────────────────────
15
16/// Maximum byte length of a header value stored inline (on the stack).
17pub const MAX_INLINE_VALUE: usize = 64;
18
19/// Number of headers kept in the inline slab before spilling to the heap.
20pub const MAX_INLINE_HEADERS: usize = 8;
21
22// ── HeaderValue ──────────────────────────────────────────────────────────────
23
24/// An HTTP response header value that avoids heap allocation for short strings.
25///
26/// | Variant  | Storage   | Allocation |
27/// |----------|-----------|------------|
28/// | `Static` | pointer   | none       |
29/// | `Inline` | 64-byte array on stack | none |
30/// | `Heap`   | `String` on heap | one |
31#[derive(Debug, Clone)]
32pub enum HeaderValue {
33    /// A compile-time constant string — zero cost to store.
34    Static(&'static str),
35    /// A short (≤ 64 byte) runtime string stored on the stack.
36    Inline(ArrayString<MAX_INLINE_VALUE>),
37    /// A long runtime string that did not fit inline.
38    Heap(String),
39}
40
41impl HeaderValue {
42    /// Return the value as a plain `&str`.
43    #[inline(always)]
44    pub fn as_str(&self) -> &str {
45        match self {
46            HeaderValue::Static(s) => s,
47            HeaderValue::Inline(s) => s.as_str(),
48            HeaderValue::Heap(s) => s.as_str(),
49        }
50    }
51
52    /// Build from an owned `String` – avoids the extra allocation when short.
53    #[inline]
54    fn from_owned(s: String) -> Self {
55        if s.len() <= MAX_INLINE_VALUE {
56            HeaderValue::Inline(ArrayString::from(&s).unwrap())
57        } else {
58            HeaderValue::Heap(s)
59        }
60    }
61}
62
63// ── IntoHeaderValue trait ────────────────────────────────────────────────────
64
65/// Convert a value into a [`HeaderValue`].
66///
67/// Implement this trait to use your own types with
68/// [`Response::with_header`](crate::http::Response::with_header).
69pub trait IntoHeaderValue {
70    fn into_header_value(self) -> HeaderValue;
71}
72
73/// `&'static str` → zero-cost `Static` variant.
74impl IntoHeaderValue for &'static str {
75    #[inline(always)]
76    fn into_header_value(self) -> HeaderValue {
77        HeaderValue::Static(self)
78    }
79}
80
81/// Owned `String` → `Inline` when short, otherwise `Heap`.
82impl IntoHeaderValue for String {
83    #[inline]
84    fn into_header_value(self) -> HeaderValue {
85        HeaderValue::from_owned(self)
86    }
87}
88
89// Numeric types ---------------------------------------------------------------
90// We write the decimal representation into an ArrayString via the standard
91// `fmt::Write` trait so we avoid pulling in a separate dependency.
92
93macro_rules! impl_into_header_value_int {
94    ($($t:ty),*) => {
95        $(impl IntoHeaderValue for $t {
96            #[inline]
97            fn into_header_value(self) -> HeaderValue {
98                let mut buf = ArrayString::<MAX_INLINE_VALUE>::new();
99                // Numbers are always shorter than 64 bytes, so write! is infallible.
100                write!(buf, "{}", self).ok();
101                HeaderValue::Inline(buf)
102            }
103        })*
104    };
105}
106
107impl_into_header_value_int!(
108    u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
109);
110
111// ── Header ───────────────────────────────────────────────────────────────────
112
113/// A single HTTP response header name/value pair.
114///
115/// Header names are always `&'static str` because they are HTTP standard
116/// field names (or custom `X-` fields) that are known at compile time.
117#[derive(Debug, Clone)]
118pub struct Header {
119    pub name: &'static str,
120    pub value: HeaderValue,
121}
122
123// ── Headers container ────────────────────────────────────────────────────────
124
125/// A compact, allocation-free (in the common case) container for HTTP response
126/// headers.
127///
128/// Up to [`MAX_INLINE_HEADERS`] (8) headers are stored entirely in a
129/// stack-allocated `ArrayVec` (*slab*). When the slab is full, subsequent
130/// headers flow into an optional heap `Vec` (*spill*) that is created on first
131/// overflow. Both pools are iterated in insertion order.
132///
133/// Using a struct (rather than an enum) avoids the `large_enum_variant` lint
134/// while keeping the same allocation-free fast path.
135#[derive(Debug, Clone)]
136pub struct Headers {
137    /// Fast path: up to 8 headers stored on the stack.
138    slab: ArrayVec<Header, MAX_INLINE_HEADERS>,
139    /// Slow path: created lazily when the slab overflows.
140    spill: Option<Vec<Header>>,
141}
142
143impl Default for Headers {
144    fn default() -> Self {
145        Headers::new()
146    }
147}
148
149impl Headers {
150    /// Create an empty header collection (no heap allocation).
151    #[inline(always)]
152    pub fn new() -> Self {
153        Headers {
154            slab: ArrayVec::new(),
155            spill: None,
156        }
157    }
158
159    /// Add a header by name and value.
160    ///
161    /// The value may be any type that implements [`IntoHeaderValue`]:
162    /// `&'static str`, `String`, or any integer type.
163    ///
164    /// Headers 1–8 go into the inline slab (stack). Any additional headers
165    /// are pushed into the lazily created spill `Vec`.
166    #[inline]
167    pub fn add(&mut self, name: &'static str, value: impl IntoHeaderValue) {
168        let header = Header {
169            name,
170            value: value.into_header_value(),
171        };
172        if !self.slab.is_full() {
173            // SAFETY: checked `!is_full()`, so push cannot panic.
174            self.slab.push(header);
175        } else {
176            self.spill.get_or_insert_with(Vec::new).push(header);
177        }
178    }
179
180    /// Add a header where both name and value are compile-time constants.
181    /// This is the absolute zero-cost path: no memcopy, no heap allocation.
182    #[inline(always)]
183    pub fn add_static(&mut self, name: &'static str, value: &'static str) {
184        self.add(name, value);
185    }
186
187    /// Iterate over all headers in insertion order (slab first, then spill).
188    #[inline]
189    pub fn iter(
190        &self,
191    ) -> std::iter::Chain<std::slice::Iter<'_, Header>, std::slice::Iter<'_, Header>> {
192        let spill_slice: &[Header] = self.spill.as_deref().unwrap_or(&[]);
193        self.slab.iter().chain(spill_slice.iter())
194    }
195
196    /// Returns `true` if no headers have been added.
197    #[inline(always)]
198    pub fn is_empty(&self) -> bool {
199        self.slab.is_empty()
200    }
201
202    /// Returns the total number of headers.
203    #[inline(always)]
204    pub fn len(&self) -> usize {
205        self.slab.len() + self.spill.as_ref().map_or(0, |v| v.len())
206    }
207}
208
209// ── Tests ────────────────────────────────────────────────────────────────────
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_inline_headers_no_spill() {
217        let mut h = Headers::new();
218        for i in 0..MAX_INLINE_HEADERS {
219            h.add("X-Test", format!("value-{}", i));
220        }
221        assert!(h.spill.is_none(), "should not spill at capacity");
222        assert_eq!(h.len(), MAX_INLINE_HEADERS);
223    }
224
225    #[test]
226    fn test_spill_to_heap_on_overflow() {
227        let mut h = Headers::new();
228        for i in 0..=MAX_INLINE_HEADERS {
229            // 9 headers → 1 over the inline limit → spill
230            h.add("X-Test", format!("value-{}", i));
231        }
232        assert!(h.spill.is_some(), "9th header should create spill Vec");
233        assert_eq!(h.len(), MAX_INLINE_HEADERS + 1);
234    }
235
236    #[test]
237    fn test_static_str_value() {
238        let mut h = Headers::new();
239        h.add("Content-Type", "application/json");
240        let hdr = h.iter().next().unwrap();
241        assert_eq!(hdr.value.as_str(), "application/json");
242        // The &'static str path should use the Static variant.
243        assert!(matches!(hdr.value, HeaderValue::Static(_)));
244    }
245
246    #[test]
247    fn test_short_string_inline() {
248        let val = "gzip".to_string();
249        let mut h = Headers::new();
250        h.add("Content-Encoding", val);
251        let hdr = h.iter().next().unwrap();
252        assert!(matches!(hdr.value, HeaderValue::Inline(_)));
253        assert_eq!(hdr.value.as_str(), "gzip");
254    }
255
256    #[test]
257    fn test_long_string_heap() {
258        let long = "x".repeat(MAX_INLINE_VALUE + 1);
259        let v = HeaderValue::from_owned(long.clone());
260        assert!(matches!(v, HeaderValue::Heap(_)));
261        assert_eq!(v.as_str(), long);
262    }
263
264    #[test]
265    fn test_integer_value_inline() {
266        let mut h = Headers::new();
267        h.add("Content-Length", 12345usize);
268        let hdr = h.iter().next().unwrap();
269        assert!(matches!(hdr.value, HeaderValue::Inline(_)));
270        assert_eq!(hdr.value.as_str(), "12345");
271    }
272
273    #[test]
274    fn test_iter_order_preserved() {
275        let mut h = Headers::new();
276        h.add("X-A", "1");
277        h.add("X-B", "2");
278        h.add("X-C", "3");
279        let names: Vec<&str> = h.iter().map(|hdr| hdr.name).collect();
280        assert_eq!(names, vec!["X-A", "X-B", "X-C"]);
281    }
282
283    #[test]
284    fn test_heap_iter_order_preserved() {
285        let mut h = Headers::new();
286        for i in 0..=MAX_INLINE_HEADERS {
287            let name: &'static str = ["A", "B", "C", "D", "E", "F", "G", "H", "I"][i];
288            h.add(name, i as u32);
289        }
290        assert!(h.spill.is_some(), "9th header should have caused a spill");
291        let vals: Vec<&str> = h.iter().map(|hdr| hdr.value.as_str()).collect::<Vec<_>>();
292        assert_eq!(vals, vec!["0", "1", "2", "3", "4", "5", "6", "7", "8"]);
293    }
294}