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