1use std::time::{Duration, SystemTime};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub enum SameSite {
6 Strict,
8 Lax,
10 None,
12}
13
14#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct Cookie {
17 name: String,
18 value: String,
19 http_only: bool,
20 secure: bool,
21 same_site: SameSite,
22 path: Option<String>,
23 domain: Option<String>,
24 max_age: Option<Duration>,
25 expires: Option<SystemTime>,
26}
27
28impl Cookie {
29 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
31 Self {
32 name: name.into(),
33 value: value.into(),
34 http_only: true,
35 secure: true,
36 same_site: SameSite::Lax,
37 path: Some("/".into()),
38 domain: None,
39 max_age: None,
40 expires: None,
41 }
42 }
43
44 pub fn removal(name: impl Into<String>) -> Self {
46 Self::new(name, "")
47 .max_age(Duration::ZERO)
48 .expires(SystemTime::UNIX_EPOCH)
49 }
50
51 pub fn http_only(mut self, value: bool) -> Self {
53 self.http_only = value;
54 self
55 }
56 pub fn secure(mut self, value: bool) -> Self {
58 self.secure = value;
59 self
60 }
61 pub fn same_site(mut self, value: SameSite) -> Self {
63 self.same_site = value;
64 self
65 }
66 pub fn path(mut self, value: impl Into<String>) -> Self {
68 self.path = Some(value.into());
69 self
70 }
71 pub fn domain(mut self, value: impl Into<String>) -> Self {
73 self.domain = Some(value.into());
74 self
75 }
76 pub fn max_age(mut self, value: Duration) -> Self {
78 self.max_age = Some(value);
79 self
80 }
81 pub fn expires(mut self, value: SystemTime) -> Self {
83 self.expires = Some(value);
84 self
85 }
86
87 pub fn name(&self) -> &str {
89 &self.name
90 }
91 pub fn value(&self) -> &str {
93 &self.value
94 }
95 pub fn is_http_only(&self) -> bool {
97 self.http_only
98 }
99 pub fn is_secure(&self) -> bool {
101 self.secure
102 }
103 pub fn same_site_value(&self) -> SameSite {
105 self.same_site
106 }
107 pub fn path_value(&self) -> Option<&str> {
109 self.path.as_deref()
110 }
111 pub fn domain_value(&self) -> Option<&str> {
113 self.domain.as_deref()
114 }
115 pub fn max_age_value(&self) -> Option<Duration> {
117 self.max_age
118 }
119 pub fn expires_value(&self) -> Option<SystemTime> {
121 self.expires
122 }
123
124 pub fn to_header_value(&self) -> String {
126 let mut cookie = cookie::Cookie::new(self.name.clone(), self.value.clone());
127 cookie.set_http_only(self.http_only);
128 cookie.set_secure(self.secure);
129 cookie.set_same_site(match self.same_site {
130 SameSite::Strict => cookie::SameSite::Strict,
131 SameSite::Lax => cookie::SameSite::Lax,
132 SameSite::None => cookie::SameSite::None,
133 });
134 if let Some(path) = &self.path {
135 cookie.set_path(path.clone());
136 }
137 if let Some(domain) = &self.domain {
138 cookie.set_domain(domain.clone());
139 }
140 if let Some(max_age) = self.max_age {
141 cookie.set_max_age(
142 cookie::time::Duration::try_from(max_age).unwrap_or(cookie::time::Duration::MAX),
143 );
144 }
145 if let Some(expires) = self.expires {
146 cookie.set_expires(cookie::time::OffsetDateTime::from(expires));
147 }
148 cookie.encoded().to_string()
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn defaults_and_removal_are_safe() {
158 let cookie = Cookie::new("session", "secret");
159 assert!(cookie.is_http_only());
160 assert!(cookie.is_secure());
161 assert_eq!(cookie.same_site_value(), SameSite::Lax);
162 assert_eq!(cookie.path_value(), Some("/"));
163
164 let removal = Cookie::removal("session")
165 .domain("example.com")
166 .path("/auth");
167 assert_eq!(removal.value(), "");
168 assert_eq!(removal.max_age_value(), Some(Duration::ZERO));
169 assert_eq!(removal.expires_value(), Some(SystemTime::UNIX_EPOCH));
170 assert_eq!(removal.domain_value(), Some("example.com"));
171 assert_eq!(removal.path_value(), Some("/auth"));
172 }
173}