#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Headers {
values: Vec<(String, Vec<String>)>,
}
impl Headers {
pub fn new() -> Headers {
Headers::default()
}
pub fn from_pairs<K: AsRef<str>, V: AsRef<str>>(pairs: impl IntoIterator<Item = (K, V)>) -> Headers {
let mut h = Headers::new();
for (k, v) in pairs {
h.append(k.as_ref(), v.as_ref());
}
h
}
pub fn with(mut self, name: &str, value: impl AsRef<str>) -> Headers {
self.set(name, value.as_ref());
self
}
pub fn get(&self, name: &str) -> Option<String> {
let name = name.to_ascii_lowercase();
self.values.iter().find(|(n, _)| *n == name).map(|(_, v)| v.join(", "))
}
pub fn has(&self, name: &str) -> bool {
let name = name.to_ascii_lowercase();
self.values.iter().any(|(n, _)| *n == name)
}
pub fn set(&mut self, name: &str, value: &str) {
let name = name.to_ascii_lowercase();
let value = clean(value);
match self.values.iter_mut().find(|(n, _)| *n == name) {
Some(entry) => entry.1 = vec![value],
None => self.values.push((name, vec![value])),
}
}
pub fn append(&mut self, name: &str, value: &str) {
let name = name.to_ascii_lowercase();
let value = clean(value);
match self.values.iter_mut().find(|(n, _)| *n == name) {
Some(entry) => entry.1.push(value),
None => self.values.push((name, vec![value])),
}
}
pub fn delete(&mut self, name: &str) {
let name = name.to_ascii_lowercase();
self.values.retain(|(n, _)| *n != name);
}
pub fn get_set_cookie(&self) -> Vec<String> {
self.values.iter().find(|(n, _)| n == "set-cookie").map(|(_, v)| v.clone()).unwrap_or_default()
}
pub fn all(&self) -> &[(String, Vec<String>)] {
&self.values
}
pub fn entries(&self) -> Vec<(String, String)> {
let mut names: Vec<&(String, Vec<String>)> = self.values.iter().collect();
names.sort_by(|a, b| a.0.cmp(&b.0));
let mut out = Vec::new();
for (name, values) in names {
if name == "set-cookie" {
for v in values {
out.push((name.clone(), v.clone()));
}
} else {
out.push((name.clone(), values.join(", ")));
}
}
out
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
fn clean(value: &str) -> String {
let v: String = value.chars().filter(|c| !matches!(c, '\r' | '\n' | '\0')).collect();
v.trim_matches(|c| c == ' ' || c == '\t').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headers_read_as_fetch_reads_them() {
let mut h = Headers::new();
h.append("Accept", "a");
h.append("accept", "b");
h.append("Set-Cookie", "x=1, y");
h.append("set-cookie", "z=2");
h.set("X-Test", " v\r\n ");
assert_eq!(h.get("ACCEPT").as_deref(), Some("a, b"));
assert_eq!(h.get_set_cookie(), vec!["x=1, y", "z=2"]);
assert_eq!(h.get("x-test").as_deref(), Some("v"));
assert_eq!(h.entries()[0], ("accept".to_string(), "a, b".to_string()));
h.delete("accept");
assert!(!h.has("Accept"));
}
}