sputnik/
response.rs

1//! Provides convenience traits and functions to build HTTP responses.
2
3use std::{
4    convert::TryInto,
5    fmt::Display,
6    time::{Duration, SystemTime},
7};
8
9use crate::http::{self, header, response::Builder, HeaderMap, StatusCode};
10
11/// Adds convenience methods to [`Builder`].
12pub trait SputnikBuilder {
13    /// Sets the Content-Type.
14    fn content_type(self, mime: mime::Mime) -> Builder;
15
16    /// Appends the Set-Cookie header.
17    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
83/// Creates a new builder with a given Location header and status code.
84pub 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
102/// Constructs an expired cookie to delete a cookie.
103pub 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
112/// Adds convenience methods to [`HeaderMap`].
113pub trait SputnikHeaders {
114    /// Sets the Content-Type.
115    fn content_type(&mut self, mime: mime::Mime);
116
117    /// Appends a Set-Cookie header.
118    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
131/// Adds a convenience method to consume a [`Builder`] with an empty body.
132pub trait EmptyBuilder<B> {
133    /// Consume the builder with an empty body.
134    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}