use std::{convert::TryInto, fmt::Display, time::{Duration, SystemTime}};
use crate::http::{self, HeaderMap, StatusCode, header, response::Builder};
pub trait SputnikBuilder {
fn content_type(self, mime: mime::Mime) -> Builder;
fn set_cookie(self, cookie: Cookie) -> Builder;
}
#[derive(Default, Debug)]
pub struct Cookie {
pub name: String,
pub value: String,
pub expires: Option<SystemTime>,
pub max_age: Option<Duration>,
pub domain: Option<String>,
pub path: Option<String>,
pub secure: Option<bool>,
pub http_only: Option<bool>,
pub same_site: Option<SameSite>,
}
impl Display for Cookie {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}={}", self.name, self.value)?;
if let Some(true) = self.http_only {
write!(f, "; HttpOnly")?;
}
if let Some(same_site) = &self.same_site {
write!(f, "; SameSite={}", same_site)?;
if same_site == &SameSite::None && self.secure.is_none() {
write!(f, "; Secure")?;
}
}
if let Some(true) = self.secure {
write!(f, "; Secure")?;
}
if let Some(path) = &self.path {
write!(f, "; Path={}", path)?;
}
if let Some(domain) = &self.domain {
write!(f, "; Domain={}", domain)?;
}
if let Some(max_age) = &self.max_age {
write!(f, "; Max-Age={}", max_age.as_secs())?;
}
if let Some(time) = self.expires {
write!(f, "; Expires={}", httpdate::fmt_http_date(time))?;
}
Ok(())
}
}
#[derive(Debug, PartialEq)]
pub enum SameSite {
Strict, Lax, None
}
impl Display for SameSite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SameSite::Strict => write!(f, "Strict"),
SameSite::Lax => write!(f, "Lax"),
SameSite::None => write!(f, "None"),
}
}
}
pub fn redirect(location: &str, code: StatusCode) -> Builder {
Builder::new().status(code).header(header::LOCATION, location)
}
impl SputnikBuilder for Builder {
fn content_type(mut self, mime: mime::Mime) -> Self {
self.headers_mut().map(|h| h.content_type(mime));
self
}
fn set_cookie(mut self, cookie: Cookie) -> Builder {
self.headers_mut().map(|h| h.set_cookie(cookie));
self
}
}
pub fn delete_cookie(name: &str) -> Cookie {
Cookie{
name: name.into(),
max_age: Some(Duration::from_secs(0)),
expires: Some(SystemTime::now() - Duration::from_secs(60*60*24)),
..Default::default()
}
}
pub trait SputnikHeaders {
fn content_type(&mut self, mime: mime::Mime);
fn set_cookie(&mut self, cookie: Cookie);
}
impl SputnikHeaders for HeaderMap {
fn content_type(&mut self, mime: mime::Mime) {
self.insert(header::CONTENT_TYPE, mime.to_string().try_into().unwrap());
}
fn set_cookie(&mut self, cookie: Cookie) {
self.append(header::SET_COOKIE, cookie.to_string().try_into().unwrap());
}
}
pub trait EmptyBuilder<B> {
fn empty(self) -> http::Result<http::response::Response<B>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_set_cookie() {
let mut map = HeaderMap::new();
map.set_cookie(Cookie{name: "some".into(), value: "cookie".into(), ..Default::default()});
map.set_cookie(Cookie{name: "some".into(), value: "cookie".into(), ..Default::default()});
assert_eq!(map.len(), 2);
}
#[test]
fn test_content_type() {
let mut map = HeaderMap::new();
map.content_type(mime::TEXT_PLAIN);
map.content_type(mime::TEXT_HTML);
assert_eq!(map.len(), 1);
assert_eq!(map.get(header::CONTENT_TYPE).unwrap(), "text/html");
}
}