1use std::{
4 convert::TryInto,
5 fmt::Display,
6 time::{Duration, SystemTime},
7};
8
9use crate::http::{self, header, response::Builder, HeaderMap, StatusCode};
10
11pub trait SputnikBuilder {
13 fn content_type(self, mime: mime::Mime) -> Builder;
15
16 fn set_cookie(self, cookie: Cookie) -> Builder;
18}
19
20#[derive(Default, Debug)]
21pub struct Cookie {
22 pub name: String,
23 pub value: String,
24 pub expires: Option<SystemTime>,
25 pub max_age: Option<Duration>,
26 pub domain: Option<String>,
27 pub path: Option<String>,
28 pub secure: Option<bool>,
29 pub http_only: Option<bool>,
30 pub same_site: Option<SameSite>,
31}
32
33impl Display for Cookie {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "{}={}", self.name, self.value)?;
36 if let Some(true) = self.http_only {
37 write!(f, "; HttpOnly")?;
38 }
39 if let Some(same_site) = &self.same_site {
40 write!(f, "; SameSite={}", same_site)?;
41
42 if same_site == &SameSite::None && self.secure.is_none() {
43 write!(f, "; Secure")?;
44 }
45 }
46 if let Some(true) = self.secure {
47 write!(f, "; Secure")?;
48 }
49 if let Some(path) = &self.path {
50 write!(f, "; Path={}", path)?;
51 }
52 if let Some(domain) = &self.domain {
53 write!(f, "; Domain={}", domain)?;
54 }
55 if let Some(max_age) = &self.max_age {
56 write!(f, "; Max-Age={}", max_age.as_secs())?;
57 }
58 if let Some(time) = self.expires {
59 write!(f, "; Expires={}", httpdate::fmt_http_date(time))?;
60 }
61
62 Ok(())
63 }
64}
65
66#[derive(Debug, PartialEq)]
67pub enum SameSite {
68 Strict,
69 Lax,
70 None,
71}
72
73impl Display for SameSite {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 SameSite::Strict => write!(f, "Strict"),
77 SameSite::Lax => write!(f, "Lax"),
78 SameSite::None => write!(f, "None"),
79 }
80 }
81}
82
83pub fn redirect(location: &str, code: StatusCode) -> Builder {
85 Builder::new()
86 .status(code)
87 .header(header::LOCATION, location)
88}
89
90impl SputnikBuilder for Builder {
91 fn content_type(mut self, mime: mime::Mime) -> Self {
92 self.headers_mut().map(|h| h.content_type(mime));
93 self
94 }
95
96 fn set_cookie(mut self, cookie: Cookie) -> Builder {
97 self.headers_mut().map(|h| h.set_cookie(cookie));
98 self
99 }
100}
101
102pub fn delete_cookie(name: &str) -> Cookie {
104 Cookie {
105 name: name.into(),
106 max_age: Some(Duration::from_secs(0)),
107 expires: Some(SystemTime::now() - Duration::from_secs(60 * 60 * 24)),
108 ..Default::default()
109 }
110}
111
112pub trait SputnikHeaders {
114 fn content_type(&mut self, mime: mime::Mime);
116
117 fn set_cookie(&mut self, cookie: Cookie);
119}
120
121impl SputnikHeaders for HeaderMap {
122 fn content_type(&mut self, mime: mime::Mime) {
123 self.insert(header::CONTENT_TYPE, mime.to_string().try_into().unwrap());
124 }
125
126 fn set_cookie(&mut self, cookie: Cookie) {
127 self.append(header::SET_COOKIE, cookie.to_string().try_into().unwrap());
128 }
129}
130
131pub trait EmptyBuilder<B> {
133 fn empty(self) -> http::Result<http::response::Response<B>>;
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_set_cookie() {
143 let mut map = HeaderMap::new();
144 map.set_cookie(Cookie {
145 name: "some".into(),
146 value: "cookie".into(),
147 ..Default::default()
148 });
149 map.set_cookie(Cookie {
150 name: "some".into(),
151 value: "cookie".into(),
152 ..Default::default()
153 });
154 assert_eq!(map.len(), 2);
155 }
156
157 #[test]
158 fn test_content_type() {
159 let mut map = HeaderMap::new();
160 map.content_type(mime::TEXT_PLAIN);
161 map.content_type(mime::TEXT_HTML);
162 assert_eq!(map.len(), 1);
163 assert_eq!(map.get(header::CONTENT_TYPE).unwrap(), "text/html");
164 }
165}