Skip to main content

sentinel_agent_protocol/
headers.rs

1//! Zero-copy header types for efficient header processing.
2//!
3//! This module provides header types that avoid allocation in the hot path
4//! by using borrowed references and `Cow` for deferred cloning.
5//!
6//! # Performance
7//!
8//! - Header iteration: O(n) with zero allocations
9//! - Header lookup: O(1) average (borrowed from source HashMap)
10//! - Conversion to owned: Only allocates when actually needed
11//! - SmallVec for values: Inline storage for single-value headers (most common)
12
13use std::borrow::Cow;
14use std::collections::HashMap;
15
16use smallvec::SmallVec;
17
18/// Header values using SmallVec for inline storage.
19///
20/// Most HTTP headers have a single value. Using SmallVec<[String; 1]>
21/// avoids heap allocation for the Vec in the common case.
22pub type HeaderValues = SmallVec<[String; 1]>;
23
24/// Optimized header map using SmallVec for values.
25///
26/// This reduces allocations for typical requests where most headers
27/// have only one value.
28pub type OptimizedHeaderMap = HashMap<String, HeaderValues>;
29
30/// Zero-copy header reference.
31///
32/// Wraps a reference to a header map without cloning.
33#[derive(Debug)]
34pub struct HeadersRef<'a> {
35    inner: &'a HashMap<String, Vec<String>>,
36}
37
38impl<'a> HeadersRef<'a> {
39    /// Create a new header reference.
40    #[inline]
41    pub fn new(headers: &'a HashMap<String, Vec<String>>) -> Self {
42        Self { inner: headers }
43    }
44
45    /// Get a header value by name.
46    #[inline]
47    pub fn get(&self, name: &str) -> Option<&Vec<String>> {
48        self.inner.get(name)
49    }
50
51    /// Get the first value for a header.
52    #[inline]
53    pub fn get_first(&self, name: &str) -> Option<&str> {
54        self.inner
55            .get(name)
56            .and_then(|v| v.first())
57            .map(|s| s.as_str())
58    }
59
60    /// Check if a header exists.
61    #[inline]
62    pub fn contains(&self, name: &str) -> bool {
63        self.inner.contains_key(name)
64    }
65
66    /// Get the number of unique header names.
67    #[inline]
68    pub fn len(&self) -> usize {
69        self.inner.len()
70    }
71
72    /// Check if headers are empty.
73    #[inline]
74    pub fn is_empty(&self) -> bool {
75        self.inner.is_empty()
76    }
77
78    /// Iterate over header names and values (no allocation).
79    #[inline]
80    pub fn iter(&self) -> impl Iterator<Item = (&str, &Vec<String>)> {
81        self.inner.iter().map(|(k, v)| (k.as_str(), v))
82    }
83
84    /// Iterate over flattened header name-value pairs.
85    #[inline]
86    pub fn iter_flat(&self) -> impl Iterator<Item = (&str, &str)> {
87        self.inner
88            .iter()
89            .flat_map(|(k, values)| values.iter().map(move |v| (k.as_str(), v.as_str())))
90    }
91
92    /// Convert to owned HashMap (clones).
93    #[inline]
94    pub fn to_owned(&self) -> HashMap<String, Vec<String>> {
95        self.inner.clone()
96    }
97
98    /// Get the underlying reference.
99    #[inline]
100    pub fn as_inner(&self) -> &HashMap<String, Vec<String>> {
101        self.inner
102    }
103}
104
105/// Copy-on-write headers for deferred cloning.
106///
107/// Allows working with headers without cloning until mutation is needed.
108#[derive(Debug, Clone)]
109pub struct HeadersCow<'a> {
110    inner: Cow<'a, HashMap<String, Vec<String>>>,
111}
112
113impl<'a> HeadersCow<'a> {
114    /// Create from a borrowed reference.
115    #[inline]
116    pub fn borrowed(headers: &'a HashMap<String, Vec<String>>) -> Self {
117        Self {
118            inner: Cow::Borrowed(headers),
119        }
120    }
121
122    /// Create from an owned HashMap.
123    #[inline]
124    pub fn owned(headers: HashMap<String, Vec<String>>) -> Self {
125        Self {
126            inner: Cow::Owned(headers),
127        }
128    }
129
130    /// Get a header value by name.
131    #[inline]
132    pub fn get(&self, name: &str) -> Option<&Vec<String>> {
133        self.inner.get(name)
134    }
135
136    /// Get the first value for a header.
137    #[inline]
138    pub fn get_first(&self, name: &str) -> Option<&str> {
139        self.inner
140            .get(name)
141            .and_then(|v| v.first())
142            .map(|s| s.as_str())
143    }
144
145    /// Check if a header exists.
146    #[inline]
147    pub fn contains(&self, name: &str) -> bool {
148        self.inner.contains_key(name)
149    }
150
151    /// Set a header value (triggers clone if borrowed).
152    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
153        self.inner.to_mut().insert(name.into(), vec![value.into()]);
154    }
155
156    /// Add a header value (triggers clone if borrowed).
157    pub fn add(&mut self, name: impl Into<String>, value: impl Into<String>) {
158        self.inner
159            .to_mut()
160            .entry(name.into())
161            .or_default()
162            .push(value.into());
163    }
164
165    /// Remove a header (triggers clone if borrowed).
166    pub fn remove(&mut self, name: &str) -> Option<Vec<String>> {
167        self.inner.to_mut().remove(name)
168    }
169
170    /// Check if the headers have been cloned.
171    #[inline]
172    pub fn is_owned(&self) -> bool {
173        matches!(self.inner, Cow::Owned(_))
174    }
175
176    /// Convert to owned HashMap.
177    #[inline]
178    pub fn into_owned(self) -> HashMap<String, Vec<String>> {
179        self.inner.into_owned()
180    }
181
182    /// Get the number of unique header names.
183    #[inline]
184    pub fn len(&self) -> usize {
185        self.inner.len()
186    }
187
188    /// Check if headers are empty.
189    #[inline]
190    pub fn is_empty(&self) -> bool {
191        self.inner.is_empty()
192    }
193
194    /// Iterate over header names and values.
195    #[inline]
196    pub fn iter(&self) -> impl Iterator<Item = (&str, &Vec<String>)> {
197        self.inner.iter().map(|(k, v)| (k.as_str(), v))
198    }
199}
200
201impl Default for HeadersCow<'_> {
202    fn default() -> Self {
203        Self::owned(HashMap::new())
204    }
205}
206
207impl<'a> From<&'a HashMap<String, Vec<String>>> for HeadersCow<'a> {
208    fn from(headers: &'a HashMap<String, Vec<String>>) -> Self {
209        Self::borrowed(headers)
210    }
211}
212
213impl From<HashMap<String, Vec<String>>> for HeadersCow<'_> {
214    fn from(headers: HashMap<String, Vec<String>>) -> Self {
215        Self::owned(headers)
216    }
217}
218
219/// Header name/value iterator that yields references.
220pub struct HeaderIterator<'a> {
221    inner: std::collections::hash_map::Iter<'a, String, Vec<String>>,
222    current_name: Option<&'a str>,
223    current_values: Option<std::slice::Iter<'a, String>>,
224}
225
226impl<'a> HeaderIterator<'a> {
227    /// Create a new header iterator.
228    pub fn new(headers: &'a HashMap<String, Vec<String>>) -> Self {
229        Self {
230            inner: headers.iter(),
231            current_name: None,
232            current_values: None,
233        }
234    }
235}
236
237impl<'a> Iterator for HeaderIterator<'a> {
238    type Item = (&'a str, &'a str);
239
240    fn next(&mut self) -> Option<Self::Item> {
241        loop {
242            // Try to get next value from current header
243            if let (Some(name), Some(values)) = (self.current_name, self.current_values.as_mut()) {
244                if let Some(value) = values.next() {
245                    return Some((name, value.as_str()));
246                }
247            }
248
249            // Move to next header
250            let (name, values) = self.inner.next()?;
251            self.current_name = Some(name.as_str());
252            self.current_values = Some(values.iter());
253        }
254    }
255}
256
257/// Common HTTP header names as constants (avoids string allocation).
258pub mod names {
259    pub const HOST: &str = "host";
260    pub const CONTENT_TYPE: &str = "content-type";
261    pub const CONTENT_LENGTH: &str = "content-length";
262    pub const USER_AGENT: &str = "user-agent";
263    pub const ACCEPT: &str = "accept";
264    pub const ACCEPT_ENCODING: &str = "accept-encoding";
265    pub const ACCEPT_LANGUAGE: &str = "accept-language";
266    pub const AUTHORIZATION: &str = "authorization";
267    pub const COOKIE: &str = "cookie";
268    pub const SET_COOKIE: &str = "set-cookie";
269    pub const CACHE_CONTROL: &str = "cache-control";
270    pub const CONNECTION: &str = "connection";
271    pub const DATE: &str = "date";
272    pub const ETAG: &str = "etag";
273    pub const IF_MATCH: &str = "if-match";
274    pub const IF_NONE_MATCH: &str = "if-none-match";
275    pub const IF_MODIFIED_SINCE: &str = "if-modified-since";
276    pub const LAST_MODIFIED: &str = "last-modified";
277    pub const LOCATION: &str = "location";
278    pub const ORIGIN: &str = "origin";
279    pub const REFERER: &str = "referer";
280    pub const SERVER: &str = "server";
281    pub const TRANSFER_ENCODING: &str = "transfer-encoding";
282    pub const VARY: &str = "vary";
283    pub const X_FORWARDED_FOR: &str = "x-forwarded-for";
284    pub const X_FORWARDED_PROTO: &str = "x-forwarded-proto";
285    pub const X_FORWARDED_HOST: &str = "x-forwarded-host";
286    pub const X_REAL_IP: &str = "x-real-ip";
287    pub const X_REQUEST_ID: &str = "x-request-id";
288    pub const X_CORRELATION_ID: &str = "x-correlation-id";
289    pub const X_TRACE_ID: &str = "x-trace-id";
290    pub const X_SPAN_ID: &str = "x-span-id";
291}
292
293/// Header name type using Cow for zero-allocation on common headers.
294///
295/// When the header name matches a well-known header, this borrows a static
296/// string instead of allocating. Unknown headers are stored as owned Strings.
297pub type CowHeaderName = Cow<'static, str>;
298
299/// Header map using Cow<'static, str> keys for zero-allocation header names.
300///
301/// # Performance
302///
303/// For common headers (Content-Type, Authorization, etc.), the key is a
304/// borrowed static string reference. For unknown headers, the key is an
305/// owned String. This avoids ~95% of header name allocations in typical
306/// HTTP traffic.
307///
308/// # Example
309///
310/// ```
311/// use sentinel_agent_protocol::headers::{CowHeaderMap, HeaderValues, intern_header_name};
312///
313/// let mut headers = CowHeaderMap::new();
314/// headers.insert(
315///     intern_header_name("content-type"),
316///     HeaderValues::from_iter(["application/json".to_string()])
317/// );
318/// ```
319pub type CowHeaderMap = HashMap<CowHeaderName, HeaderValues>;
320
321/// Intern a header name, returning a static reference for known headers.
322///
323/// This is the key optimization: common headers like "Content-Type" or
324/// "Authorization" return `Cow::Borrowed(&'static str)` instead of
325/// allocating a new String.
326///
327/// # Performance
328///
329/// - Known headers: O(1) lookup, zero allocation
330/// - Unknown headers: O(1) to create owned Cow, one allocation
331///
332/// # Example
333///
334/// ```
335/// use sentinel_agent_protocol::headers::intern_header_name;
336/// use std::borrow::Cow;
337///
338/// // Known header - no allocation
339/// let ct = intern_header_name("content-type");
340/// assert!(matches!(ct, Cow::Borrowed(_)));
341///
342/// // Unknown header - allocates once
343/// let custom = intern_header_name("x-custom-header");
344/// assert!(matches!(custom, Cow::Owned(_)));
345/// ```
346#[inline]
347pub fn intern_header_name(name: &str) -> CowHeaderName {
348    // Case-insensitive matching for HTTP headers
349    let lower = name.to_ascii_lowercase();
350
351    match lower.as_str() {
352        "host" => Cow::Borrowed(names::HOST),
353        "content-type" => Cow::Borrowed(names::CONTENT_TYPE),
354        "content-length" => Cow::Borrowed(names::CONTENT_LENGTH),
355        "user-agent" => Cow::Borrowed(names::USER_AGENT),
356        "accept" => Cow::Borrowed(names::ACCEPT),
357        "accept-encoding" => Cow::Borrowed(names::ACCEPT_ENCODING),
358        "accept-language" => Cow::Borrowed(names::ACCEPT_LANGUAGE),
359        "authorization" => Cow::Borrowed(names::AUTHORIZATION),
360        "cookie" => Cow::Borrowed(names::COOKIE),
361        "set-cookie" => Cow::Borrowed(names::SET_COOKIE),
362        "cache-control" => Cow::Borrowed(names::CACHE_CONTROL),
363        "connection" => Cow::Borrowed(names::CONNECTION),
364        "date" => Cow::Borrowed(names::DATE),
365        "etag" => Cow::Borrowed(names::ETAG),
366        "if-match" => Cow::Borrowed(names::IF_MATCH),
367        "if-none-match" => Cow::Borrowed(names::IF_NONE_MATCH),
368        "if-modified-since" => Cow::Borrowed(names::IF_MODIFIED_SINCE),
369        "last-modified" => Cow::Borrowed(names::LAST_MODIFIED),
370        "location" => Cow::Borrowed(names::LOCATION),
371        "origin" => Cow::Borrowed(names::ORIGIN),
372        "referer" => Cow::Borrowed(names::REFERER),
373        "server" => Cow::Borrowed(names::SERVER),
374        "transfer-encoding" => Cow::Borrowed(names::TRANSFER_ENCODING),
375        "vary" => Cow::Borrowed(names::VARY),
376        "x-forwarded-for" => Cow::Borrowed(names::X_FORWARDED_FOR),
377        "x-forwarded-proto" => Cow::Borrowed(names::X_FORWARDED_PROTO),
378        "x-forwarded-host" => Cow::Borrowed(names::X_FORWARDED_HOST),
379        "x-real-ip" => Cow::Borrowed(names::X_REAL_IP),
380        "x-request-id" => Cow::Borrowed(names::X_REQUEST_ID),
381        "x-correlation-id" => Cow::Borrowed(names::X_CORRELATION_ID),
382        "x-trace-id" => Cow::Borrowed(names::X_TRACE_ID),
383        "x-span-id" => Cow::Borrowed(names::X_SPAN_ID),
384        _ => Cow::Owned(lower), // Unknown header - use the lowercased string
385    }
386}
387
388/// Convert standard headers to Cow-optimized format.
389///
390/// This converts both header names and values to the optimized format,
391/// using static references for known header names.
392#[inline]
393pub fn to_cow_optimized(headers: HashMap<String, Vec<String>>) -> CowHeaderMap {
394    headers
395        .into_iter()
396        .map(|(name, values)| (intern_header_name(&name), HeaderValues::from_vec(values)))
397        .collect()
398}
399
400/// Convert Cow-optimized headers back to standard format.
401///
402/// This converts header names back to owned Strings.
403#[inline]
404pub fn from_cow_optimized(headers: CowHeaderMap) -> HashMap<String, Vec<String>> {
405    headers
406        .into_iter()
407        .map(|(name, values)| (name.into_owned(), values.into_vec()))
408        .collect()
409}
410
411/// Iterate over Cow headers yielding (name, value) pairs.
412#[inline]
413pub fn iter_flat_cow(headers: &CowHeaderMap) -> impl Iterator<Item = (&str, &str)> {
414    headers
415        .iter()
416        .flat_map(|(name, values)| values.iter().map(move |v| (name.as_ref(), v.as_str())))
417}
418
419/// Convert standard headers to optimized format.
420///
421/// This is useful when receiving headers from external sources (JSON, gRPC)
422/// and converting them for internal processing.
423#[inline]
424pub fn to_optimized(headers: HashMap<String, Vec<String>>) -> OptimizedHeaderMap {
425    headers
426        .into_iter()
427        .map(|(name, values)| (name, HeaderValues::from_vec(values)))
428        .collect()
429}
430
431/// Convert optimized headers back to standard format.
432///
433/// This is useful when serializing headers for external transmission.
434#[inline]
435pub fn from_optimized(headers: OptimizedHeaderMap) -> HashMap<String, Vec<String>> {
436    headers
437        .into_iter()
438        .map(|(name, values)| (name, values.into_vec()))
439        .collect()
440}
441
442/// Iterate over headers yielding (name, value) pairs without allocation.
443///
444/// This is the most efficient way to convert headers to gRPC format.
445#[inline]
446pub fn iter_flat(headers: &HashMap<String, Vec<String>>) -> impl Iterator<Item = (&str, &str)> {
447    headers
448        .iter()
449        .flat_map(|(name, values)| values.iter().map(move |v| (name.as_str(), v.as_str())))
450}
451
452/// Iterate over optimized headers yielding (name, value) pairs.
453#[inline]
454pub fn iter_flat_optimized(headers: &OptimizedHeaderMap) -> impl Iterator<Item = (&str, &str)> {
455    headers
456        .iter()
457        .flat_map(|(name, values)| values.iter().map(move |v| (name.as_str(), v.as_str())))
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    fn sample_headers() -> HashMap<String, Vec<String>> {
465        let mut h = HashMap::new();
466        h.insert(
467            "content-type".to_string(),
468            vec!["application/json".to_string()],
469        );
470        h.insert(
471            "accept".to_string(),
472            vec!["text/html".to_string(), "application/json".to_string()],
473        );
474        h.insert("x-custom".to_string(), vec!["value".to_string()]);
475        h
476    }
477
478    #[test]
479    fn test_headers_ref() {
480        let headers = sample_headers();
481        let ref_ = HeadersRef::new(&headers);
482
483        assert_eq!(ref_.get_first("content-type"), Some("application/json"));
484        assert_eq!(ref_.get("accept").map(|v| v.len()), Some(2));
485        assert!(ref_.contains("x-custom"));
486        assert!(!ref_.contains("not-present"));
487        assert_eq!(ref_.len(), 3);
488    }
489
490    #[test]
491    fn test_headers_ref_iter() {
492        let headers = sample_headers();
493        let ref_ = HeadersRef::new(&headers);
494
495        let flat: Vec<_> = ref_.iter_flat().collect();
496        assert!(flat.contains(&("content-type", "application/json")));
497        assert!(flat.contains(&("accept", "text/html")));
498        assert!(flat.contains(&("accept", "application/json")));
499    }
500
501    #[test]
502    fn test_headers_cow_borrowed() {
503        let headers = sample_headers();
504        let cow = HeadersCow::borrowed(&headers);
505
506        assert!(!cow.is_owned());
507        assert_eq!(cow.get_first("content-type"), Some("application/json"));
508    }
509
510    #[test]
511    fn test_headers_cow_mutation() {
512        let headers = sample_headers();
513        let mut cow = HeadersCow::borrowed(&headers);
514
515        assert!(!cow.is_owned());
516
517        // Mutation triggers clone
518        cow.set("x-new", "new-value");
519        assert!(cow.is_owned());
520
521        assert_eq!(cow.get_first("x-new"), Some("new-value"));
522        // Original headers unchanged
523        assert!(!headers.contains_key("x-new"));
524    }
525
526    #[test]
527    fn test_headers_cow_add() {
528        let headers = sample_headers();
529        let mut cow = HeadersCow::borrowed(&headers);
530
531        cow.add("accept", "text/plain");
532        assert!(cow.is_owned());
533
534        let accept = cow.get("accept").unwrap();
535        assert_eq!(accept.len(), 3);
536    }
537
538    #[test]
539    fn test_header_iterator() {
540        let headers = sample_headers();
541        let iter = HeaderIterator::new(&headers);
542
543        let pairs: Vec<_> = iter.collect();
544        assert!(pairs.contains(&("content-type", "application/json")));
545        assert!(pairs.contains(&("accept", "text/html")));
546        assert!(pairs.contains(&("accept", "application/json")));
547        assert!(pairs.contains(&("x-custom", "value")));
548    }
549
550    #[test]
551    fn test_header_names() {
552        use names::*;
553
554        // Just verify the constants exist and are lowercase
555        assert_eq!(CONTENT_TYPE, "content-type");
556        assert_eq!(AUTHORIZATION, "authorization");
557        assert_eq!(X_FORWARDED_FOR, "x-forwarded-for");
558    }
559
560    #[test]
561    fn test_optimized_header_map() {
562        let mut optimized: OptimizedHeaderMap = HashMap::new();
563
564        // Single value - stored inline (no Vec allocation)
565        optimized.insert(
566            "content-type".to_string(),
567            HeaderValues::from_iter(["application/json".to_string()]),
568        );
569
570        // Multiple values
571        optimized.insert(
572            "accept".to_string(),
573            HeaderValues::from_iter(["text/html".to_string(), "application/json".to_string()]),
574        );
575
576        assert_eq!(optimized.get("content-type").map(|v| v.len()), Some(1));
577        assert_eq!(optimized.get("accept").map(|v| v.len()), Some(2));
578    }
579
580    #[test]
581    fn test_to_from_optimized() {
582        let headers = sample_headers();
583
584        // Convert to optimized
585        let optimized = to_optimized(headers.clone());
586        assert_eq!(optimized.len(), headers.len());
587
588        // Convert back
589        let back = from_optimized(optimized);
590        assert_eq!(back, headers);
591    }
592
593    #[test]
594    fn test_iter_flat_helper() {
595        let headers = sample_headers();
596        let pairs: Vec<_> = iter_flat(&headers).collect();
597
598        // Should have 4 pairs (1 content-type + 2 accept + 1 x-custom)
599        assert_eq!(pairs.len(), 4);
600        assert!(pairs.contains(&("content-type", "application/json")));
601        assert!(pairs.contains(&("accept", "text/html")));
602        assert!(pairs.contains(&("accept", "application/json")));
603        assert!(pairs.contains(&("x-custom", "value")));
604    }
605
606    #[test]
607    fn test_iter_flat_optimized_helper() {
608        let headers = sample_headers();
609        let optimized = to_optimized(headers);
610        let pairs: Vec<_> = iter_flat_optimized(&optimized).collect();
611
612        assert_eq!(pairs.len(), 4);
613        assert!(pairs.contains(&("content-type", "application/json")));
614    }
615
616    #[test]
617    fn test_smallvec_single_value_inline() {
618        // Verify SmallVec stores single value inline
619        let values: HeaderValues = HeaderValues::from_iter(["single".to_string()]);
620
621        // SmallVec<[String; 1]> should not spill to heap for single value
622        assert!(!values.spilled());
623        assert_eq!(values.len(), 1);
624        assert_eq!(values[0], "single");
625    }
626
627    #[test]
628    fn test_smallvec_multiple_values_spill() {
629        // Verify SmallVec spills to heap for multiple values
630        let values: HeaderValues =
631            HeaderValues::from_iter(["first".to_string(), "second".to_string()]);
632
633        // SmallVec<[String; 1]> should spill for 2+ values
634        assert!(values.spilled());
635        assert_eq!(values.len(), 2);
636    }
637
638    #[test]
639    fn test_intern_header_name_known() {
640        // Known headers should return borrowed static strings
641        let ct = intern_header_name("content-type");
642        assert!(matches!(ct, Cow::Borrowed(_)));
643        assert_eq!(ct, "content-type");
644
645        // Case-insensitive
646        let ct_upper = intern_header_name("Content-Type");
647        assert!(matches!(ct_upper, Cow::Borrowed(_)));
648        assert_eq!(ct_upper, "content-type");
649
650        // Mixed case
651        let ct_mixed = intern_header_name("CONTENT-TYPE");
652        assert!(matches!(ct_mixed, Cow::Borrowed(_)));
653        assert_eq!(ct_mixed, "content-type");
654    }
655
656    #[test]
657    fn test_intern_header_name_unknown() {
658        // Unknown headers should return owned strings
659        let custom = intern_header_name("x-custom-header");
660        assert!(matches!(custom, Cow::Owned(_)));
661        assert_eq!(custom, "x-custom-header");
662    }
663
664    #[test]
665    fn test_intern_header_name_all_known() {
666        // Verify all known headers are interned correctly
667        let known_headers = [
668            "host",
669            "content-type",
670            "content-length",
671            "user-agent",
672            "accept",
673            "accept-encoding",
674            "accept-language",
675            "authorization",
676            "cookie",
677            "set-cookie",
678            "cache-control",
679            "connection",
680            "date",
681            "etag",
682            "if-match",
683            "if-none-match",
684            "if-modified-since",
685            "last-modified",
686            "location",
687            "origin",
688            "referer",
689            "server",
690            "transfer-encoding",
691            "vary",
692            "x-forwarded-for",
693            "x-forwarded-proto",
694            "x-forwarded-host",
695            "x-real-ip",
696            "x-request-id",
697            "x-correlation-id",
698            "x-trace-id",
699            "x-span-id",
700        ];
701
702        for header in known_headers {
703            let interned = intern_header_name(header);
704            assert!(
705                matches!(interned, Cow::Borrowed(_)),
706                "Header '{}' should be interned as borrowed",
707                header
708            );
709            assert_eq!(interned, header);
710        }
711    }
712
713    #[test]
714    fn test_cow_header_map() {
715        let mut headers = CowHeaderMap::new();
716
717        // Insert using interned names
718        headers.insert(
719            intern_header_name("content-type"),
720            HeaderValues::from_iter(["application/json".to_string()]),
721        );
722        headers.insert(
723            intern_header_name("x-custom"),
724            HeaderValues::from_iter(["value".to_string()]),
725        );
726
727        // Lookup works with borrowed strings
728        assert!(headers.contains_key("content-type"));
729        assert!(headers.contains_key("x-custom"));
730    }
731
732    #[test]
733    fn test_to_from_cow_optimized() {
734        let headers = sample_headers();
735
736        // Convert to Cow optimized
737        let cow_optimized = to_cow_optimized(headers.clone());
738        assert_eq!(cow_optimized.len(), headers.len());
739
740        // Known headers should be borrowed
741        for name in cow_optimized.keys() {
742            if name == "content-type" || name == "accept" {
743                assert!(
744                    matches!(name, Cow::Borrowed(_)),
745                    "Known header '{}' should be borrowed",
746                    name
747                );
748            }
749        }
750
751        // Convert back
752        let back = from_cow_optimized(cow_optimized);
753        assert_eq!(back, headers);
754    }
755
756    #[test]
757    fn test_iter_flat_cow() {
758        let headers = sample_headers();
759        let cow_optimized = to_cow_optimized(headers);
760        let pairs: Vec<_> = iter_flat_cow(&cow_optimized).collect();
761
762        assert_eq!(pairs.len(), 4);
763        assert!(pairs.contains(&("content-type", "application/json")));
764        assert!(pairs.contains(&("accept", "text/html")));
765        assert!(pairs.contains(&("accept", "application/json")));
766        assert!(pairs.contains(&("x-custom", "value")));
767    }
768}