use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub struct Cookie {
name: String,
value: String,
domain: Option<String>,
path: Option<String>,
max_age: Option<i64>,
secure: bool,
http_only: bool,
same_site: Option<SameSite>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SameSite {
Strict,
Lax,
None,
}
impl Cookie {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
let name = name.into();
let value = value.into();
if name.contains(|c: char| c == ';' || c == '=' || c.is_control()) {
panic!("Invalid cookie name: contains forbidden characters");
}
if value.contains(|c: char| c == ';' || c.is_control()) {
panic!("Invalid cookie value: contains forbidden characters");
}
Self {
name,
value,
domain: None,
path: Some("/".to_string()),
max_age: None,
secure: false,
http_only: false,
same_site: None,
}
}
pub fn domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
pub fn max_age(mut self, seconds: i64) -> Self {
self.max_age = Some(seconds);
self
}
pub fn secure(mut self) -> Self {
self.secure = true;
self
}
pub fn http_only(mut self) -> Self {
self.http_only = true;
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
if same_site == SameSite::None {
self.secure = true;
}
self.same_site = Some(same_site);
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn value(&self) -> &str {
&self.value
}
pub fn to_header_value(&self) -> String {
let mut parts = vec![format!("{}={}", self.name, self.value)];
if let Some(domain) = &self.domain {
parts.push(format!("Domain={}", domain));
}
if let Some(path) = &self.path {
parts.push(format!("Path={}", path));
}
if let Some(max_age) = self.max_age {
parts.push(format!("Max-Age={}", max_age));
}
if self.secure {
parts.push("Secure".to_string());
}
if self.http_only {
parts.push("HttpOnly".to_string());
}
if let Some(same_site) = self.same_site {
parts.push(format!("SameSite={}", same_site));
}
parts.join("; ")
}
}
impl fmt::Display for SameSite {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SameSite::Strict => write!(f, "Strict"),
SameSite::Lax => write!(f, "Lax"),
SameSite::None => write!(f, "None"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Cookies {
cookies: HashMap<String, String>,
}
impl Cookies {
pub fn new() -> Self {
Self::default()
}
pub fn parse(cookie_header: &str) -> Self {
let mut cookies = HashMap::new();
for pair in cookie_header.split(';') {
let pair = pair.trim();
if let Some((name, value)) = pair.split_once('=') {
cookies.insert(name.trim().to_string(), value.trim().to_string());
}
}
Self { cookies }
}
pub fn get(&self, name: &str) -> Option<&str> {
self.cookies.get(name).map(|s| s.as_str())
}
pub fn contains(&self, name: &str) -> bool {
self.cookies.contains_key(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.cookies.keys().map(|s| s.as_str())
}
pub fn len(&self) -> usize {
self.cookies.len()
}
pub fn is_empty(&self) -> bool {
self.cookies.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cookie_basic() {
let cookie = Cookie::new("session", "abc123");
assert_eq!(cookie.name(), "session");
assert_eq!(cookie.value(), "abc123");
}
#[test]
fn test_cookie_to_header() {
let cookie = Cookie::new("session", "abc123")
.path("/")
.max_age(3600)
.http_only()
.secure()
.same_site(SameSite::Strict);
let header = cookie.to_header_value();
assert!(header.contains("session=abc123"));
assert!(header.contains("Path=/"));
assert!(header.contains("Max-Age=3600"));
assert!(header.contains("HttpOnly"));
assert!(header.contains("Secure"));
assert!(header.contains("SameSite=Strict"));
}
#[test]
fn test_cookies_parse() {
let cookies = Cookies::parse("session=abc123; user=john; theme=dark");
assert_eq!(cookies.get("session"), Some("abc123"));
assert_eq!(cookies.get("user"), Some("john"));
assert_eq!(cookies.get("theme"), Some("dark"));
assert_eq!(cookies.len(), 3);
}
#[test]
fn test_cookies_parse_with_spaces() {
let cookies = Cookies::parse("session=abc123 ; user = john ; theme=dark");
assert_eq!(cookies.get("session"), Some("abc123"));
assert_eq!(cookies.get("user"), Some("john"));
assert_eq!(cookies.get("theme"), Some("dark"));
}
#[test]
fn test_cookies_empty() {
let cookies = Cookies::new();
assert!(cookies.is_empty());
assert_eq!(cookies.len(), 0);
}
}