pavex/cookie/response_cookies.rs
1use pavex_macros::methods;
2
3use super::{Processor, ResponseCookie, ResponseCookieId};
4use crate::cookie::response::ResponseCookiesIter;
5
6/// A collection of [`ResponseCookie`]s to be attached to an HTTP response
7/// using the `Set-Cookie` header.
8///
9/// # Adding a cookie
10///
11/// A set's life begins via [`ResponseCookies::new()`] and calls to
12/// [`ResponseCookies::insert()`]:
13///
14/// ```rust
15/// use pavex::cookie::{ResponseCookie, ResponseCookies};
16///
17/// let mut set = ResponseCookies::new();
18/// set.insert(("name", "value"));
19/// set.insert(("second", "another"));
20/// set.insert(ResponseCookie::new("third", "again").set_path("/"));
21/// ```
22///
23/// # Removing a cookie
24///
25/// If you want to tell the client to remove a cookie, you need to
26/// insert a [`RemovalCookie`] into the set.
27/// Note that any `T: Into<ResponseCookie>` can be passed into
28/// these methods.
29///
30/// ```rust
31/// use pavex::cookie::{ResponseCookie, ResponseCookies, RemovalCookie, ResponseCookieId};
32///
33/// let mut set = ResponseCookies::new();
34/// let removal = RemovalCookie::new("name").set_path("/");
35/// // This will tell the client to remove the cookie with name "name"
36/// // and path "/".
37/// set.insert(removal);
38///
39/// // If you insert a cookie with the same name and path, it will replace
40/// // the removal cookie.
41/// let cookie = ResponseCookie::new("name", "value").set_path("/");
42/// set.insert(cookie);
43///
44/// let retrieved = set.get(ResponseCookieId::new("name").set_path("/")).unwrap();
45/// assert_eq!(retrieved.value(), "value");
46/// ```
47///
48/// If you want to remove a cookie from the set without telling the client to remove it,
49/// you can use [`ResponseCookies::discard()`].
50///
51/// # Retrieving a cookie
52///
53/// Cookies can be retrieved with [`ResponseCookies::get()`].
54/// Check the method's documentation for more information.
55///
56/// [`RemovalCookie`]: super::RemovalCookie
57#[derive(Default, Debug, Clone)]
58pub struct ResponseCookies(biscotti::ResponseCookies<'static>);
59
60#[methods(pavex = crate)]
61impl ResponseCookies {
62 /// Creates an empty cookie set.
63 ///
64 /// # Example
65 ///
66 /// ```rust
67 /// use pavex::cookie::ResponseCookies;
68 ///
69 /// let set = ResponseCookies::new();
70 /// assert_eq!(set.iter().count(), 0);
71 /// ```
72 #[request_scoped(pavex = crate)]
73 pub fn new() -> ResponseCookies {
74 ResponseCookies::default()
75 }
76
77 /// Returns a reference to the [`ResponseCookie`] inside this set with the specified `id`.
78 ///
79 /// # Via id
80 ///
81 /// ```rust
82 /// use pavex::cookie::{ResponseCookies, ResponseCookie, ResponseCookieId};
83 ///
84 /// let mut set = ResponseCookies::new();
85 /// assert!(set.get("name").is_none());
86 ///
87 /// let cookie = ResponseCookie::new("name", "value").set_path("/");
88 /// set.insert(cookie);
89 ///
90 /// // By specifying just the name, the domain and path are assumed to be None.
91 /// let id = ResponseCookieId::new("name");
92 /// // `name` has a path of `/`, so it doesn't match the empty path.
93 /// assert!(set.get(id).is_none());
94 ///
95 /// let id = ResponseCookieId::new("name").set_path("/");
96 /// // You need to specify a matching path to get the cookie we inserted above.
97 /// assert_eq!(set.get(id).map(|c| c.value()), Some("value"));
98 /// ```
99 ///
100 /// # Via name
101 ///
102 /// ```rust
103 /// use pavex::cookie::{ResponseCookies, ResponseCookie, ResponseCookieId};
104 ///
105 /// let mut set = ResponseCookies::new();
106 /// assert!(set.get("name").is_none());
107 ///
108 /// let cookie = ResponseCookie::new("name", "value");
109 /// set.insert(cookie);
110 ///
111 /// // By specifying just the name, the domain and path are assumed to be None.
112 /// assert_eq!(set.get("name").map(|c| c.value()), Some("value"));
113 /// ```
114 pub fn get<'map, 'key, Key>(&'map self, id: Key) -> Option<&'map ResponseCookie<'static>>
115 where
116 Key: Into<ResponseCookieId<'key>>,
117 {
118 self.0.get(id)
119 }
120
121 /// Inserts `cookie` into this set.
122 /// If a cookie with the same [`ResponseCookieId`] already
123 /// exists, it is replaced with `cookie` and the old cookie is returned.
124 ///
125 /// # Example
126 ///
127 /// ```rust
128 /// use pavex::cookie::{ResponseCookies, ResponseCookie, ResponseCookieId};
129 ///
130 /// let mut set = ResponseCookies::new();
131 /// set.insert(("name", "value"));
132 /// set.insert(("second", "two"));
133 /// // Replaces the "second" cookie with a new one.
134 /// assert!(set.insert(("second", "three")).is_some());
135 ///
136 /// assert_eq!(set.get("name").map(|c| c.value()), Some("value"));
137 /// assert_eq!(set.get("second").map(|c| c.value()), Some("three"));
138 /// assert_eq!(set.iter().count(), 2);
139 ///
140 /// // If we insert another cookie with name "second", but different domain and path,
141 /// // it won't replace the existing one.
142 /// let cookie = ResponseCookie::new("second", "four").set_domain("rust-lang.org");
143 /// set.insert(cookie);
144 ///
145 /// assert_eq!(set.get("second").map(|c| c.value()), Some("three"));
146 /// let id = ResponseCookieId::new("second").set_domain("rust-lang.org");
147 /// assert_eq!(set.get(id).map(|c| c.value()), Some("four"));
148 /// assert_eq!(set.iter().count(), 3);
149 /// ```
150 pub fn insert<C>(&mut self, cookie: C) -> Option<ResponseCookie<'static>>
151 where
152 C: Into<ResponseCookie<'static>>,
153 {
154 self.0.insert(cookie)
155 }
156
157 /// Discard `cookie` from this set.
158 ///
159 /// **`discard` does not instruct the client to remove the cookie**.
160 /// You need to insert a [`RemovalCookie`] into [`ResponseCookies`] to do that.
161 ///
162 /// # Example
163 ///
164 /// ```rust
165 /// use pavex::cookie::{ResponseCookies, ResponseCookie};
166 ///
167 /// let mut set = ResponseCookies::new();
168 /// set.insert(("second", "two"));
169 /// set.discard("second");
170 ///
171 /// assert!(set.get("second").is_none());
172 /// assert_eq!(set.iter().count(), 0);
173 /// ```
174 ///
175 /// # Example with path and domain
176 ///
177 /// A cookie is identified by its name, domain, and path.
178 /// If you want to discard a cookie with a non-empty domain and/or path, you need to specify them.
179 ///
180 /// ```rust
181 /// use pavex::cookie::{ResponseCookies, ResponseCookie, ResponseCookieId};
182 ///
183 /// let mut set = ResponseCookies::new();
184 /// let cookie = ResponseCookie::new("name", "value").set_domain("rust-lang.org").set_path("/");
185 /// let id = cookie.id();
186 /// set.insert((cookie));
187 ///
188 /// // This won't discard the cookie because the path and the domain don't match.
189 /// set.discard("second");
190 /// assert_eq!(set.iter().count(), 1);
191 /// assert!(set.get(id).is_some());
192 ///
193 /// // This will discard the cookie because the name, the path and the domain match.
194 /// let id = ResponseCookieId::new("name").set_domain("rust-lang.org").set_path("/");
195 /// set.discard(id);
196 /// assert_eq!(set.iter().count(), 0);
197 /// ```
198 ///
199 /// [`RemovalCookie`]: super::RemovalCookie
200 pub fn discard<'map, 'key, Key>(&'map mut self, id: Key)
201 where
202 Key: Into<ResponseCookieId<'key>>,
203 {
204 self.0.discard(id)
205 }
206
207 /// Returns an iterator over all the cookies present in this set.
208 ///
209 /// # Example
210 ///
211 /// ```rust
212 /// use pavex::cookie::{ResponseCookies, ResponseCookie};
213 ///
214 /// let mut set = ResponseCookies::new();
215 ///
216 /// set.insert(("name", "value"));
217 /// set.insert(("second", "two"));
218 /// set.insert(("new", "third"));
219 /// set.insert(("another", "fourth"));
220 /// set.insert(("yac", "fifth"));
221 ///
222 /// set.discard("name");
223 /// set.discard("another");
224 ///
225 /// // There are three cookies in the set: "second", "new", and "yac".
226 /// # assert_eq!(set.iter().count(), 3);
227 /// for cookie in set.iter() {
228 /// match cookie.name() {
229 /// "second" => assert_eq!(cookie.value(), "two"),
230 /// "new" => assert_eq!(cookie.value(), "third"),
231 /// "yac" => assert_eq!(cookie.value(), "fifth"),
232 /// _ => unreachable!("there are only three cookies in the set")
233 /// }
234 /// }
235 /// ```
236 pub fn iter(&self) -> ResponseCookiesIter<'_> {
237 ResponseCookiesIter {
238 cookies: self.0.iter(),
239 }
240 }
241
242 /// Returns the values that should be sent to the client as `Set-Cookie` headers.
243 pub fn header_values(self, processor: &Processor) -> impl Iterator<Item = String> + '_ {
244 self.0.header_values(processor)
245 }
246}
247
248#[cfg(test)]
249mod test {
250 use super::ResponseCookies;
251
252 #[test]
253 #[allow(deprecated)]
254 fn simple() {
255 let mut c = ResponseCookies::new();
256
257 c.insert(("test", ""));
258 c.insert(("test2", ""));
259 c.discard("test");
260
261 assert!(c.get("test").is_none());
262 assert!(c.get("test2").is_some());
263
264 c.insert(("test3", ""));
265 c.discard("test2");
266 c.discard("test3");
267
268 assert!(c.get("test").is_none());
269 assert!(c.get("test2").is_none());
270 assert!(c.get("test3").is_none());
271 }
272
273 #[test]
274 fn set_is_send() {
275 fn is_send<T: Send>(_: T) -> bool {
276 true
277 }
278
279 assert!(is_send(ResponseCookies::new()))
280 }
281}