biscotti/
response_cookie.rs

1use jiff::tz::TimeZone;
2use jiff::{SignedDuration, Zoned};
3
4use crate::{Expiration, RemovalCookie, ResponseCookieId, SameSite};
5use std::borrow::Cow;
6use std::fmt;
7
8/// A cookie set by a server in an HTTP response using the `Set-Cookie` header.
9///
10/// ## Constructing a `ResponseCookie`
11///
12/// To construct a cookie with only a name/value, use [`ResponseCookie::new()`]:
13///
14/// ```rust
15/// use biscotti::ResponseCookie;
16///
17/// let cookie = ResponseCookie::new("name", "value");
18/// assert_eq!(cookie.to_string(), "name=value");
19/// ```
20///
21/// ## Building a `ResponseCookie`
22///
23/// To construct more elaborate cookies, use `ResponseCookie`'s `set_*` methods.
24///
25/// ```rust
26/// use biscotti::ResponseCookie;
27///
28/// let cookie = ResponseCookie::new("name", "value")
29///     .set_domain("www.rust-lang.org")
30///     .set_path("/")
31///     .set_secure(true)
32///     .set_http_only(true);
33/// ```
34#[derive(Debug, Clone)]
35pub struct ResponseCookie<'c> {
36    /// The cookie's name.
37    pub(crate) name: Cow<'c, str>,
38    /// The cookie's value.
39    pub(crate) value: Cow<'c, str>,
40    /// The cookie's expiration, if any.
41    pub(crate) expires: Option<Expiration>,
42    /// The cookie's maximum age, if any.
43    pub(crate) max_age: Option<SignedDuration>,
44    /// The cookie's domain, if any.
45    pub(crate) domain: Option<Cow<'c, str>>,
46    /// The cookie's path domain, if any.
47    pub(crate) path: Option<Cow<'c, str>>,
48    /// Whether this cookie was marked Secure.
49    pub(crate) secure: Option<bool>,
50    /// Whether this cookie was marked HttpOnly.
51    pub(crate) http_only: Option<bool>,
52    /// The draft `SameSite` attribute.
53    pub(crate) same_site: Option<SameSite>,
54    /// The draft `Partitioned` attribute.
55    pub(crate) partitioned: Option<bool>,
56}
57
58impl<'c> ResponseCookie<'c> {
59    /// Creates a new [`ResponseCookie`] with the given name and value.
60    ///
61    /// # Example
62    ///
63    /// ```rust
64    /// use biscotti::ResponseCookie;
65    ///
66    /// let cookie = ResponseCookie::new("name", "value");
67    /// assert_eq!(cookie.name_value(), ("name", "value"));
68    ///
69    /// // This is equivalent to `from` with a `(name, value)` tuple:
70    /// let cookie = ResponseCookie::from(("name", "value"));
71    /// assert_eq!(cookie.name_value(), ("name", "value"));
72    /// ```
73    pub fn new<N, V>(name: N, value: V) -> Self
74    where
75        N: Into<Cow<'c, str>>,
76        V: Into<Cow<'c, str>>,
77    {
78        ResponseCookie {
79            name: name.into(),
80            value: value.into(),
81            expires: None,
82            max_age: None,
83            domain: None,
84            path: None,
85            secure: None,
86            http_only: None,
87            same_site: None,
88            partitioned: None,
89        }
90    }
91
92    /// Converts `self` into a [`ResponseCookie`] with a static lifetime with as few
93    /// allocations as possible.
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// use biscotti::ResponseCookie;
99    ///
100    /// let c = ResponseCookie::new("a", "b");
101    /// let owned_cookie = c.into_owned();
102    /// assert_eq!(owned_cookie.name_value(), ("a", "b"));
103    /// ```
104    pub fn into_owned(self) -> ResponseCookie<'static> {
105        let to_owned = |s: Cow<'c, str>| match s {
106            Cow::Borrowed(s) => Cow::Owned(s.to_owned()),
107            Cow::Owned(s) => Cow::Owned(s),
108        };
109        ResponseCookie {
110            name: to_owned(self.name),
111            value: to_owned(self.value),
112            expires: self.expires,
113            max_age: self.max_age,
114            domain: self.domain.map(to_owned),
115            path: self.path.map(to_owned),
116            secure: self.secure,
117            http_only: self.http_only,
118            same_site: self.same_site,
119            partitioned: self.partitioned,
120        }
121    }
122
123    /// Returns the name of `self`.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// use biscotti::ResponseCookie;
129    ///
130    /// let c = ResponseCookie::new("name", "value");
131    /// assert_eq!(c.name(), "name");
132    /// ```
133    #[inline]
134    pub fn name(&self) -> &str {
135        self.name.as_ref()
136    }
137
138    /// Returns the value of `self`.
139    ///
140    /// Does not strip surrounding quotes. See [`ResponseCookie::value_trimmed()`] for a
141    /// version that does.
142    ///
143    /// # Example
144    ///
145    /// ```
146    /// use biscotti::ResponseCookie;
147    ///
148    /// let c = ResponseCookie::new("name", "value");
149    /// assert_eq!(c.value(), "value");
150    ///
151    /// let c = ResponseCookie::new("name", "\"value\"");
152    /// assert_eq!(c.value(), "\"value\"");
153    /// ```
154    #[inline]
155    pub fn value(&self) -> &str {
156        self.value.as_ref()
157    }
158
159    /// Returns the value of `self` with surrounding double-quotes trimmed.
160    ///
161    /// This is _not_ the value of the cookie (_that_ is [`ResponseCookie::value()`]).
162    /// Instead, this is the value with a surrounding pair of double-quotes, if
163    /// any, trimmed away. Quotes are only trimmed when they form a pair and
164    /// never otherwise. The trimmed value is never used for other operations,
165    /// such as equality checking, on `self`.
166    ///
167    /// # Example
168    ///
169    /// ```
170    /// use biscotti::ResponseCookie;
171    /// let c0 = ResponseCookie::new("name", "value");
172    /// assert_eq!(c0.value_trimmed(), "value");
173    ///
174    /// let c = ResponseCookie::new("name", "\"value\"");
175    /// assert_eq!(c.value_trimmed(), "value");
176    /// assert!(c != c0);
177    ///
178    /// let c = ResponseCookie::new("name", "\"value");
179    /// assert_eq!(c.value(), "\"value");
180    /// assert_eq!(c.value_trimmed(), "\"value");
181    /// assert!(c != c0);
182    ///
183    /// let c = ResponseCookie::new("name", "\"value\"\"");
184    /// assert_eq!(c.value(), "\"value\"\"");
185    /// assert_eq!(c.value_trimmed(), "value\"");
186    /// assert!(c != c0);
187    /// ```
188    #[inline]
189    pub fn value_trimmed(&self) -> &str {
190        #[inline(always)]
191        fn trim_quotes(s: &str) -> &str {
192            if s.len() < 2 {
193                return s;
194            }
195
196            let bytes = s.as_bytes();
197            match (bytes.first(), bytes.last()) {
198                (Some(b'"'), Some(b'"')) => &s[1..(s.len() - 1)],
199                _ => s,
200            }
201        }
202
203        trim_quotes(self.value())
204    }
205
206    /// Returns the name and value of `self` as a tuple of `(name, value)`.
207    ///
208    /// # Example
209    ///
210    /// ```
211    /// use biscotti::ResponseCookie;
212    ///
213    /// let c = ResponseCookie::new("name", "value");
214    /// assert_eq!(c.name_value(), ("name", "value"));
215    /// ```
216    #[inline]
217    pub fn name_value(&self) -> (&str, &str) {
218        (self.name(), self.value())
219    }
220
221    /// Returns the name and [trimmed value](ResponseCookie::value_trimmed()) of `self`
222    /// as a tuple of `(name, trimmed_value)`.
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// use biscotti::ResponseCookie;
228    ///
229    /// let c = ResponseCookie::new("name", "\"value\"");
230    /// assert_eq!(c.name_value_trimmed(), ("name", "value"));
231    /// ```
232    #[inline]
233    pub fn name_value_trimmed(&self) -> (&str, &str) {
234        (self.name(), self.value_trimmed())
235    }
236
237    /// Returns whether this cookie was marked `HttpOnly` or not. Returns
238    /// `Some(true)` when the cookie was explicitly set (manually or parsed) as
239    /// `HttpOnly`, `Some(false)` when `http_only` was manually set to `false`,
240    /// and `None` otherwise.
241    ///
242    /// # Example
243    ///
244    /// ```
245    /// use biscotti::ResponseCookie;
246    ///
247    /// let mut c = ResponseCookie::new("name", "value");
248    /// assert_eq!(c.http_only(), None);
249    ///
250    /// // An explicitly set "false" value.
251    /// c = c.set_http_only(false);
252    /// assert_eq!(c.http_only(), Some(false));
253    ///
254    /// // An explicitly set "true" value.
255    /// c = c.set_http_only(true);
256    /// assert_eq!(c.http_only(), Some(true));
257    /// ```
258    #[inline]
259    pub fn http_only(&self) -> Option<bool> {
260        self.http_only
261    }
262
263    /// Returns whether this cookie was marked `Secure` or not. Returns
264    /// `Some(true)` when the cookie was explicitly set (manually or parsed) as
265    /// `Secure`, `Some(false)` when `secure` was manually set to `false`, and
266    /// `None` otherwise.
267    ///
268    /// # Example
269    ///
270    /// ```
271    /// use biscotti::ResponseCookie;
272    ///
273    /// let mut c = ResponseCookie::new("name", "value");
274    /// assert_eq!(c.secure(), None);
275    ///
276    /// // An explicitly set "false" value.
277    /// c = c.set_secure(false);
278    /// assert_eq!(c.secure(), Some(false));
279    ///
280    /// // An explicitly set "true" value.
281    /// c = c.set_secure(true);
282    /// assert_eq!(c.secure(), Some(true));
283    /// ```
284    #[inline]
285    pub fn secure(&self) -> Option<bool> {
286        self.secure
287    }
288
289    /// Returns the `SameSite` attribute of this cookie if one was specified.
290    ///
291    /// # Example
292    ///
293    /// ```
294    /// use biscotti::{ResponseCookie, SameSite};
295    ///
296    /// let mut c = ResponseCookie::new("name", "value");
297    /// assert_eq!(c.same_site(), None);
298    ///
299    /// c = c.set_same_site(SameSite::Lax);
300    /// assert_eq!(c.same_site(), Some(SameSite::Lax));
301    ///
302    /// c = c.set_same_site(None);
303    /// assert_eq!(c.same_site(), None);
304    /// ```
305    #[inline]
306    pub fn same_site(&self) -> Option<SameSite> {
307        self.same_site
308    }
309
310    /// Returns whether this cookie was marked `Partitioned` or not. Returns
311    /// `Some(true)` when the cookie was explicitly set (manually or parsed) as
312    /// `Partitioned`, `Some(false)` when `partitioned` was manually set to `false`,
313    /// and `None` otherwise.
314    ///
315    /// **Note:** This cookie attribute is experimental! Its meaning and
316    /// definition are not standardized and therefore subject to change.
317    ///
318    /// [HTTP draft]: https://github.com/privacycg/CHIPS
319    ///
320    /// # Example
321    ///
322    /// ```
323    /// use biscotti::ResponseCookie;
324    ///
325    /// let mut c = ResponseCookie::new("name", "value");
326    /// assert_eq!(c.partitioned(), None);
327    ///
328    /// // An explicitly set "false" value.
329    /// c = c.set_partitioned(false);
330    /// assert_eq!(c.partitioned(), Some(false));
331    ///
332    /// // An explicitly set "true" value.
333    /// c = c.set_partitioned(true);
334    /// assert_eq!(c.partitioned(), Some(true));
335    /// ```
336    #[inline]
337    pub fn partitioned(&self) -> Option<bool> {
338        self.partitioned
339    }
340
341    /// Returns the specified max-age of the cookie if one was specified.
342    ///
343    /// # Example
344    ///
345    /// ```
346    /// use biscotti::{ResponseCookie, time::SignedDuration};
347    ///
348    /// let mut c = ResponseCookie::new("name", "value");
349    /// assert_eq!(c.max_age(), None);
350    ///
351    /// c = c.set_max_age(SignedDuration::from_hours(1));
352    /// assert_eq!(c.max_age().map(|age| age.as_hours()), Some(1));
353    /// ```
354    #[inline]
355    pub fn max_age(&self) -> Option<SignedDuration> {
356        self.max_age
357    }
358
359    /// Returns the `Path` of the cookie if one was specified.
360    ///
361    /// # Example
362    ///
363    /// ```
364    /// use biscotti::ResponseCookie;
365    ///
366    /// let mut c = ResponseCookie::new("name", "value");
367    /// assert_eq!(c.path(), None);
368    ///
369    /// c = c.set_path("/");
370    /// assert_eq!(c.path(), Some("/"));
371    ///
372    /// c = c.unset_path();
373    /// assert_eq!(c.path(), None);
374    /// ```
375    #[inline]
376    pub fn path(&self) -> Option<&str> {
377        match self.path {
378            Some(ref c) => Some(c.as_ref()),
379            None => None,
380        }
381    }
382
383    /// Returns the `Domain` of the cookie if one was specified.
384    ///
385    /// This does not consider whether the `Domain` is valid; validation is left
386    /// to higher-level libraries, as needed. However, if the `Domain` starts
387    /// with a leading `.`, the leading `.` is stripped.
388    ///
389    /// # Example
390    ///
391    /// ```
392    /// use biscotti::ResponseCookie;
393    ///
394    /// let mut c = ResponseCookie::new("name", "value");
395    /// assert_eq!(c.domain(), None);
396    ///
397    /// c = c.set_domain("crates.io");
398    /// assert_eq!(c.domain(), Some("crates.io"));
399    ///
400    /// c = c.set_domain(".crates.io");
401    /// assert_eq!(c.domain(), Some("crates.io"));
402    ///
403    /// // Note that `..crates.io` is not a valid domain.
404    /// c = c.set_domain("..crates.io");
405    /// assert_eq!(c.domain(), Some(".crates.io"));
406    ///
407    /// c = c.unset_domain();
408    /// assert_eq!(c.domain(), None);
409    /// ```
410    #[inline]
411    pub fn domain(&self) -> Option<&str> {
412        match self.domain {
413            Some(ref c) => {
414                let domain = c.as_ref();
415                domain.strip_prefix('.').or(Some(domain))
416            }
417            None => None,
418        }
419    }
420
421    /// Returns the [`Expiration`] of the cookie if one was specified.
422    ///
423    /// # Example
424    ///
425    /// ```
426    /// use biscotti::{ResponseCookie, Expiration};
427    /// use biscotti::time::{tz::TimeZone, civil::date};
428    ///
429    /// let mut c = ResponseCookie::new("name", "value");
430    /// assert_eq!(c.expires(), None);
431    ///
432    /// c = c.set_expires(None);
433    /// assert_eq!(c.expires(), Some(&Expiration::Session));
434    ///
435    /// let expire_time = date(2017, 10, 21)
436    ///     .at(7, 28, 0, 0)
437    ///     .to_zoned(TimeZone::UTC)
438    ///     .unwrap();
439    /// c = c.set_expires(Some(expire_time));
440    /// assert_eq!(c.expires().and_then(|e| e.datetime()).map(|t| t.year()), Some(2017));
441    /// ```
442    #[inline]
443    pub fn expires(&self) -> Option<&Expiration> {
444        self.expires.as_ref()
445    }
446
447    /// Returns the expiration date-time of the cookie if one was specified.
448    ///
449    /// It returns `None` if the cookie is a session cookie or if the expiration
450    /// was not specified.
451    ///
452    /// # Example
453    ///
454    /// ```
455    /// use biscotti::{Expiration, ResponseCookie};
456    /// use biscotti::time::{civil::date, tz::TimeZone};
457    ///
458    /// let mut c = ResponseCookie::new("name", "value");
459    /// assert_eq!(c.expires_datetime(), None);
460    ///
461    /// // Here, `cookie.expires()` returns `Some(Expiration::Session)`.
462    /// c = c.set_expires(Expiration::Session);
463    /// assert_eq!(c.expires_datetime(), None);
464    ///
465    /// let expire_time = date(2017, 10, 21)
466    ///     .at(7, 28, 0, 0)
467    ///     .to_zoned(TimeZone::UTC)
468    ///     .unwrap();
469    /// c = c.set_expires(Some(expire_time));
470    /// assert_eq!(c.expires_datetime().map(|t| t.year()), Some(2017));
471    /// ```
472    #[inline]
473    pub fn expires_datetime(&self) -> Option<&Zoned> {
474        self.expires.as_ref().and_then(|e| e.datetime())
475    }
476
477    /// Sets the name of `self` to `name`.
478    ///
479    /// # Example
480    ///
481    /// ```
482    /// use biscotti::ResponseCookie;
483    ///
484    /// let mut c = ResponseCookie::new("name", "value");
485    /// assert_eq!(c.name(), "name");
486    ///
487    /// c = c.set_name("foo");
488    /// assert_eq!(c.name(), "foo");
489    /// ```
490    pub fn set_name<N: Into<Cow<'c, str>>>(mut self, name: N) -> Self {
491        self.name = name.into();
492        self
493    }
494
495    /// Sets the value of `self` to `value`.
496    ///
497    /// # Example
498    ///
499    /// ```
500    /// use biscotti::ResponseCookie;
501    ///
502    /// let mut c = ResponseCookie::new("name", "value");
503    /// assert_eq!(c.value(), "value");
504    ///
505    /// c = c.set_value("bar");
506    /// assert_eq!(c.value(), "bar");
507    /// ```
508    pub fn set_value<V: Into<Cow<'c, str>>>(mut self, value: V) -> Self {
509        self.value = value.into();
510        self
511    }
512
513    /// Sets the value of `http_only` in `self` to `value`.  If `value` is
514    /// `None`, the field is unset.
515    ///
516    /// # Example
517    ///
518    /// ```
519    /// use biscotti::ResponseCookie;
520    ///
521    /// let mut c = ResponseCookie::new("name", "value");
522    /// assert_eq!(c.http_only(), None);
523    ///
524    /// c = c.set_http_only(true);
525    /// assert_eq!(c.http_only(), Some(true));
526    ///
527    /// c = c.set_http_only(false);
528    /// assert_eq!(c.http_only(), Some(false));
529    ///
530    /// c = c.set_http_only(None);
531    /// assert_eq!(c.http_only(), None);
532    /// ```
533    #[inline]
534    pub fn set_http_only<T: Into<Option<bool>>>(mut self, value: T) -> Self {
535        self.http_only = value.into();
536        self
537    }
538
539    /// Sets the value of `secure` in `self` to `value`. If `value` is `None`,
540    /// the field is unset.
541    ///
542    /// # Example
543    ///
544    /// ```
545    /// use biscotti::ResponseCookie;
546    ///
547    /// let mut c = ResponseCookie::new("name", "value");
548    /// assert_eq!(c.secure(), None);
549    ///
550    /// c = c.set_secure(true);
551    /// assert_eq!(c.secure(), Some(true));
552    ///
553    /// c = c.set_secure(false);
554    /// assert_eq!(c.secure(), Some(false));
555    ///
556    /// c = c.set_secure(None);
557    /// assert_eq!(c.secure(), None);
558    /// ```
559    #[inline]
560    pub fn set_secure<T: Into<Option<bool>>>(mut self, value: T) -> Self {
561        self.secure = value.into();
562        self
563    }
564
565    /// Sets the value of `same_site` in `self` to `value`. If `value` is
566    /// `None`, the field is unset.
567    ///
568    /// # Example
569    ///
570    /// ```
571    /// use biscotti::{ResponseCookie, SameSite};
572    ///
573    /// let mut c = ResponseCookie::new("name", "value");
574    /// assert_eq!(c.same_site(), None);
575    ///
576    /// c = c.set_same_site(SameSite::Strict);
577    /// assert_eq!(c.same_site(), Some(SameSite::Strict));
578    /// assert_eq!(c.to_string(), "name=value; SameSite=Strict");
579    ///
580    /// c = c.set_same_site(None);
581    /// assert_eq!(c.same_site(), None);
582    /// assert_eq!(c.to_string(), "name=value");
583    /// ```
584    ///
585    /// # Example: `SameSite::None`
586    ///
587    /// If `value` is `SameSite::None`, the "Secure"
588    /// flag will be set when the cookie is written out unless `secure` is
589    /// explicitly set to `false` via [`ResponseCookie::set_secure()`] or the equivalent
590    /// builder method.
591    ///
592    /// ```
593    /// use biscotti::{ResponseCookie, SameSite};
594    ///
595    /// let mut c = ResponseCookie::new("name", "value");
596    /// assert_eq!(c.same_site(), None);
597    ///
598    /// c = c.set_same_site(SameSite::None);
599    /// assert_eq!(c.same_site(), Some(SameSite::None));
600    /// assert_eq!(c.to_string(), "name=value; SameSite=None; Secure");
601    ///
602    /// c = c.set_secure(false);
603    /// assert_eq!(c.to_string(), "name=value; SameSite=None");
604    /// ```
605    #[inline]
606    pub fn set_same_site<T: Into<Option<SameSite>>>(mut self, value: T) -> Self {
607        self.same_site = value.into();
608        self
609    }
610
611    /// Sets the value of `partitioned` in `self` to `value`. If `value` is
612    /// `None`, the field is unset.
613    ///
614    /// **Note:** _Partitioned_ cookies require the `Secure` attribute to be
615    /// set. As such, `Partitioned` cookies are always rendered with the
616    /// `Secure` attribute, irrespective of the `Secure` attribute's setting.
617    ///
618    /// **Note:** This cookie attribute is an [HTTP draft]! Its meaning and
619    /// definition are not standardized and therefore subject to change.
620    ///
621    /// [HTTP draft]: https://datatracker.ietf.org/doc/draft-cutler-httpbis-partitioned-cookies/
622    ///
623    /// # Example
624    ///
625    /// ```
626    /// use biscotti::ResponseCookie;
627    ///
628    /// let mut c = ResponseCookie::new("name", "value");
629    /// assert_eq!(c.partitioned(), None);
630    ///
631    /// c = c.set_partitioned(true);
632    /// assert_eq!(c.partitioned(), Some(true));
633    /// assert!(c.to_string().contains("Secure"));
634    ///
635    /// c = c.set_partitioned(false);
636    /// assert_eq!(c.partitioned(), Some(false));
637    /// assert!(!c.to_string().contains("Secure"));
638    ///
639    /// c = c.set_partitioned(None);
640    /// assert_eq!(c.partitioned(), None);
641    /// assert!(!c.to_string().contains("Secure"));
642    /// ```
643    #[inline]
644    pub fn set_partitioned<T: Into<Option<bool>>>(mut self, value: T) -> Self {
645        self.partitioned = value.into();
646        self
647    }
648
649    /// Sets the value of `max_age` in `self` to `value`. If `value` is `None`,
650    /// the field is unset.
651    ///
652    /// # Example
653    ///
654    /// ```rust
655    /// use biscotti::{ResponseCookie, time::SignedDuration};
656    ///
657    /// # fn main() {
658    /// let mut c = ResponseCookie::new("name", "value");
659    /// assert_eq!(c.max_age(), None);
660    ///
661    /// let max_age = SignedDuration::from_hours(10);
662    /// c = c.set_max_age(max_age);
663    /// assert_eq!(c.max_age(), Some(max_age));
664    ///
665    /// c = c.set_max_age(None);
666    /// assert!(c.max_age().is_none());
667    /// # }
668    /// ```
669    #[inline]
670    pub fn set_max_age<D: Into<Option<SignedDuration>>>(mut self, value: D) -> Self {
671        self.max_age = value.into();
672        self
673    }
674
675    /// Sets the `path` of `self` to `path`.
676    ///
677    /// # Example
678    ///
679    /// ```rust
680    /// use biscotti::ResponseCookie;
681    ///
682    /// let mut c = ResponseCookie::new("name", "value");
683    /// assert_eq!(c.path(), None);
684    ///
685    /// c = c.set_path("/");
686    /// assert_eq!(c.path(), Some("/"));
687    /// ```
688    pub fn set_path<P: Into<Cow<'c, str>>>(mut self, path: P) -> Self {
689        self.path = Some(path.into());
690        self
691    }
692
693    /// Unsets the `path` of `self`.
694    ///
695    /// # Example
696    ///
697    /// ```
698    /// use biscotti::ResponseCookie;
699    ///
700    /// let mut c = ResponseCookie::new("name", "value");
701    /// assert_eq!(c.path(), None);
702    ///
703    /// c = c.set_path("/");
704    /// assert_eq!(c.path(), Some("/"));
705    ///
706    /// c = c.unset_path();
707    /// assert_eq!(c.path(), None);
708    /// ```
709    pub fn unset_path(mut self) -> Self {
710        self.path = None;
711        self
712    }
713
714    /// Sets the `domain` of `self` to `domain`.
715    ///
716    /// # Example
717    ///
718    /// ```
719    /// use biscotti::ResponseCookie;
720    ///
721    /// let mut c = ResponseCookie::new("name", "value");
722    /// assert_eq!(c.domain(), None);
723    ///
724    /// c = c.set_domain("rust-lang.org");
725    /// assert_eq!(c.domain(), Some("rust-lang.org"));
726    /// ```
727    pub fn set_domain<D: Into<Cow<'c, str>>>(mut self, domain: D) -> Self {
728        self.domain = Some(domain.into());
729        self
730    }
731
732    /// Unsets the `domain` of `self`.
733    ///
734    /// # Example
735    ///
736    /// ```
737    /// use biscotti::ResponseCookie;
738    ///
739    /// let mut c = ResponseCookie::new("name", "value");
740    /// assert_eq!(c.domain(), None);
741    ///
742    /// c = c.set_domain("rust-lang.org");
743    /// assert_eq!(c.domain(), Some("rust-lang.org"));
744    ///
745    /// c = c.unset_domain();
746    /// assert_eq!(c.domain(), None);
747    /// ```
748    pub fn unset_domain(mut self) -> Self {
749        self.domain = None;
750        self
751    }
752
753    /// Sets the expires field of `self` to `time`. If `time` is `None`, an
754    /// expiration of [`Session`](Expiration::Session) is set.
755    ///
756    /// # Example
757    ///
758    /// ```
759    /// use biscotti::{ResponseCookie, Expiration};
760    /// use biscotti::time::{Span, Zoned};
761    ///
762    /// let mut c = ResponseCookie::new("name", "value");
763    /// assert_eq!(c.expires(), None);
764    ///
765    /// let mut now = Zoned::now();
766    /// now += Span::new().weeks(52);
767    ///
768    /// c = c.set_expires(now);
769    /// assert!(c.expires().is_some());
770    ///
771    /// c = c.set_expires(None);
772    /// assert_eq!(c.expires(), Some(&Expiration::Session));
773    /// ```
774    pub fn set_expires<T: Into<Expiration>>(mut self, time: T) -> Self {
775        // RFC 6265 requires dates not to exceed 9999 years,
776        // but `jiff`'s `Zoned` goes up to 9999 years,
777        // so no need to check at runtime.
778        self.expires = Some(time.into());
779        self
780    }
781
782    /// Unsets the `expires` of `self`.
783    ///
784    /// # Example
785    ///
786    /// ```
787    /// use biscotti::{ResponseCookie, Expiration};
788    ///
789    /// let mut c = ResponseCookie::new("name", "value");
790    /// assert_eq!(c.expires(), None);
791    ///
792    /// c = c.set_expires(None);
793    /// assert_eq!(c.expires(), Some(&Expiration::Session));
794    ///
795    /// c = c.unset_expires();
796    /// assert_eq!(c.expires(), None);
797    /// ```
798    pub fn unset_expires(mut self) -> Self {
799        self.expires = None;
800        self
801    }
802
803    /// Makes `self` a "permanent" cookie by extending its expiration and max
804    /// age 20 years into the future.
805    ///
806    /// # Example
807    ///
808    /// ```rust
809    /// use biscotti::{ResponseCookie, time::SignedDuration};
810    ///
811    /// # fn main() {
812    /// let mut c = ResponseCookie::new("foo", "bar");
813    /// assert!(c.expires().is_none());
814    /// assert!(c.max_age().is_none());
815    ///
816    /// c = c.make_permanent();
817    /// assert!(c.expires().is_some());
818    /// assert_eq!(c.max_age(), Some(SignedDuration::from_hours(24 * 365 * 20)));
819    /// # }
820    /// ```
821    pub fn make_permanent(self) -> Self {
822        let twenty_years = SignedDuration::from_hours(24 * 365 * 20);
823        self.set_max_age(twenty_years)
824            .set_expires(Zoned::now().saturating_add(twenty_years))
825    }
826
827    /// Make `self` a "removal" cookie by clearing its value and
828    /// setting an expiration date far in the past.
829    ///
830    /// # Example
831    ///
832    /// ```rust
833    /// use biscotti::{ResponseCookie, time::Zoned};
834    ///
835    /// # fn main() {
836    /// let c = ResponseCookie::new("foo", "bar");
837    /// let removal = c.into_removal();
838    ///
839    /// // You can convert a `RemovalCookie` back into a "raw" `ResponseCookie`
840    /// // to inspect its properties.
841    /// let raw: ResponseCookie = removal.into();
842    /// assert_eq!(raw.value(), "");
843    /// let expiration = raw.expires_datetime().unwrap();
844    /// assert!(expiration < Zoned::now());
845    /// # }
846    /// ```
847    pub fn into_removal(self) -> RemovalCookie<'c> {
848        let mut c = RemovalCookie::new(self.name);
849        if let Some(path) = self.path {
850            c = c.set_path(path);
851        }
852        if let Some(domain) = self.domain {
853            c = c.set_domain(domain);
854        }
855        c
856    }
857
858    /// Returns a [`ResponseCookieId`] that can be used to identify `self` in a
859    /// collection of response cookies.
860    ///
861    /// It takes into account the `name`, `domain`, and `path` of `self`.
862    pub fn id(&self) -> ResponseCookieId<'c> {
863        let mut id = ResponseCookieId::new(self.name.clone());
864        if let Some(path) = self.path.as_ref() {
865            id = id.set_path(path.clone());
866        }
867        if let Some(domain) = self.domain.as_ref() {
868            id = id.set_domain(domain.clone());
869        }
870        id
871    }
872
873    fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result {
874        if let Some(true) = self.http_only() {
875            write!(f, "; HttpOnly")?;
876        }
877
878        if let Some(same_site) = self.same_site() {
879            write!(f, "; SameSite={same_site}")?;
880        }
881
882        if let Some(true) = self.partitioned() {
883            write!(f, "; Partitioned")?;
884        }
885
886        if self.secure() == Some(true)
887            || self.partitioned() == Some(true)
888            || self.secure().is_none() && self.same_site() == Some(SameSite::None)
889        {
890            write!(f, "; Secure")?;
891        }
892
893        if let Some(path) = self.path() {
894            write!(f, "; Path={path}")?;
895        }
896
897        if let Some(domain) = self.domain() {
898            write!(f, "; Domain={domain}")?;
899        }
900
901        if let Some(max_age) = self.max_age() {
902            write!(f, "; Max-Age={}", max_age.as_secs())?;
903        }
904
905        if let Some(time) = self.expires_datetime() {
906            static GMT: std::sync::LazyLock<TimeZone> = std::sync::LazyLock::new(|| {
907                jiff::tz::TimeZone::get("GMT")
908                    .expect("Failed to fetch the 'GMT' timezone from the system database")
909            });
910
911            // From http://tools.ietf.org/html/rfc2616#section-3.3.1.
912            let time = time.with_time_zone(GMT.clone());
913            write!(f, "; Expires={}", time.strftime("%a, %d %b %Y %T %Z"))?;
914        }
915
916        Ok(())
917    }
918}
919
920impl fmt::Display for ResponseCookie<'_> {
921    /// Formats the cookie `self` as a `Set-Cookie` header value.
922    ///
923    /// # Example
924    ///
925    /// ```rust
926    /// use biscotti::ResponseCookie;
927    ///
928    /// let cookie = ResponseCookie::new("foo", "bar").set_path("/");
929    /// assert_eq!(cookie.to_string(), "foo=bar; Path=/");
930    /// ```
931    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
932        write!(f, "{}={}", self.name(), self.value())?;
933        self.fmt_parameters(f)
934    }
935}
936
937impl<'b> PartialEq<ResponseCookie<'b>> for ResponseCookie<'_> {
938    fn eq(&self, other: &ResponseCookie<'b>) -> bool {
939        let so_far_so_good = self.name() == other.name()
940            && self.value() == other.value()
941            && self.http_only() == other.http_only()
942            && self.secure() == other.secure()
943            && self.partitioned() == other.partitioned()
944            && self.max_age() == other.max_age()
945            && self.expires() == other.expires();
946
947        if !so_far_so_good {
948            return false;
949        }
950
951        match (self.path(), other.path()) {
952            (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {}
953            (None, None) => {}
954            _ => return false,
955        };
956
957        match (self.domain(), other.domain()) {
958            (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {}
959            (None, None) => {}
960            _ => return false,
961        };
962
963        true
964    }
965}
966
967impl<'a, N, V> From<(N, V)> for ResponseCookie<'a>
968where
969    N: Into<Cow<'a, str>>,
970    V: Into<Cow<'a, str>>,
971{
972    fn from((name, value): (N, V)) -> Self {
973        ResponseCookie::new(name, value)
974    }
975}
976
977impl<'a> AsRef<ResponseCookie<'a>> for ResponseCookie<'a> {
978    fn as_ref(&self) -> &ResponseCookie<'a> {
979        self
980    }
981}
982
983impl<'a> AsMut<ResponseCookie<'a>> for ResponseCookie<'a> {
984    fn as_mut(&mut self) -> &mut ResponseCookie<'a> {
985        self
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use jiff::civil::DateTime;
992    use jiff::tz::TimeZone;
993
994    use crate::time::SignedDuration;
995    use crate::{ResponseCookie, SameSite};
996
997    #[test]
998    fn format() {
999        let cookie = ResponseCookie::new("foo", "bar");
1000        assert_eq!(&cookie.to_string(), "foo=bar");
1001
1002        let cookie = ResponseCookie::new("foo", "bar").set_http_only(true);
1003        assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly");
1004
1005        let cookie = ResponseCookie::new("foo", "bar").set_max_age(SignedDuration::from_secs(10));
1006        assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10");
1007
1008        let cookie = ResponseCookie::new("foo", "bar").set_secure(true);
1009        assert_eq!(&cookie.to_string(), "foo=bar; Secure");
1010
1011        let cookie = ResponseCookie::new("foo", "bar").set_path("/");
1012        assert_eq!(&cookie.to_string(), "foo=bar; Path=/");
1013
1014        let cookie = ResponseCookie::new("foo", "bar").set_domain("www.rust-lang.org");
1015        assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org");
1016
1017        let cookie = ResponseCookie::new("foo", "bar").set_domain(".rust-lang.org");
1018        assert_eq!(&cookie.to_string(), "foo=bar; Domain=rust-lang.org");
1019
1020        let cookie = ResponseCookie::new("foo", "bar").set_domain("rust-lang.org");
1021        assert_eq!(&cookie.to_string(), "foo=bar; Domain=rust-lang.org");
1022
1023        let expires = DateTime::constant(2015, 10, 21, 7, 28, 0, 0)
1024            .to_zoned(TimeZone::UTC)
1025            .unwrap();
1026        let cookie = ResponseCookie::new("foo", "bar").set_expires(expires);
1027        assert_eq!(
1028            &cookie.to_string(),
1029            "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
1030        );
1031
1032        let cookie = ResponseCookie::new("foo", "bar").set_same_site(SameSite::Strict);
1033        assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict");
1034
1035        let cookie = ResponseCookie::new("foo", "bar").set_same_site(SameSite::Lax);
1036        assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax");
1037
1038        let mut cookie = ResponseCookie::new("foo", "bar").set_same_site(SameSite::None);
1039        assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure");
1040
1041        cookie = cookie.set_partitioned(true);
1042        assert_eq!(
1043            &cookie.to_string(),
1044            "foo=bar; SameSite=None; Partitioned; Secure"
1045        );
1046
1047        cookie = cookie.set_same_site(None);
1048        assert_eq!(&cookie.to_string(), "foo=bar; Partitioned; Secure");
1049
1050        cookie = cookie.set_secure(false);
1051        assert_eq!(&cookie.to_string(), "foo=bar; Partitioned; Secure");
1052
1053        cookie = cookie.set_secure(None);
1054        assert_eq!(&cookie.to_string(), "foo=bar; Partitioned; Secure");
1055
1056        cookie = cookie.set_partitioned(None);
1057        assert_eq!(&cookie.to_string(), "foo=bar");
1058
1059        let mut c = ResponseCookie::new("foo", "bar")
1060            .set_same_site(SameSite::None)
1061            .set_secure(false);
1062        assert_eq!(&c.to_string(), "foo=bar; SameSite=None");
1063        c = c.set_secure(true);
1064        assert_eq!(&c.to_string(), "foo=bar; SameSite=None; Secure");
1065    }
1066}