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}