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}