cookie_monster/jar.rs
1use std::{borrow::Borrow, collections::HashSet, fmt::Debug, hash::Hash};
2
3use crate::Cookie;
4
5/// A generic `CookieJar` for cookie management. Can be used to read update or delete cookies from
6/// a user session.
7///
8/// ## `axum` feature
9///
10/// Note that to set the cookies, the jar _must_ be returned from the handler. Otherwise the
11/// cookies are not updated.
12///
13///
14/// ## Example
15/// ```rust
16/// use cookie_monster::{CookieJar, Cookie};
17///
18/// static COOKIE_NAME: &str = "session";
19///
20/// async fn handler(mut jar: CookieJar) -> CookieJar {
21///
22/// if let Some(cookie) = jar.get(COOKIE_NAME) {
23/// println!("Removing cookie {cookie:?}");
24/// jar.remove(Cookie::named(COOKIE_NAME));
25/// } else {
26/// let cookie = Cookie::new(COOKIE_NAME, "hello, world");
27/// println!("Setting cookie {cookie:?}");
28/// jar.add(cookie);
29/// }
30///
31/// // Important, return the jar to update the cookies!
32/// jar
33/// }
34/// ```
35#[derive(Default, Debug)]
36pub struct CookieJar {
37 cookies: HashSet<HashCookie>,
38}
39
40pub(crate) enum HashCookie {
41 // An original cookie. These should never be sent back to the user-agent.
42 Original(Cookie),
43 // A new cookie, the should always be sent back to the user-agent.
44 New(Cookie),
45 // A removed cookie, the should always be sent back to the user-agent but should never be
46 // visible by the user.
47 Removal(Cookie),
48}
49
50impl HashCookie {
51 fn name(&self) -> &str {
52 match self {
53 HashCookie::Original(c) => c.name(),
54 HashCookie::New(c) => c.name(),
55 HashCookie::Removal(c) => c.name(),
56 }
57 }
58}
59
60impl Hash for HashCookie {
61 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
62 self.name().hash(state);
63 }
64}
65
66impl PartialEq for HashCookie {
67 fn eq(&self, other: &Self) -> bool {
68 self.name() == other.name()
69 }
70}
71
72impl Borrow<str> for HashCookie {
73 fn borrow(&self) -> &str {
74 self.name()
75 }
76}
77
78impl Eq for HashCookie {}
79
80impl Debug for HashCookie {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 HashCookie::Original(cookie) => cookie.fmt(f),
84 HashCookie::New(cookie) => cookie.fmt(f),
85 HashCookie::Removal(cookie) => cookie.fmt(f),
86 }
87 }
88}
89
90impl CookieJar {
91 /// Creates an empty `CookieJar`.
92 pub fn empty() -> Self {
93 Self::default()
94 }
95
96 /// Parses the given `cookie` header value and return a `CookieJar`. This function ignores
97 /// cookies that were not able to be parsed.
98 pub fn from_cookie(header: &str) -> Self {
99 Self::from_original(header.split(';').flat_map(Cookie::parse_cookie))
100 }
101
102 /// Parses the given `cookie` header value and return a `CookieJar`. The cookie name and values
103 /// are percent-decoded. Cookies that were not able to be parsed are ignored.
104 #[cfg(feature = "percent-encode")]
105 pub fn from_encoded_cookie(header: &str) -> Self {
106 Self::from_original(header.split(';').flat_map(Cookie::parse_cookie_encoded))
107 }
108
109 /// Adds an __original__ cookie to the jar. These are never sent back to the
110 /// user-agent, but are visible in the cookie jar.
111 pub fn add_original(&mut self, cookie: Cookie) {
112 self.cookies.insert(HashCookie::Original(cookie));
113 }
114
115 // Creates a `CookieJar` from an iterator of cookies. It is assumed that the cookies are
116 // __original__. E.g. from a `Cookie` header value.
117 pub fn from_original<T: IntoIterator<Item = Cookie>>(cookies: T) -> Self {
118 let mut jar = Self::empty();
119
120 for cookie in cookies {
121 jar.add_original(cookie);
122 }
123
124 jar
125 }
126
127 /// Get a cookie by name. Gives back either an __original__ or newly added cookie.
128 pub fn get(&self, name: &str) -> Option<&Cookie> {
129 self.cookies.get(name).and_then(|c| match c {
130 HashCookie::New(c) | HashCookie::Original(c) => Some(c),
131 HashCookie::Removal(_) => None,
132 })
133 }
134
135 /// Iterate over all changes. This returns all removed and newly created cookies.
136 pub fn set_cookie_headers(&self) -> impl Iterator<Item = crate::Result<String>> {
137 self.cookies
138 .iter()
139 .filter_map(|c| match c {
140 HashCookie::Original(_) => None,
141 HashCookie::New(c) | HashCookie::Removal(c) => Some(c),
142 })
143 .map(Cookie::serialize)
144 }
145
146 /// Removes the cookie from the local cookie store and issues a cookie with an Expires
147 /// attribute in the past and Max-Age of 0 seconds.
148 ///
149 /// If one of the `time`, `chrono` or `jiff` features are enabled, the Expires tag is set to the
150 /// current time minus one year. If none of the those features are enabled, the Expires
151 /// attribute is set to 1 Jan 1970 00:00.
152 ///
153 /// **To ensure a cookie is removed from the user-agent, set the `Path` and `Domain` attributes
154 /// with the same values that were used to create the cookie.**
155 pub fn remove(&mut self, cookie: impl Into<Cookie>) {
156 let cookie = HashCookie::Removal(cookie.into().into_remove());
157 self.cookies.replace(cookie);
158 }
159
160 /// Adds a cookie to the jar. If a cookie with the same name is already in the jar, it is
161 /// replaced with the given cookie.
162 pub fn add(&mut self, cookie: impl Into<Cookie>) {
163 self.cookies.replace(HashCookie::New(cookie.into()));
164 }
165
166 #[allow(unused)]
167 pub(crate) fn iter_non_original(&self) -> impl Iterator<Item = &Cookie> {
168 self.cookies.iter().flat_map(|cookie| match cookie {
169 HashCookie::Original(_) => None,
170 HashCookie::New(cookie) | HashCookie::Removal(cookie) => Some(cookie),
171 })
172 }
173}