Skip to main content

rocket_cache_response/
cache_control.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display, Formatter},
4};
5
6/// The number of seconds in a year, which is the biggest `max-age` most caches accept.
7const IMMUTABLE_MAX_AGE: u32 = 31536000;
8
9/// The header values of the fixed directive combinations, so that the common cases need no allocation.
10const WELL_KNOWN_HEADER_VALUES: [(CacheControl, &str); 4] = [
11    (CacheControl::new(), ""),
12    (CacheControl::no_cache(), "no-cache"),
13    (CacheControl::no_store(), "no-store"),
14    (CacheControl::immutable(), "public, max-age=31536000, immutable"),
15];
16
17/// Which caches are allowed to store a response.
18#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
19pub enum Cacheability {
20    /// Send no `public`, `private` or `no-store` directive, so every cache falls back to its own default behavior.
21    #[default]
22    Unspecified,
23    /// `public`: every cache may store the response, including CDNs and other shared caches.
24    Public,
25    /// `private`: only the browser that made the request may store the response.
26    Private,
27    /// `no-store`: no cache may store the response at all.
28    NoStore,
29}
30
31/// The directives of a `Cache-Control` response header.
32///
33/// Every directive is a public field, so an uncommon combination can be built with the struct update syntax.
34///
35/// ```rust
36/// use rocket_cache_response::CacheControl;
37///
38/// let cache_control = CacheControl {
39///     s_max_age: Some(86400),
40///     ..CacheControl::public(60)
41/// };
42///
43/// assert_eq!("public, max-age=60, s-maxage=86400", cache_control.to_string());
44/// ```
45#[derive(Debug, Clone, Copy, Eq, PartialEq)]
46pub struct CacheControl {
47    /// Which caches are allowed to store the response.
48    pub cacheability:           Cacheability,
49    /// `max-age`: for how many seconds the response stays fresh.
50    pub max_age:                Option<u32>,
51    /// `s-maxage`: replaces `max_age` for shared caches only.
52    pub s_max_age:              Option<u32>,
53    /// `no-cache`: a stored response must be revalidated with the origin server before every reuse.
54    pub no_cache:               bool,
55    /// `must-revalidate`: a stale response must not be reused without a successful revalidation.
56    pub must_revalidate:        bool,
57    /// `proxy-revalidate`: the same as `must_revalidate`, but it applies to shared caches only.
58    pub proxy_revalidate:       bool,
59    /// `no-transform`: caches and proxies must not modify the payload.
60    pub no_transform:           bool,
61    /// `immutable`: the body never changes, so the browser should not revalidate it even on a reload.
62    pub immutable:              bool,
63    /// `stale-while-revalidate`: for how many seconds a stale response may still be served while it is revalidated in the background.
64    pub stale_while_revalidate: Option<u32>,
65    /// `stale-if-error`: for how many seconds a stale response may still be served after the origin server fails.
66    pub stale_if_error:         Option<u32>,
67}
68
69impl Default for CacheControl {
70    #[inline]
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl CacheControl {
77    /// Set no directive at all, which results in an empty header value.
78    #[inline]
79    pub const fn new() -> Self {
80        Self {
81            cacheability:           Cacheability::Unspecified,
82            max_age:                None,
83            s_max_age:              None,
84            no_cache:               false,
85            must_revalidate:        false,
86            proxy_revalidate:       false,
87            no_transform:           false,
88            immutable:              false,
89            stale_while_revalidate: None,
90            stale_if_error:         None,
91        }
92    }
93
94    /// `public, max-age=<max_age>`: every cache, shared ones included, may reuse the response for `max_age` seconds.
95    #[inline]
96    pub const fn public(max_age: u32) -> Self {
97        let mut cache_control = Self::new();
98
99        cache_control.cacheability = Cacheability::Public;
100        cache_control.max_age = Some(max_age);
101
102        cache_control
103    }
104
105    /// `private, max-age=<max_age>`: only the browser that made the request may reuse the response, for `max_age` seconds.
106    #[inline]
107    pub const fn private(max_age: u32) -> Self {
108        let mut cache_control = Self::new();
109
110        cache_control.cacheability = Cacheability::Private;
111        cache_control.max_age = Some(max_age);
112
113        cache_control
114    }
115
116    /// `no-cache`: a cache may store the response, but it must ask the origin server whether the stored copy is still good before every reuse.
117    #[inline]
118    pub const fn no_cache() -> Self {
119        let mut cache_control = Self::new();
120
121        cache_control.no_cache = true;
122
123        cache_control
124    }
125
126    /// `no-store`: no cache may store the response at all.
127    #[inline]
128    pub const fn no_store() -> Self {
129        let mut cache_control = Self::new();
130
131        cache_control.cacheability = Cacheability::NoStore;
132
133        cache_control
134    }
135
136    /// `public, max-age=31536000, immutable`: the response never changes, so it should be reused for a year without any revalidation.
137    #[inline]
138    pub const fn immutable() -> Self {
139        let mut cache_control = Self::public(IMMUTABLE_MAX_AGE);
140
141        cache_control.immutable = true;
142
143        cache_control
144    }
145
146    /// Format the directives into a `Cache-Control` header value.
147    #[inline]
148    pub fn to_header_value(&self) -> Cow<'static, str> {
149        for (cache_control, header_value) in WELL_KNOWN_HEADER_VALUES {
150            if *self == cache_control {
151                return Cow::Borrowed(header_value);
152            }
153        }
154
155        Cow::Owned(self.to_string())
156    }
157}
158
159impl Display for CacheControl {
160    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
161        let mut separator = "";
162
163        match self.cacheability {
164            Cacheability::Unspecified => (),
165            Cacheability::Public => write_directive(f, &mut separator, "public")?,
166            Cacheability::Private => write_directive(f, &mut separator, "private")?,
167            Cacheability::NoStore => write_directive(f, &mut separator, "no-store")?,
168        }
169
170        if self.no_cache {
171            write_directive(f, &mut separator, "no-cache")?;
172        }
173
174        if let Some(max_age) = self.max_age {
175            write_seconds_directive(f, &mut separator, "max-age", max_age)?;
176        }
177
178        if let Some(s_max_age) = self.s_max_age {
179            write_seconds_directive(f, &mut separator, "s-maxage", s_max_age)?;
180        }
181
182        if self.must_revalidate {
183            write_directive(f, &mut separator, "must-revalidate")?;
184        }
185
186        if self.proxy_revalidate {
187            write_directive(f, &mut separator, "proxy-revalidate")?;
188        }
189
190        if self.no_transform {
191            write_directive(f, &mut separator, "no-transform")?;
192        }
193
194        if self.immutable {
195            write_directive(f, &mut separator, "immutable")?;
196        }
197
198        if let Some(stale_while_revalidate) = self.stale_while_revalidate {
199            write_seconds_directive(
200                f,
201                &mut separator,
202                "stale-while-revalidate",
203                stale_while_revalidate,
204            )?;
205        }
206
207        if let Some(stale_if_error) = self.stale_if_error {
208            write_seconds_directive(f, &mut separator, "stale-if-error", stale_if_error)?;
209        }
210
211        Ok(())
212    }
213}
214
215#[inline]
216fn write_directive(
217    f: &mut Formatter<'_>,
218    separator: &mut &'static str,
219    directive: &str,
220) -> fmt::Result {
221    f.write_str(separator)?;
222    f.write_str(directive)?;
223
224    *separator = ", ";
225
226    Ok(())
227}
228
229#[inline]
230fn write_seconds_directive(
231    f: &mut Formatter<'_>,
232    separator: &mut &'static str,
233    directive: &str,
234    seconds: u32,
235) -> fmt::Result {
236    write_directive(f, separator, directive)?;
237
238    write!(f, "={seconds}")
239}