Skip to main content

goose_http/headers/
mod.rs

1//! HTTP header utilities and case-insensitive storage.
2//!
3//! This module provides the foundational header map implementation used across
4//! the Goose HTTP server to store and retrieve header fields while preserving
5//! insertion order and supporting multi-value semantics.
6
7use std::collections::HashMap;
8use std::fmt;
9
10/// Represents an HTTP/1.1 header name with case-insensitive comparison.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct HeaderName(String);
13
14impl HeaderName {
15    /// Create a new header name (internally lowercased for case-insensitive comparison).
16    pub fn new(name: impl Into<String>) -> Self {
17        let name = name.into();
18        HeaderName(name.to_lowercase())
19    }
20
21    /// Get the header name as a string slice.
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25}
26
27impl From<&str> for HeaderName {
28    fn from(s: &str) -> Self {
29        HeaderName::new(s)
30    }
31}
32
33impl From<String> for HeaderName {
34    fn from(s: String) -> Self {
35        HeaderName::new(s)
36    }
37}
38
39impl fmt::Display for HeaderName {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(f, "{}", self.0)
42    }
43}
44
45/// Common HTTP header field names as string constants for efficient lookups.
46pub mod header_keys {
47    /// Content-Type header (RFC 9110 Section 8.3).
48    pub const CONTENT_TYPE: &str = "content-type";
49    /// Content-Encoding header (RFC 9110 Section 8.4).
50    pub const CONTENT_ENCODING: &str = "content-encoding";
51    /// Content-Language header (RFC 9110 Section 12.5.4).
52    pub const CONTENT_LANGUAGE: &str = "content-language";
53    /// Content-Location header (RFC 9110 Section 8.5).
54    pub const CONTENT_LOCATION: &str = "content-location";
55
56    /// Request and response framing headers.
57    pub const CONTENT_LENGTH: &str = "content-length";
58    pub const HOST: &str = "host";
59    pub const TRANSFER_ENCODING: &str = "transfer-encoding";
60    pub const CONNECTION: &str = "connection";
61    pub const DATE: &str = "date";
62    pub const EXPECT: &str = "expect";
63
64    /// Authentication and caching related headers.
65    pub const AUTHORIZATION: &str = "authorization";
66    pub const CACHE_CONTROL: &str = "cache-control";
67    pub const ETAG: &str = "etag";
68    pub const EXPIRES: &str = "expires";
69    pub const IF_MATCH: &str = "if-match";
70    pub const IF_MODIFIED_SINCE: &str = "if-modified-since";
71    pub const IF_UNMODIFIED_SINCE: &str = "if-unmodified-since";
72    pub const IF_NONE_MATCH: &str = "if-none-match";
73    pub const IF_RANGE: &str = "if-range";
74    pub const LAST_MODIFIED: &str = "last-modified";
75    pub const AGE: &str = "age";
76    pub const VARY: &str = "vary";
77    pub const PRAGMA: &str = "pragma";
78    pub const RANGE: &str = "range";
79    pub const CONTENT_RANGE: &str = "content-range";
80    pub const ACCEPT_RANGES: &str = "accept-ranges";
81    pub const ALLOW: &str = "allow";
82
83    /// Content negotiation headers.
84    pub const ACCEPT: &str = "accept";
85    pub const ACCEPT_ENCODING: &str = "accept-encoding";
86    pub const ACCEPT_LANGUAGE: &str = "accept-language";
87
88    /// Miscellaneous headers often used in examples.
89    pub const USER_AGENT: &str = "user-agent";
90    pub const SERVER: &str = "server";
91    pub const SET_COOKIE: &str = "set-cookie";
92    pub const COOKIE: &str = "cookie";
93    pub const LOCATION: &str = "location";
94    pub const UPGRADE: &str = "upgrade";
95}
96
97/// Represents HTTP headers with support for multiple values per key.
98#[derive(Debug, Clone)]
99pub struct Headers {
100    map: HashMap<HeaderName, Vec<String>>,
101    order: Vec<HeaderName>,
102}
103
104impl Headers {
105    /// Create a new empty header collection.
106    pub fn new() -> Self {
107        Headers {
108            map: HashMap::new(),
109            order: Vec::new(),
110        }
111    }
112
113    /// Insert a header, replacing any existing values.
114    pub fn insert(&mut self, name: impl Into<HeaderName>, value: impl Into<String>) {
115        let name = name.into();
116        let value = value.into();
117
118        if !self.map.contains_key(&name) {
119            self.order.push(name.clone());
120        }
121
122        self.map.insert(name, vec![value]);
123    }
124
125    /// Append a header value (for headers that can have multiple values).
126    pub fn append(&mut self, name: impl Into<HeaderName>, value: impl Into<String>) {
127        let name = name.into();
128        let value = value.into();
129
130        if !self.map.contains_key(&name) {
131            self.order.push(name.clone());
132        }
133
134        self.map.entry(name).or_insert_with(Vec::new).push(value);
135    }
136
137    /// Get the first value for a header (most common use case).
138    pub fn get(&self, name: impl Into<HeaderName>) -> Option<&str> {
139        let name = name.into();
140        self.map
141            .get(&name)
142            .and_then(|values| values.first().map(|s| s.as_str()))
143    }
144
145    /// Get all values for a header.
146    pub fn get_all(&self, name: impl Into<HeaderName>) -> Option<&[String]> {
147        let name = name.into();
148        self.map.get(&name).map(|v| v.as_slice())
149    }
150
151    /// Check if a header exists.
152    pub fn contains(&self, name: impl Into<HeaderName>) -> bool {
153        let name = name.into();
154        self.map.contains_key(&name)
155    }
156
157    /// Remove a header and return its values.
158    pub fn remove(&mut self, name: impl Into<HeaderName>) -> Option<Vec<String>> {
159        let name = name.into();
160        if let Some(values) = self.map.remove(&name) {
161            self.order.retain(|n| n != &name);
162            Some(values)
163        } else {
164            None
165        }
166    }
167
168    /// Get the number of unique header names.
169    pub fn len(&self) -> usize {
170        self.map.len()
171    }
172
173    /// Check if headers are empty.
174    pub fn is_empty(&self) -> bool {
175        self.map.is_empty()
176    }
177
178    /// Iterate over headers in insertion order.
179    pub fn iter(&self) -> impl Iterator<Item = (&HeaderName, &Vec<String>)> {
180        self.order
181            .iter()
182            .filter_map(move |name| self.map.get(name).map(|values| (name, values)))
183    }
184
185    /// Clear all headers.
186    pub fn clear(&mut self) {
187        self.map.clear();
188        self.order.clear();
189    }
190}
191
192impl Default for Headers {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198impl fmt::Display for Headers {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        for (name, values) in self.iter() {
201            for value in values {
202                writeln!(f, "{}: {}", name, value)?;
203            }
204        }
205        Ok(())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_case_insensitive_header_name() {
215        let name1 = HeaderName::new("Content-Type");
216        let name2 = HeaderName::new("content-type");
217        let name3 = HeaderName::new("CONTENT-TYPE");
218
219        assert_eq!(name1, name2);
220        assert_eq!(name2, name3);
221    }
222
223    #[test]
224    fn test_insert_and_get() {
225        let mut headers = Headers::new();
226        headers.insert("Content-Type", "application/json");
227
228        assert_eq!(headers.get("content-type"), Some("application/json"));
229        assert_eq!(headers.get("Content-Type"), Some("application/json"));
230    }
231
232    #[test]
233    fn test_append_multiple_values() {
234        let mut headers = Headers::new();
235        headers.append("Accept", "text/html");
236        headers.append("Accept", "application/json");
237
238        let values = headers.get_all("accept").unwrap();
239        assert_eq!(values.len(), 2);
240        assert_eq!(values[0], "text/html");
241        assert_eq!(values[1], "application/json");
242    }
243
244    #[test]
245    fn test_insert_replaces() {
246        let mut headers = Headers::new();
247        headers.insert("Content-Type", "text/html");
248        headers.insert("Content-Type", "application/json");
249
250        let values = headers.get_all("content-type").unwrap();
251        assert_eq!(values.len(), 1);
252        assert_eq!(values[0], "application/json");
253    }
254
255    #[test]
256    fn test_remove() {
257        let mut headers = Headers::new();
258        headers.insert("Content-Type", "application/json");
259
260        let removed = headers.remove("content-type");
261        assert!(removed.is_some());
262        assert_eq!(removed.unwrap(), vec!["application/json"]);
263        assert!(headers.get("content-type").is_none());
264    }
265
266    #[test]
267    fn test_order_preservation() {
268        let mut headers = Headers::new();
269        headers.insert("Content-Type", "application/json");
270        headers.insert("Content-Length", "1234");
271        headers.insert("Host", "example.com");
272
273        let order: Vec<String> = headers
274            .iter()
275            .map(|(name, _)| name.as_str().to_string())
276            .collect();
277
278        assert_eq!(order, vec!["content-type", "content-length", "host"]);
279    }
280}