trillium_caching_headers/
cache_control.rs

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