Skip to main content

rama_http_headers/common/
cache_control.rs

1use std::fmt;
2use std::iter::FromIterator;
3use std::str::FromStr;
4use std::time::Duration;
5
6use rama_core::error::{BoxError, ErrorContext as _};
7use rama_http_types::{HeaderName, HeaderValue};
8
9use crate::util::{self, Seconds, csv};
10use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
11
12/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
13/// with extensions in [RFC8246](https://www.rfc-editor.org/rfc/rfc8246)
14///
15/// The `Cache-Control` header field is used to specify directives for
16/// caches along the request/response chain.  Such cache directives are
17/// unidirectional in that the presence of a directive in a request does
18/// not imply that the same directive is to be given in the response.
19///
20/// ## ABNF
21///
22/// ```text
23/// Cache-Control   = 1#cache-directive
24/// cache-directive = token [ "=" ( token / quoted-string ) ]
25/// ```
26///
27/// ## Example values
28///
29/// * `no-cache`
30/// * `private, community="UCI"`
31/// * `max-age=30`
32///
33/// # Example
34///
35/// ```
36/// use rama_http_headers::CacheControl;
37///
38/// let cc = CacheControl::new();
39/// ```
40#[derive(PartialEq, Clone, Debug)]
41pub struct CacheControl {
42    flags: Flags,
43    max_age: Option<Seconds>,
44    max_stale: Option<Seconds>,
45    min_fresh: Option<Seconds>,
46    s_max_age: Option<Seconds>,
47}
48
49#[derive(Debug, Clone, PartialEq)]
50struct Flags {
51    bits: u64,
52}
53
54impl Flags {
55    const NO_CACHE: Self = Self { bits: 0b000000001 };
56    const NO_STORE: Self = Self { bits: 0b000000010 };
57    const NO_TRANSFORM: Self = Self { bits: 0b000000100 };
58    const ONLY_IF_CACHED: Self = Self { bits: 0b000001000 };
59    const MUST_REVALIDATE: Self = Self { bits: 0b000010000 };
60    const PUBLIC: Self = Self { bits: 0b000100000 };
61    const PRIVATE: Self = Self { bits: 0b001000000 };
62    const PROXY_REVALIDATE: Self = Self { bits: 0b010000000 };
63    const IMMUTABLE: Self = Self { bits: 0b100000000 };
64    const MUST_UNDERSTAND: Self = Self { bits: 0b1000000000 };
65
66    fn empty() -> Self {
67        Self { bits: 0 }
68    }
69
70    #[expect(clippy::needless_pass_by_value)]
71    fn contains(&self, flag: Self) -> bool {
72        (self.bits & flag.bits) != 0
73    }
74
75    #[expect(clippy::needless_pass_by_value)]
76    fn insert(&mut self, flag: Self) {
77        self.bits |= flag.bits;
78    }
79}
80
81impl Default for CacheControl {
82    #[inline]
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl CacheControl {
89    /// Construct a new empty `CacheControl` header.
90    #[must_use]
91    pub fn new() -> Self {
92        Self {
93            flags: Flags::empty(),
94            max_age: None,
95            max_stale: None,
96            min_fresh: None,
97            s_max_age: None,
98        }
99    }
100
101    // presets
102
103    /// `public, immutable, max-age=31536000` — for content-hashed /
104    /// versioned URLs (e.g. `/theme.css?v=<git-sha>`).
105    ///
106    /// The URL changes whenever the content changes, so the cached response
107    /// is safe to keep for a year; `immutable` additionally stops the browser
108    /// from revalidating on reload. Reach for this only on content-hashed
109    /// URLs — on a stable URL whose body can change in place, use
110    /// [`Self::no_cache`] or [`Self::short_shared_revalidate`] instead.
111    #[must_use]
112    pub fn immutable_one_year() -> Self {
113        Self::new()
114            .with_public()
115            .with_immutable()
116            .with_max_age_seconds(31_536_000)
117    }
118
119    /// `no-cache` — the response is cacheable but must be revalidated with
120    /// the origin before reuse.
121    ///
122    /// The right default for service-worker scripts, HTML, or anything whose
123    /// URL is stable but whose body may change in place.
124    #[must_use]
125    pub fn no_cache() -> Self {
126        Self::new().with_no_cache()
127    }
128
129    /// `public, max-age=<secs>, must-revalidate` — a short shared cache for
130    /// non-fingerprinted but rarely-changing files (`robots.txt`,
131    /// `sitemap.xml`, `security.txt`).
132    ///
133    /// Lets a CDN absorb crawler bursts without making content updates
134    /// invisible for long.
135    #[must_use]
136    pub fn short_shared_revalidate(max_age_seconds: u32) -> Self {
137        Self::new()
138            .with_public()
139            .with_max_age_seconds(u64::from(max_age_seconds))
140            .with_must_revalidate()
141    }
142
143    // getters
144
145    /// Check if the `no-cache` directive is set.
146    #[must_use]
147    pub fn has_no_cache(self) -> bool {
148        self.flags.contains(Flags::NO_CACHE)
149    }
150
151    /// Check if the `no-store` directive is set.
152    #[must_use]
153    pub fn has_no_store(self) -> bool {
154        self.flags.contains(Flags::NO_STORE)
155    }
156
157    /// Check if the `no-transform` directive is set.
158    #[must_use]
159    pub fn has_no_transform(self) -> bool {
160        self.flags.contains(Flags::NO_TRANSFORM)
161    }
162
163    /// Check if the `only-if-cached` directive is set.
164    #[must_use]
165    pub fn has_only_if_cached(self) -> bool {
166        self.flags.contains(Flags::ONLY_IF_CACHED)
167    }
168
169    /// Check if the `public` directive is set.
170    #[must_use]
171    pub fn has_public(self) -> bool {
172        self.flags.contains(Flags::PUBLIC)
173    }
174
175    /// Check if the `private` directive is set.
176    #[must_use]
177    pub fn has_private(self) -> bool {
178        self.flags.contains(Flags::PRIVATE)
179    }
180
181    /// Check if the `immutable` directive is set.
182    #[must_use]
183    pub fn has_immutable(self) -> bool {
184        self.flags.contains(Flags::IMMUTABLE)
185    }
186
187    /// Check if the `must-revalidate` directive is set.
188    #[must_use]
189    pub fn has_must_revalidate(&self) -> bool {
190        self.flags.contains(Flags::MUST_REVALIDATE)
191    }
192
193    /// Check if the `must-understand` directive is set.
194    #[must_use]
195    pub fn has_must_understand(self) -> bool {
196        self.flags.contains(Flags::MUST_UNDERSTAND)
197    }
198
199    /// Get the value of the `max-age` directive if set.
200    pub fn max_age(&self) -> Option<Duration> {
201        self.max_age.map(Into::into)
202    }
203
204    /// Get the value of the `max-stale` directive if set.
205    pub fn max_stale(&self) -> Option<Duration> {
206        self.max_stale.map(Into::into)
207    }
208
209    /// Get the value of the `min-fresh` directive if set.
210    pub fn min_fresh(&self) -> Option<Duration> {
211        self.min_fresh.map(Into::into)
212    }
213
214    /// Get the value of the `s-maxage` directive if set.
215    pub fn s_max_age(&self) -> Option<Duration> {
216        self.s_max_age.map(Into::into)
217    }
218
219    // setters
220
221    rama_utils::macros::generate_set_and_with! {
222        /// Set the `no-cache` directive.
223        pub fn no_cache(mut self) -> Self {
224            self.flags.insert(Flags::NO_CACHE);
225            self
226        }
227    }
228
229    rama_utils::macros::generate_set_and_with! {
230        /// Set the `no-store` directive.
231        pub fn no_store(mut self) -> Self {
232            self.flags.insert(Flags::NO_STORE);
233            self
234        }
235    }
236
237    rama_utils::macros::generate_set_and_with! {
238        /// Set the `no-transform` directive.
239        pub fn no_transform(mut self) -> Self {
240            self.flags.insert(Flags::NO_TRANSFORM);
241            self
242        }
243    }
244
245    rama_utils::macros::generate_set_and_with! {
246        /// Set the `only-if-cached` directive.
247        pub fn only_if_cached(mut self) -> Self {
248            self.flags.insert(Flags::ONLY_IF_CACHED);
249            self
250        }
251    }
252
253    rama_utils::macros::generate_set_and_with! {
254        /// Set the `private` directive.
255        pub fn private(mut self) -> Self {
256            self.flags.insert(Flags::PRIVATE);
257            self
258        }
259    }
260
261    rama_utils::macros::generate_set_and_with! {
262        /// Set the `public` directive.
263        pub fn public(mut self) -> Self {
264            self.flags.insert(Flags::PUBLIC);
265            self
266        }
267    }
268
269    rama_utils::macros::generate_set_and_with! {
270        /// Set the `immutable` directive.
271        pub fn immutable(mut self) -> Self {
272            self.flags.insert(Flags::IMMUTABLE);
273            self
274        }
275    }
276
277    rama_utils::macros::generate_set_and_with! {
278        /// Set the `must-revalidate` directive.
279        pub fn must_revalidate(mut self) -> Self {
280            self.flags.insert(Flags::MUST_REVALIDATE);
281            self
282        }
283    }
284
285    rama_utils::macros::generate_set_and_with! {
286        /// Set the `must-understand` directive.
287        pub fn must_understand(mut self) -> Self {
288            self.flags.insert(Flags::MUST_UNDERSTAND);
289            self
290        }
291    }
292
293    rama_utils::macros::generate_set_and_with! {
294        /// Set the `max-age` directive.
295        pub fn max_age_duration_rounded(mut self, dur: Duration) -> Self {
296            self.max_age = Some(Seconds::from_duration_rounded(dur));
297            self
298        }
299    }
300
301    rama_utils::macros::generate_set_and_with! {
302        /// Set the `max-age` directive.
303        pub fn max_age_seconds(mut self, seconds: u64) -> Self {
304            self.max_age = Some(Seconds::new(seconds));
305            self
306        }
307    }
308
309    rama_utils::macros::generate_set_and_with! {
310        /// Try to set the `max-age` directive.
311        pub fn max_age_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
312            self.max_age = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
313            Ok(self)
314        }
315    }
316
317    rama_utils::macros::generate_set_and_with! {
318        /// Set the `max-stale` directive.
319        pub fn max_stale_duration_rounded(mut self, dur: Duration) -> Self {
320            self.max_stale = Some(Seconds::from_duration_rounded(dur));
321            self
322        }
323    }
324
325    rama_utils::macros::generate_set_and_with! {
326        /// Set the `max-stale` directive.
327        pub fn max_stale_seconds(mut self, seconds: u64) -> Self {
328            self.max_stale = Some(Seconds::new(seconds));
329            self
330        }
331    }
332
333    rama_utils::macros::generate_set_and_with! {
334        /// Try to set the `max-stale` directive.
335        pub fn max_stale_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
336            self.max_stale = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
337            Ok(self)
338        }
339    }
340
341    rama_utils::macros::generate_set_and_with! {
342        /// Set the `min-fresh` directive.
343        pub fn min_fresh_duration_rounded(mut self, dur: Duration) -> Self {
344            self.min_fresh = Some(Seconds::from_duration_rounded(dur));
345            self
346        }
347    }
348
349    rama_utils::macros::generate_set_and_with! {
350        /// Set the `min-fresh` directive.
351        pub fn min_fresh_seconds(mut self, seconds: u64) -> Self {
352            self.min_fresh = Some(Seconds::new(seconds));
353            self
354        }
355    }
356
357    rama_utils::macros::generate_set_and_with! {
358        /// Try to set the `min-fresh` directive.
359        pub fn min_fresh_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
360            self.min_fresh = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
361            Ok(self)
362        }
363    }
364
365    rama_utils::macros::generate_set_and_with! {
366        /// Set the `s-maxage` directive.
367        pub fn s_max_age_duration_rounded(mut self, dur: Duration) -> Self {
368            self.s_max_age = Some(Seconds::from_duration_rounded(dur));
369            self
370        }
371    }
372
373    rama_utils::macros::generate_set_and_with! {
374        /// Set the `s-maxage` directive.
375        pub fn s_max_age_seconds(mut self, seconds: u64) -> Self {
376            self.s_max_age = Some(Seconds::new(seconds));
377            self
378        }
379    }
380
381    rama_utils::macros::generate_set_and_with! {
382        /// Try to set the `s-maxage` directive.
383        pub fn s_max_age_duration(mut self, dur: Duration) -> Result<Self, BoxError> {
384            self.s_max_age = Some(Seconds::try_from_duration(dur).context("duration contains sub nano seconds")?);
385            Ok(self)
386        }
387    }
388}
389
390impl TypedHeader for CacheControl {
391    fn name() -> &'static HeaderName {
392        &::rama_http_types::header::CACHE_CONTROL
393    }
394}
395
396impl HeaderDecode for CacheControl {
397    fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
398        csv::from_comma_delimited(values).map(|FromIter(cc)| cc)
399    }
400}
401
402impl HeaderEncode for CacheControl {
403    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
404        values.extend(::std::iter::once(util::fmt(Fmt(self))));
405    }
406}
407
408// Adapter to be used in Header::decode
409struct FromIter(CacheControl);
410
411impl FromIterator<KnownDirective> for FromIter {
412    fn from_iter<I>(iter: I) -> Self
413    where
414        I: IntoIterator<Item = KnownDirective>,
415    {
416        let mut cc = CacheControl::new();
417
418        // ignore all unknown directives
419        let iter = iter.into_iter().filter_map(|dir| match dir {
420            KnownDirective::Known(dir) => Some(dir),
421            KnownDirective::Unknown => None,
422        });
423
424        for directive in iter {
425            match directive {
426                Directive::NoCache => {
427                    cc.flags.insert(Flags::NO_CACHE);
428                }
429                Directive::NoStore => {
430                    cc.flags.insert(Flags::NO_STORE);
431                }
432                Directive::NoTransform => {
433                    cc.flags.insert(Flags::NO_TRANSFORM);
434                }
435                Directive::OnlyIfCached => {
436                    cc.flags.insert(Flags::ONLY_IF_CACHED);
437                }
438                Directive::MustRevalidate => {
439                    cc.flags.insert(Flags::MUST_REVALIDATE);
440                }
441                Directive::MustUnderstand => {
442                    cc.flags.insert(Flags::MUST_UNDERSTAND);
443                }
444                Directive::Public => {
445                    cc.flags.insert(Flags::PUBLIC);
446                }
447                Directive::Private => {
448                    cc.flags.insert(Flags::PRIVATE);
449                }
450                Directive::Immutable => {
451                    cc.flags.insert(Flags::IMMUTABLE);
452                }
453                Directive::ProxyRevalidate => {
454                    cc.flags.insert(Flags::PROXY_REVALIDATE);
455                }
456                Directive::MaxAge(secs) => {
457                    cc.max_age = Some(Seconds::new(secs));
458                }
459                Directive::MaxStale(secs) => {
460                    cc.max_stale = Some(Seconds::new(secs));
461                }
462                Directive::MinFresh(secs) => {
463                    cc.min_fresh = Some(Seconds::new(secs));
464                }
465                Directive::SMaxAge(secs) => {
466                    cc.s_max_age = Some(Seconds::new(secs));
467                }
468            }
469        }
470
471        Self(cc)
472    }
473}
474
475struct Fmt<'a>(&'a CacheControl);
476
477impl fmt::Display for Fmt<'_> {
478    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
479        let if_flag = |f: Flags, dir: Directive| {
480            if self.0.flags.contains(f) {
481                Some(dir)
482            } else {
483                None
484            }
485        };
486
487        let slice = &[
488            if_flag(Flags::NO_CACHE, Directive::NoCache),
489            if_flag(Flags::NO_STORE, Directive::NoStore),
490            if_flag(Flags::NO_TRANSFORM, Directive::NoTransform),
491            if_flag(Flags::ONLY_IF_CACHED, Directive::OnlyIfCached),
492            if_flag(Flags::MUST_REVALIDATE, Directive::MustRevalidate),
493            if_flag(Flags::PUBLIC, Directive::Public),
494            if_flag(Flags::PRIVATE, Directive::Private),
495            if_flag(Flags::IMMUTABLE, Directive::Immutable),
496            if_flag(Flags::MUST_UNDERSTAND, Directive::MustUnderstand),
497            if_flag(Flags::PROXY_REVALIDATE, Directive::ProxyRevalidate),
498            self.0
499                .max_age
500                .as_ref()
501                .map(|s| Directive::MaxAge(s.as_u64())),
502            self.0
503                .max_stale
504                .as_ref()
505                .map(|s| Directive::MaxStale(s.as_u64())),
506            self.0
507                .min_fresh
508                .as_ref()
509                .map(|s| Directive::MinFresh(s.as_u64())),
510            self.0
511                .s_max_age
512                .as_ref()
513                .map(|s| Directive::SMaxAge(s.as_u64())),
514        ];
515
516        let iter = slice.iter().filter_map(|o| *o);
517
518        csv::fmt_comma_delimited(f, iter)
519    }
520}
521
522#[derive(Clone, Copy)]
523enum KnownDirective {
524    Known(Directive),
525    Unknown,
526}
527
528#[derive(Clone, Copy)]
529enum Directive {
530    NoCache,
531    NoStore,
532    NoTransform,
533    OnlyIfCached,
534
535    // request directives
536    MaxAge(u64),
537    MaxStale(u64),
538    MinFresh(u64),
539
540    // response directives
541    MustRevalidate,
542    MustUnderstand,
543    Public,
544    Private,
545    Immutable,
546    ProxyRevalidate,
547    SMaxAge(u64),
548}
549
550impl fmt::Display for Directive {
551    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
552        fmt::Display::fmt(
553            match *self {
554                Self::NoCache => "no-cache",
555                Self::NoStore => "no-store",
556                Self::NoTransform => "no-transform",
557                Self::OnlyIfCached => "only-if-cached",
558
559                Self::MaxAge(secs) => return write!(f, "max-age={secs}"),
560                Self::MaxStale(secs) => return write!(f, "max-stale={secs}"),
561                Self::MinFresh(secs) => return write!(f, "min-fresh={secs}"),
562
563                Self::MustRevalidate => "must-revalidate",
564                Self::MustUnderstand => "must-understand",
565                Self::Public => "public",
566                Self::Private => "private",
567                Self::Immutable => "immutable",
568                Self::ProxyRevalidate => "proxy-revalidate",
569                Self::SMaxAge(secs) => return write!(f, "s-maxage={secs}"),
570            },
571            f,
572        )
573    }
574}
575
576impl FromStr for KnownDirective {
577    type Err = ();
578    fn from_str(s: &str) -> Result<Self, Self::Err> {
579        Ok(Self::Known(match s {
580            "no-cache" => Directive::NoCache,
581            "no-store" => Directive::NoStore,
582            "no-transform" => Directive::NoTransform,
583            "only-if-cached" => Directive::OnlyIfCached,
584            "must-revalidate" => Directive::MustRevalidate,
585            "public" => Directive::Public,
586            "private" => Directive::Private,
587            "immutable" => Directive::Immutable,
588            "must-understand" => Directive::MustUnderstand,
589            "proxy-revalidate" => Directive::ProxyRevalidate,
590            "" => return Err(()),
591            _ => match s.find('=') {
592                Some(idx) if idx + 1 < s.len() => {
593                    match (&s[..idx], (s[idx + 1..]).trim_matches('"')) {
594                        ("max-age", secs) => {
595                            secs.parse().map(Directive::MaxAge).map_err(|_e| ())?
596                        }
597                        ("max-stale", secs) => {
598                            secs.parse().map(Directive::MaxStale).map_err(|_e| ())?
599                        }
600                        ("min-fresh", secs) => {
601                            secs.parse().map(Directive::MinFresh).map_err(|_e| ())?
602                        }
603                        ("s-maxage", secs) => {
604                            secs.parse().map(Directive::SMaxAge).map_err(|_e| ())?
605                        }
606                        _unknown => return Ok(Self::Unknown),
607                    }
608                }
609                Some(_) | None => return Ok(Self::Unknown),
610            },
611        }))
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::super::{test_decode, test_encode};
618    use super::*;
619
620    #[test]
621    fn test_parse_multiple_headers() {
622        assert_eq!(
623            test_decode::<CacheControl>(&["no-cache", "private"]).unwrap(),
624            CacheControl::new().with_no_cache().with_private(),
625        );
626    }
627
628    #[test]
629    fn test_parse_argument() {
630        assert_eq!(
631            test_decode::<CacheControl>(&["max-age=100, private"]).unwrap(),
632            CacheControl::new().with_max_age_seconds(100).with_private(),
633        );
634    }
635
636    #[test]
637    fn test_parse_quote_form() {
638        assert_eq!(
639            test_decode::<CacheControl>(&["max-age=\"200\""]).unwrap(),
640            CacheControl::new().with_max_age_seconds(200),
641        );
642    }
643
644    #[test]
645    fn test_parse_quoted_comma() {
646        assert_eq!(
647            test_decode::<CacheControl>(&["foo=\"a, private, immutable, b\", no-cache"]).unwrap(),
648            CacheControl::new().with_no_cache(),
649            "unknown extensions are ignored but shouldn't fail parsing",
650        )
651    }
652
653    #[test]
654    fn test_parse_extension() {
655        assert_eq!(
656            test_decode::<CacheControl>(&["foo, no-cache, bar=baz"]).unwrap(),
657            CacheControl::new().with_no_cache(),
658            "unknown extensions are ignored but shouldn't fail parsing",
659        );
660    }
661
662    #[test]
663    fn test_immutable() {
664        let cc = CacheControl::new().with_immutable();
665        let headers = test_encode(cc.clone());
666        assert_eq!(headers["cache-control"], "immutable");
667        assert_eq!(test_decode::<CacheControl>(&["immutable"]).unwrap(), cc);
668        assert!(cc.has_immutable());
669    }
670
671    #[test]
672    fn test_must_revalidate() {
673        let cc = CacheControl::new().with_must_revalidate();
674        let headers = test_encode(cc.clone());
675        assert_eq!(headers["cache-control"], "must-revalidate");
676        assert_eq!(
677            test_decode::<CacheControl>(&["must-revalidate"]).unwrap(),
678            cc
679        );
680        assert!(cc.has_must_revalidate());
681    }
682
683    #[test]
684    fn test_must_understand() {
685        let cc = CacheControl::new().with_must_understand();
686        let headers = test_encode(cc.clone());
687        assert_eq!(headers["cache-control"], "must-understand");
688        assert_eq!(
689            test_decode::<CacheControl>(&["must-understand"]).unwrap(),
690            cc
691        );
692        assert!(cc.has_must_understand());
693    }
694
695    #[test]
696    fn test_parse_bad_syntax() {
697        assert_eq!(test_decode::<CacheControl>(&["max-age=lolz"]), None);
698    }
699
700    #[test]
701    fn encode_one_flag_directive() {
702        let cc = CacheControl::new().with_no_cache();
703
704        let headers = test_encode(cc);
705        assert_eq!(headers["cache-control"], "no-cache");
706    }
707
708    #[test]
709    fn encode_one_param_directive() {
710        let cc = CacheControl::new().with_max_age_seconds(300);
711
712        let headers = test_encode(cc);
713        assert_eq!(headers["cache-control"], "max-age=300");
714    }
715
716    #[test]
717    fn encode_two_directive() {
718        let headers = test_encode(CacheControl::new().with_no_cache().with_private());
719        assert_eq!(headers["cache-control"], "no-cache, private");
720
721        let headers = test_encode(
722            CacheControl::new()
723                .with_no_cache()
724                .with_max_age_seconds(100),
725        );
726        assert_eq!(headers["cache-control"], "no-cache, max-age=100");
727    }
728
729    #[test]
730    fn preset_immutable_one_year() {
731        let headers = test_encode(CacheControl::immutable_one_year());
732        assert_eq!(
733            headers["cache-control"],
734            "public, immutable, max-age=31536000"
735        );
736    }
737
738    #[test]
739    fn preset_no_cache() {
740        let headers = test_encode(CacheControl::no_cache());
741        assert_eq!(headers["cache-control"], "no-cache");
742    }
743
744    #[test]
745    fn preset_short_shared_revalidate() {
746        let headers = test_encode(CacheControl::short_shared_revalidate(3600));
747        assert_eq!(
748            headers["cache-control"],
749            "must-revalidate, public, max-age=3600"
750        );
751    }
752}