use std::collections::HashMap;
use http::header::{COOKIE, SET_COOKIE};
use http::HeaderMap;
#[derive(Default, Clone)]
pub(crate) struct CookieJar {
entries: HashMap<String, String>,
}
impl CookieJar {
pub(crate) fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.entries.insert(name.into(), value.into());
}
pub(crate) fn store(&mut self, headers: &HeaderMap) {
for value in headers.get_all(SET_COOKIE).iter() {
let Ok(text) = value.to_str() else {
continue;
};
let pair = text.split(';').next().unwrap_or(text).trim();
if let Some((name, value)) = pair.split_once('=') {
self.entries
.insert(name.trim().to_owned(), value.trim().to_owned());
}
}
}
pub(crate) fn header_value(&self) -> Option<String> {
if self.entries.is_empty() {
return None;
}
let mut pairs: Vec<String> = self
.entries
.iter()
.map(|(name, value)| format!("{name}={value}"))
.collect();
pairs.sort();
Some(pairs.join("; "))
}
pub(crate) fn apply(&self, headers: &mut HeaderMap) {
if let Some(value) = self.header_value() {
if let Ok(value) = value.parse() {
headers.insert(COOKIE, value);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderValue;
#[test]
fn store_overwrites_values_and_sorts_header_output() {
let mut headers = HeaderMap::new();
headers.append(SET_COOKIE, HeaderValue::from_static("session=one; Path=/"));
headers.append(SET_COOKIE, HeaderValue::from_static("theme=dark; HttpOnly"));
headers.append(SET_COOKIE, HeaderValue::from_static("session=two; Secure"));
let mut jar = CookieJar::default();
jar.store(&headers);
assert_eq!(
jar.header_value().as_deref(),
Some("session=two; theme=dark")
);
}
#[test]
fn apply_uses_seeded_cookie_entries() {
let mut jar = CookieJar::default();
jar.set("token", "abc");
jar.set("mode", "test");
let mut headers = HeaderMap::new();
jar.apply(&mut headers);
assert_eq!(headers[COOKIE], "mode=test; token=abc");
}
}