Skip to main content

pick/formats/
headers.rs

1use crate::error::PickError;
2use serde_json::Value;
3
4pub fn parse(input: &str) -> Result<Value, PickError> {
5    let mut map = serde_json::Map::new();
6
7    for line in input.lines() {
8        let line = line.trim();
9
10        // Skip empty lines and HTTP status lines
11        if line.is_empty() || line.starts_with("HTTP/") {
12            continue;
13        }
14
15        if let Some(colon_pos) = line.find(':') {
16            let key = line[..colon_pos].trim().to_lowercase();
17            let value = line[colon_pos + 1..].trim();
18            match map.entry(key) {
19                serde_json::map::Entry::Occupied(mut e) => {
20                    if let Value::String(existing) = e.get() {
21                        let combined = format!("{}, {}", existing, value);
22                        e.insert(Value::String(combined));
23                    }
24                }
25                serde_json::map::Entry::Vacant(e) => {
26                    e.insert(Value::String(value.to_string()));
27                }
28            }
29        }
30    }
31
32    if map.is_empty() {
33        return Err(PickError::ParseError(
34            "headers".into(),
35            "no headers found".into(),
36        ));
37    }
38
39    Ok(Value::Object(map))
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use serde_json::json;
46
47    #[test]
48    fn parse_standard_headers() {
49        let input = "Content-Type: application/json\nContent-Length: 1234";
50        let v = parse(input).unwrap();
51        assert_eq!(v["content-type"], json!("application/json"));
52        assert_eq!(v["content-length"], json!("1234"));
53    }
54
55    #[test]
56    fn parse_with_status_line() {
57        let input = "HTTP/1.1 200 OK\nContent-Type: text/html\nServer: nginx";
58        let v = parse(input).unwrap();
59        assert_eq!(v["content-type"], json!("text/html"));
60        assert_eq!(v["server"], json!("nginx"));
61        // HTTP status line should be skipped
62        assert!(v.get("http/1.1 200 ok").is_none());
63    }
64
65    #[test]
66    fn parse_case_insensitive_keys() {
67        let input = "Content-Type: text/html\nX-REQUEST-ID: abc123";
68        let v = parse(input).unwrap();
69        assert_eq!(v["content-type"], json!("text/html"));
70        assert_eq!(v["x-request-id"], json!("abc123"));
71    }
72
73    #[test]
74    fn parse_value_with_colon() {
75        let input = "Location: https://example.com:8080/path";
76        let v = parse(input).unwrap();
77        assert_eq!(v["location"], json!("https://example.com:8080/path"));
78    }
79
80    #[test]
81    fn parse_empty_value() {
82        let input = "X-Empty:\nContent-Type: text/html";
83        let v = parse(input).unwrap();
84        assert_eq!(v["x-empty"], json!(""));
85    }
86
87    #[test]
88    fn parse_with_empty_lines() {
89        let input = "Content-Type: text/html\n\nX-After: value";
90        let v = parse(input).unwrap();
91        assert_eq!(v["content-type"], json!("text/html"));
92        assert_eq!(v["x-after"], json!("value"));
93    }
94
95    #[test]
96    fn parse_empty_input() {
97        assert!(parse("").is_err());
98    }
99
100    #[test]
101    fn parse_only_status_line() {
102        assert!(parse("HTTP/1.1 200 OK").is_err());
103    }
104
105    #[test]
106    fn parse_rate_limit_headers() {
107        let input =
108            "X-RateLimit-Limit: 100\nX-RateLimit-Remaining: 42\nX-RateLimit-Reset: 1609459200";
109        let v = parse(input).unwrap();
110        assert_eq!(v["x-ratelimit-remaining"], json!("42"));
111    }
112}