Skip to main content

go_http/
header.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::HashMap;
4use std::io::{self, Write};
5
6/// HTTP headers — a multi-valued map with case-insensitive canonical keys.
7///
8/// Keys are stored in canonical form ("Content-Type", not "content-type").
9/// Mirrors Go's `http.Header`.
10#[derive(Debug, Clone, Default, PartialEq)]
11pub struct Header(HashMap<String, Vec<String>>);
12
13impl Header {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    /// Canonical form of a header key: "content-type" → "Content-Type".
19    pub fn canonical_key(s: &str) -> String {
20        let mut out = String::with_capacity(s.len());
21        let mut upper = true;
22        for c in s.chars() {
23            if c == '-' {
24                out.push('-');
25                upper = true;
26            } else if upper {
27                out.extend(c.to_uppercase());
28                upper = false;
29            } else {
30                out.extend(c.to_lowercase());
31            }
32        }
33        out
34    }
35
36    /// Return the first value for the given key, if any.
37    pub fn get(&self, key: &str) -> Option<&str> {
38        let k = Self::canonical_key(key);
39        self.0.get(&k).and_then(|v| v.first()).map(String::as_str)
40    }
41
42    /// Set the key to a single value, replacing any existing values.
43    pub fn set(&mut self, key: &str, value: impl Into<String>) {
44        let k = Self::canonical_key(key);
45        self.0.insert(k, vec![value.into()]);
46    }
47
48    /// Add a value to the key without replacing existing values.
49    pub fn add(&mut self, key: &str, value: impl Into<String>) {
50        let k = Self::canonical_key(key);
51        self.0.entry(k).or_default().push(value.into());
52    }
53
54    /// Remove all values for the given key.
55    pub fn del(&mut self, key: &str) {
56        let k = Self::canonical_key(key);
57        self.0.remove(&k);
58    }
59
60    /// All values for the given key.
61    pub fn values(&self, key: &str) -> &[String] {
62        let k = Self::canonical_key(key);
63        self.0.get(&k).map(Vec::as_slice).unwrap_or(&[])
64    }
65
66    /// Iterate over all (key, values) pairs.
67    pub fn iter(&self) -> impl Iterator<Item = (&str, &[String])> {
68        self.0.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
69    }
70
71    /// `true` if the header map contains no entries.
72    pub fn is_empty(&self) -> bool {
73        self.0.is_empty()
74    }
75
76    /// Serialize the headers to wire format (each "Key: value\r\n" line).
77    /// Does not write the terminal blank line.
78    pub fn write_to(&self, w: &mut impl Write) -> io::Result<()> {
79        let mut keys: Vec<&String> = self.0.keys().collect();
80        keys.sort();
81        for key in keys {
82            for val in &self.0[key] {
83                write!(w, "{key}: {val}\r\n")?;
84            }
85        }
86        Ok(())
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn canonical_key() {
96        assert_eq!(Header::canonical_key("content-type"), "Content-Type");
97        assert_eq!(Header::canonical_key("x-request-id"), "X-Request-Id");
98        assert_eq!(Header::canonical_key("ACCEPT"), "Accept");
99    }
100
101    #[test]
102    fn set_get_del() {
103        let mut h = Header::new();
104        h.set("content-type", "text/plain");
105        assert_eq!(h.get("Content-Type"), Some("text/plain"));
106        h.del("content-type");
107        assert_eq!(h.get("content-type"), None);
108    }
109
110    #[test]
111    fn add_multi_value() {
112        let mut h = Header::new();
113        h.add("Accept", "text/html");
114        h.add("accept", "application/json");
115        assert_eq!(h.values("Accept"), &["text/html", "application/json"]);
116    }
117
118    #[test]
119    fn write_wire_format() {
120        let mut h = Header::new();
121        h.set("Content-Type", "text/plain");
122        h.set("Content-Length", "5");
123        let mut buf = Vec::new();
124        h.write_to(&mut buf).unwrap();
125        let s = String::from_utf8(buf).unwrap();
126        assert!(s.contains("Content-Type: text/plain\r\n"));
127        assert!(s.contains("Content-Length: 5\r\n"));
128    }
129}