Skip to main content

trillium_caching_headers/
cache_control.rs

1use CacheControlDirective::*;
2use std::{
3    fmt::{Display, Write},
4    ops::{Deref, DerefMut},
5    str::FromStr,
6    time::Duration,
7};
8use trillium::{Conn, Handler, HeaderValues, KnownHeaderName};
9/// An enum representation of the
10/// [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
11/// directives.
12#[derive(Debug, Clone, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum CacheControlDirective {
15    /// [`immutable`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#revalidation_and_reloading)
16    Immutable,
17
18    /// [`max-age`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#expiration)
19    MaxAge(Duration),
20
21    /// [`max-fresh`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#expiration)
22    MaxFresh(Duration),
23
24    /// [`max-stale`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#expiration)
25    MaxStale(Option<Duration>),
26
27    /// [`must-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#revalidation_and_reloading)
28    MustRevalidate,
29
30    /// [`no-cache`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#cacheability)
31    NoCache,
32
33    /// [`no-store`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#cacheability)
34    NoStore,
35
36    /// [`no-transform`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#other)
37    NoTransform,
38
39    /// [`only-if-cached`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#other)
40    OnlyIfCached,
41
42    /// [`private`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#cacheability)
43    Private,
44
45    /// [`proxy-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#revalidation_and_reloading)
46    ProxyRevalidate,
47
48    /// [`public`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#cacheability)
49    Public,
50
51    /// [`s-maxage`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#expiration)
52    SMaxage(Duration),
53
54    /// [`stale-if-error`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#expiration)
55    StaleIfError(Duration),
56
57    /// [`stale-while-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#expiration)
58    StaleWhileRevalidate(Duration),
59
60    /// an enum variant that will contain any unrecognized directives
61    UnknownDirective(String),
62}
63
64impl Handler for CacheControlDirective {
65    async fn run(&self, conn: Conn) -> Conn {
66        conn.with_response_header(KnownHeaderName::CacheControl, self.clone())
67    }
68}
69
70impl Handler for CacheControlHeader {
71    async fn run(&self, conn: Conn) -> Conn {
72        conn.with_response_header(KnownHeaderName::CacheControl, self.clone())
73    }
74}
75
76/// A representation of the
77/// [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
78/// header.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct CacheControlHeader(Vec<CacheControlDirective>);
81
82/// Construct a CacheControlHeader. Alias for [`CacheControlHeader::new`]
83pub fn cache_control(into: impl Into<CacheControlHeader>) -> CacheControlHeader {
84    into.into()
85}
86
87impl<T> From<T> for CacheControlHeader
88where
89    T: IntoIterator<Item = CacheControlDirective>,
90{
91    fn from(directives: T) -> Self {
92        directives.into_iter().collect()
93    }
94}
95
96impl From<CacheControlDirective> for CacheControlHeader {
97    fn from(directive: CacheControlDirective) -> Self {
98        Self(vec![directive])
99    }
100}
101
102impl FromIterator<CacheControlDirective> for CacheControlHeader {
103    fn from_iter<T: IntoIterator<Item = CacheControlDirective>>(iter: T) -> Self {
104        Self(iter.into_iter().collect())
105    }
106}
107
108impl From<CacheControlDirective> for HeaderValues {
109    fn from(ccd: CacheControlDirective) -> HeaderValues {
110        let header: CacheControlHeader = ccd.into();
111        header.into()
112    }
113}
114
115impl From<CacheControlHeader> for HeaderValues {
116    fn from(cch: CacheControlHeader) -> Self {
117        cch.to_string().into()
118    }
119}
120
121impl CacheControlHeader {
122    /// construct a new cache control header. alias for [`CacheControlHeader::from`]
123    pub fn new(into: impl Into<Self>) -> Self {
124        into.into()
125    }
126
127    /// returns true if one of the directives is `immutable`
128    pub fn is_immutable(&self) -> bool {
129        self.contains(&Immutable)
130    }
131
132    /// returns a duration if one of the directives is `max-age`
133    pub fn max_age(&self) -> Option<Duration> {
134        self.iter().find_map(|d| match d {
135            MaxAge(d) => Some(*d),
136            _ => None,
137        })
138    }
139
140    /// returns a duration if one of the directives is `max-fresh`
141    pub fn max_fresh(&self) -> Option<Duration> {
142        self.iter().find_map(|d| match d {
143            MaxFresh(d) => Some(*d),
144            _ => None,
145        })
146    }
147
148    /// returns Some(None) if one of the directives is `max-stale` but
149    /// no value is provided. returns Some(Some(duration)) if one of
150    /// the directives is max-stale and includes a duration in
151    /// seconds, such as `max-stale=3600`. Returns None if there is no
152    /// `max-stale` directive
153    pub fn max_stale(&self) -> Option<Option<Duration>> {
154        self.iter().find_map(|d| match d {
155            MaxStale(d) => Some(*d),
156            _ => None,
157        })
158    }
159
160    /// returns true if this header contains a `must-revalidate` directive
161    pub fn must_revalidate(&self) -> bool {
162        self.contains(&MustRevalidate)
163    }
164
165    /// returns true if this header contains a `no-cache` directive
166    pub fn is_no_cache(&self) -> bool {
167        self.contains(&NoCache)
168    }
169
170    /// returns true if this header contains a `no-store` directive
171    pub fn is_no_store(&self) -> bool {
172        self.contains(&NoStore)
173    }
174
175    /// returns true if this header contains a `no-transform`
176    /// directive
177    pub fn is_no_transform(&self) -> bool {
178        self.contains(&NoTransform)
179    }
180
181    /// returns true if this header contains an `only-if-cached`
182    /// directive
183    pub fn is_only_if_cached(&self) -> bool {
184        self.contains(&OnlyIfCached)
185    }
186
187    /// returns true if this header contains a `private` directive
188    pub fn is_private(&self) -> bool {
189        self.contains(&Private)
190    }
191
192    /// returns true if this header contains a `proxy-revalidate`
193    /// directive
194    pub fn is_proxy_revalidate(&self) -> bool {
195        self.contains(&ProxyRevalidate)
196    }
197
198    /// returns true if this header contains a `proxy` directive
199    pub fn is_public(&self) -> bool {
200        self.contains(&Public)
201    }
202
203    /// returns a duration if this header contains an `s-maxage`
204    /// directive
205    pub fn s_maxage(&self) -> Option<Duration> {
206        self.iter().find_map(|h| match h {
207            SMaxage(d) => Some(*d),
208            _ => None,
209        })
210    }
211
212    /// returns a duration if this header contains a stale-if-error
213    /// directive
214    pub fn stale_if_error(&self) -> Option<Duration> {
215        self.iter().find_map(|h| match h {
216            StaleIfError(d) => Some(*d),
217            _ => None,
218        })
219    }
220
221    /// returns a duration if this header contains a
222    /// stale-while-revalidate directive
223    pub fn stale_while_revalidate(&self) -> Option<Duration> {
224        self.iter().find_map(|h| match h {
225            StaleWhileRevalidate(d) => Some(*d),
226            _ => None,
227        })
228    }
229}
230
231impl Deref for CacheControlHeader {
232    type Target = [CacheControlDirective];
233
234    fn deref(&self) -> &Self::Target {
235        self.0.as_slice()
236    }
237}
238
239impl DerefMut for CacheControlHeader {
240    fn deref_mut(&mut self) -> &mut Self::Target {
241        self.0.as_mut_slice()
242    }
243}
244
245#[derive(Debug, Clone, Copy)]
246pub struct CacheControlParseError;
247impl std::error::Error for CacheControlParseError {}
248impl Display for CacheControlParseError {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.write_str("cache control parse error")
251    }
252}
253
254impl Display for CacheControlHeader {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        let mut first = true;
257        for directive in &self.0 {
258            if first {
259                first = false;
260            } else {
261                f.write_char(',')?;
262            }
263
264            match directive {
265                Immutable => write!(f, "immutable"),
266                MaxAge(d) => write!(f, "max-age={}", d.as_secs()),
267                MaxFresh(d) => write!(f, "max-fresh={}", d.as_secs()),
268                MaxStale(Some(d)) => write!(f, "max-stale={}", d.as_secs()),
269                MaxStale(None) => write!(f, "max-stale"),
270                MustRevalidate => write!(f, "must-revalidate"),
271                NoCache => write!(f, "no-cache"),
272                NoStore => write!(f, "no-store"),
273                NoTransform => write!(f, "no-transform"),
274                OnlyIfCached => write!(f, "only-if-cached"),
275                Private => write!(f, "private"),
276                ProxyRevalidate => write!(f, "proxy-revalidate"),
277                Public => write!(f, "public"),
278                SMaxage(d) => write!(f, "s-maxage={}", d.as_secs()),
279                StaleIfError(d) => write!(f, "stale-if-error={}", d.as_secs()),
280                StaleWhileRevalidate(d) => write!(f, "stale-while-revalidate={}", d.as_secs()),
281                UnknownDirective(directive) => write!(f, "{directive}"),
282            }?;
283        }
284
285        Ok(())
286    }
287}
288
289impl FromStr for CacheControlHeader {
290    type Err = CacheControlParseError;
291
292    fn from_str(s: &str) -> Result<Self, Self::Err> {
293        s.to_ascii_lowercase()
294            .split(',')
295            .map(str::trim)
296            .map(|directive| match directive {
297                "immutable" => Ok(Immutable),
298                "must-revalidate" => Ok(MustRevalidate),
299                "no-cache" => Ok(NoCache),
300                "no-store" => Ok(NoStore),
301                "no-transform" => Ok(NoTransform),
302                "only-if-cached" => Ok(OnlyIfCached),
303                "private" => Ok(Private),
304                "proxy-revalidate" => Ok(ProxyRevalidate),
305                "public" => Ok(Public),
306                "max-stale" => Ok(MaxStale(None)),
307                other => match other.split_once('=') {
308                    Some((directive, number)) => {
309                        let seconds = number.parse().map_err(|_| CacheControlParseError)?;
310                        let seconds = Duration::from_secs(seconds);
311                        match directive {
312                            "max-age" => Ok(MaxAge(seconds)),
313                            "max-fresh" => Ok(MaxFresh(seconds)),
314                            "max-stale" => Ok(MaxStale(Some(seconds))),
315                            "s-maxage" => Ok(SMaxage(seconds)),
316                            "stale-if-error" => Ok(StaleIfError(seconds)),
317                            "stale-while-revalidate" => Ok(StaleWhileRevalidate(seconds)),
318                            _ => Ok(UnknownDirective(String::from(other))),
319                        }
320                    }
321
322                    None => Ok(UnknownDirective(String::from(other))),
323                },
324            })
325            .collect::<Result<Vec<_>, _>>()
326            .map(Self)
327    }
328}
329#[cfg(test)]
330mod test {
331    use super::*;
332    #[test]
333    fn parse() {
334        assert_eq!(
335            CacheControlHeader(vec![NoStore]),
336            "no-store".parse().unwrap()
337        );
338
339        let long = "private,no-cache,no-store,max-age=0,must-revalidate,pre-check=0,post-check=0"
340            .parse()
341            .unwrap();
342
343        assert_eq!(
344            CacheControlHeader::from([
345                Private,
346                NoCache,
347                NoStore,
348                MaxAge(Duration::ZERO),
349                MustRevalidate,
350                UnknownDirective("pre-check=0".to_string()),
351                UnknownDirective("post-check=0".to_string())
352            ]),
353            long
354        );
355
356        assert_eq!(
357            long.to_string(),
358            "private,no-cache,no-store,max-age=0,must-revalidate,pre-check=0,post-check=0"
359        );
360
361        assert_eq!(
362            CacheControlHeader::from([Public, MaxAge(Duration::from_secs(604800)), Immutable]),
363            "public, max-age=604800, immutable".parse().unwrap()
364        );
365    }
366}