headers_ext/common/
cache_control.rs

1use std::fmt;
2use std::iter::FromIterator;
3use std::str::FromStr;
4use std::time::Duration;
5
6use util::{self, csv, Seconds};
7use {HeaderValue};
8
9/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
10///
11/// The `Cache-Control` header field is used to specify directives for
12/// caches along the request/response chain.  Such cache directives are
13/// unidirectional in that the presence of a directive in a request does
14/// not imply that the same directive is to be given in the response.
15///
16/// ## ABNF
17///
18/// ```text
19/// Cache-Control   = 1#cache-directive
20/// cache-directive = token [ "=" ( token / quoted-string ) ]
21/// ```
22///
23/// ## Example values
24///
25/// * `no-cache`
26/// * `private, community="UCI"`
27/// * `max-age=30`
28///
29/// # Example
30///
31/// ```
32/// # extern crate headers_ext as headers;
33/// use headers::CacheControl;
34///
35/// let cc = CacheControl::new();
36/// ```
37#[derive(PartialEq, Clone, Debug)]
38pub struct CacheControl {
39    flags: Flags,
40    max_age: Option<Seconds>,
41    max_stale: Option<Seconds>,
42    min_fresh: Option<Seconds>,
43    s_max_age: Option<Seconds>,
44}
45
46bitflags! {
47    struct Flags: u32 {
48        const NO_CACHE         = 0b00000001;
49        const NO_STORE         = 0b00000010;
50        const NO_TRANSFORM     = 0b00000100;
51        const ONLY_IF_CACHED   = 0b00001000;
52        const MUST_REVALIDATE  = 0b00010000;
53        const PUBLIC           = 0b00100000;
54        const PRIVATE          = 0b01000000;
55        const PROXY_REVALIDATE = 0b10000000;
56    }
57}
58
59impl CacheControl {
60    /// Construct a new empty `CacheControl` header.
61    pub fn new() -> Self {
62        CacheControl {
63            flags: Flags::empty(),
64            max_age: None,
65            max_stale: None,
66            min_fresh: None,
67            s_max_age: None,
68        }
69    }
70
71    // getters
72
73    /// Check if the `no-cache` directive is set.
74    pub fn no_cache(&self) -> bool {
75        self.flags.contains(Flags::NO_CACHE)
76    }
77
78    /// Check if the `no-store` directive is set.
79    pub fn no_store(&self) -> bool {
80        self.flags.contains(Flags::NO_STORE)
81    }
82
83    /// Check if the `no-transform` directive is set.
84    pub fn no_transform(&self) -> bool {
85        self.flags.contains(Flags::NO_TRANSFORM)
86    }
87
88    /// Check if the `only-if-cached` directive is set.
89    pub fn only_if_cached(&self) -> bool {
90        self.flags.contains(Flags::ONLY_IF_CACHED)
91    }
92
93    /// Check if the `public` directive is set.
94    pub fn public(&self) -> bool {
95        self.flags.contains(Flags::PUBLIC)
96    }
97
98    /// Check if the `private` directive is set.
99    pub fn private(&self) -> bool {
100        self.flags.contains(Flags::PRIVATE)
101    }
102
103    /// Get the value of the `max-age` directive if set.
104    pub fn max_age(&self) -> Option<Duration> {
105        self.max_age.map(Into::into)
106    }
107
108    /// Get the value of the `max-stale` directive if set.
109    pub fn max_stale(&self) -> Option<Duration> {
110        self.max_stale.map(Into::into)
111    }
112
113
114    /// Get the value of the `min-fresh` directive if set.
115    pub fn min_fresh(&self) -> Option<Duration> {
116        self.min_fresh.map(Into::into)
117    }
118
119    /// Get the value of the `s-maxage` directive if set.
120    pub fn s_max_age(&self) -> Option<Duration> {
121        self.s_max_age.map(Into::into)
122    }
123
124    // setters
125
126    /// Set the `no-cache` directive.
127    pub fn with_no_cache(mut self) -> Self {
128        self.flags.insert(Flags::NO_CACHE);
129        self
130    }
131
132    /// Set the `no-store` directive.
133    pub fn with_no_store(mut self) -> Self {
134        self.flags.insert(Flags::NO_STORE);
135        self
136    }
137
138    /// Set the `no-transform` directive.
139    pub fn with_no_transform(mut self) -> Self {
140        self.flags.insert(Flags::NO_TRANSFORM);
141        self
142    }
143
144    /// Set the `only-if-cached` directive.
145    pub fn with_only_if_cached(mut self) -> Self {
146        self.flags.insert(Flags::ONLY_IF_CACHED);
147        self
148    }
149
150    /// Set the `private` directive.
151    pub fn with_private(mut self) -> Self {
152        self.flags.insert(Flags::PRIVATE);
153        self
154    }
155
156    /// Set the `public` directive.
157    pub fn with_public(mut self) -> Self {
158        self.flags.insert(Flags::PUBLIC);
159        self
160    }
161
162    /// Set the `max-age` directive.
163    pub fn with_max_age(mut self, seconds: Duration) -> Self {
164        self.max_age = Some(seconds.into());
165        self
166    }
167
168    /// Set the `max-stale` directive.
169    pub fn with_max_stale(mut self, seconds: Duration) -> Self {
170        self.max_stale = Some(seconds.into());
171        self
172    }
173
174    /// Set the `min-fresh` directive.
175    pub fn with_min_fresh(mut self, seconds: Duration) -> Self {
176        self.min_fresh = Some(seconds.into());
177        self
178    }
179
180    /// Set the `s-maxage` directive.
181    pub fn with_s_max_age(mut self, seconds: Duration) -> Self {
182        self.s_max_age = Some(seconds.into());
183        self
184    }
185}
186
187impl ::Header for CacheControl {
188    const NAME: &'static ::HeaderName = &::http::header::CACHE_CONTROL;
189
190    fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, ::Error> {
191        csv::from_comma_delimited(values)
192            .map(|FromIter(cc)| cc)
193
194    }
195
196    fn encode<E: Extend<::HeaderValue>>(&self, values: &mut E) {
197        values.extend(::std::iter::once(util::fmt(Fmt(self))));
198    }
199}
200
201// Adapter to be used in Header::decode
202struct FromIter(CacheControl);
203
204impl FromIterator<KnownDirective> for FromIter {
205    fn from_iter<I>(iter: I) -> Self
206    where
207        I: IntoIterator<Item=KnownDirective>,
208    {
209        let mut cc = CacheControl::new();
210
211        // ignore all unknown directives
212        let iter = iter
213            .into_iter()
214            .filter_map(|dir| match dir {
215                KnownDirective::Known(dir) => Some(dir),
216                KnownDirective::Unknown => None,
217            });
218
219        for directive in iter {
220            match directive {
221                Directive::NoCache => {
222                    cc.flags.insert(Flags::NO_CACHE);
223                },
224                Directive::NoStore => {
225                    cc.flags.insert(Flags::NO_STORE);
226                },
227                Directive::NoTransform => {
228                    cc.flags.insert(Flags::NO_TRANSFORM);
229                },
230                Directive::OnlyIfCached => {
231                    cc.flags.insert(Flags::ONLY_IF_CACHED);
232                },
233                Directive::MustRevalidate => {
234                    cc.flags.insert(Flags::MUST_REVALIDATE);
235                },
236                Directive::Public => {
237                    cc.flags.insert(Flags::PUBLIC);
238                },
239                Directive::Private => {
240                    cc.flags.insert(Flags::PRIVATE);
241                },
242                Directive::ProxyRevalidate => {
243                    cc.flags.insert(Flags::PROXY_REVALIDATE);
244                },
245                Directive::MaxAge(secs) => {
246                    cc.max_age = Some(Duration::from_secs(secs.into()).into());
247                },
248                Directive::MaxStale(secs) => {
249                    cc.max_stale = Some(Duration::from_secs(secs.into()).into());
250                },
251                Directive::MinFresh(secs) => {
252                    cc.min_fresh = Some(Duration::from_secs(secs.into()).into());
253                },
254                Directive::SMaxAge(secs) => {
255                    cc.s_max_age = Some(Duration::from_secs(secs.into()).into());
256                },
257            }
258        }
259
260        FromIter(cc)
261    }
262}
263
264struct Fmt<'a>(&'a CacheControl);
265
266impl<'a> fmt::Display for Fmt<'a> {
267    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
268        let if_flag = |f: Flags, dir: Directive| {
269            if self.0.flags.contains(f) {
270                Some(dir)
271            } else {
272                None
273            }
274        };
275
276        let slice = &[
277            if_flag(Flags::NO_CACHE, Directive::NoCache),
278            if_flag(Flags::NO_STORE, Directive::NoStore),
279            if_flag(Flags::NO_TRANSFORM, Directive::NoTransform),
280            if_flag(Flags::ONLY_IF_CACHED, Directive::OnlyIfCached),
281            if_flag(Flags::MUST_REVALIDATE, Directive::MustRevalidate),
282            if_flag(Flags::PUBLIC, Directive::Public),
283            if_flag(Flags::PRIVATE, Directive::Private),
284            if_flag(Flags::PROXY_REVALIDATE, Directive::ProxyRevalidate),
285            self.0.max_age.as_ref().map(|s| Directive::MaxAge(s.as_u64())),
286            self.0.max_stale.as_ref().map(|s| Directive::MaxStale(s.as_u64())),
287            self.0.min_fresh.as_ref().map(|s| Directive::MinFresh(s.as_u64())),
288            self.0.s_max_age.as_ref().map(|s| Directive::SMaxAge(s.as_u64())),
289        ];
290
291        let iter = slice
292            .iter()
293            .filter_map(|o| *o);
294
295        csv::fmt_comma_delimited(f, iter)
296    }
297}
298
299#[derive(Clone, Copy)]
300enum KnownDirective {
301    Known(Directive),
302    Unknown,
303}
304
305#[derive(Clone, Copy)]
306enum Directive {
307    NoCache,
308    NoStore,
309    NoTransform,
310    OnlyIfCached,
311
312    // request directives
313    MaxAge(u64),
314    MaxStale(u64),
315    MinFresh(u64),
316
317    // response directives
318    MustRevalidate,
319    Public,
320    Private,
321    ProxyRevalidate,
322    SMaxAge(u64),
323}
324
325impl fmt::Display for Directive {
326    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
327        fmt::Display::fmt(match *self {
328            Directive::NoCache => "no-cache",
329            Directive::NoStore => "no-store",
330            Directive::NoTransform => "no-transform",
331            Directive::OnlyIfCached => "only-if-cached",
332
333            Directive::MaxAge(secs) => return write!(f, "max-age={}", secs),
334            Directive::MaxStale(secs) => return write!(f, "max-stale={}", secs),
335            Directive::MinFresh(secs) => return write!(f, "min-fresh={}", secs),
336
337            Directive::MustRevalidate => "must-revalidate",
338            Directive::Public => "public",
339            Directive::Private => "private",
340            Directive::ProxyRevalidate => "proxy-revalidate",
341            Directive::SMaxAge(secs) => return write!(f, "s-maxage={}", secs),
342        }, f)
343    }
344}
345
346impl FromStr for KnownDirective {
347    type Err = ();
348    fn from_str(s: &str) -> Result<Self, Self::Err> {
349        Ok(KnownDirective::Known(match s {
350            "no-cache" => Directive::NoCache,
351            "no-store" => Directive::NoStore,
352            "no-transform" => Directive::NoTransform,
353            "only-if-cached" => Directive::OnlyIfCached,
354            "must-revalidate" => Directive::MustRevalidate,
355            "public" => Directive::Public,
356            "private" => Directive::Private,
357            "proxy-revalidate" => Directive::ProxyRevalidate,
358            "" => return Err(()),
359            _ => match s.find('=') {
360                Some(idx) if idx+1 < s.len() => match (&s[..idx], (&s[idx+1..]).trim_matches('"')) {
361                    ("max-age" , secs) => secs.parse().map(Directive::MaxAge).map_err(|_| ())?,
362                    ("max-stale", secs) => secs.parse().map(Directive::MaxStale).map_err(|_| ())?,
363                    ("min-fresh", secs) => secs.parse().map(Directive::MinFresh).map_err(|_| ())?,
364                    ("s-maxage", secs) => secs.parse().map(Directive::SMaxAge).map_err(|_| ())?,
365                    _unknown => return Ok(KnownDirective::Unknown),
366                },
367                Some(_) | None => return Ok(KnownDirective::Unknown),
368            }
369        }))
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use super::super::{test_decode, test_encode};
377
378    #[test]
379    fn test_parse_multiple_headers() {
380        assert_eq!(
381            test_decode::<CacheControl>(&["no-cache", "private"]).unwrap(),
382            CacheControl::new()
383                .with_no_cache()
384                .with_private(),
385        );
386    }
387
388    #[test]
389    fn test_parse_argument() {
390        assert_eq!(
391            test_decode::<CacheControl>(&["max-age=100, private"]).unwrap(),
392            CacheControl::new()
393                .with_max_age(Duration::from_secs(100))
394                .with_private(),
395        );
396    }
397
398    #[test]
399    fn test_parse_quote_form() {
400        assert_eq!(
401            test_decode::<CacheControl>(&["max-age=\"200\""]).unwrap(),
402            CacheControl::new()
403                .with_max_age(Duration::from_secs(200)),
404        );
405    }
406
407    #[test]
408    fn test_parse_extension() {
409        assert_eq!(
410            test_decode::<CacheControl>(&["foo, no-cache, bar=baz"]).unwrap(),
411            CacheControl::new()
412                .with_no_cache(),
413            "unknown extensions are ignored but shouldn't fail parsing",
414        );
415    }
416
417    #[test]
418    fn test_parse_bad_syntax() {
419        assert_eq!(
420            test_decode::<CacheControl>(&["max-age=lolz"]),
421            None,
422        );
423    }
424
425    #[test]
426    fn encode_one_flag_directive() {
427        let cc = CacheControl::new()
428            .with_no_cache();
429
430        let headers = test_encode(cc);
431        assert_eq!(headers["cache-control"], "no-cache");
432    }
433
434    #[test]
435    fn encode_one_param_directive() {
436        let cc = CacheControl::new()
437            .with_max_age(Duration::from_secs(300));
438
439        let headers = test_encode(cc);
440        assert_eq!(headers["cache-control"], "max-age=300");
441    }
442
443    #[test]
444    fn encode_two_directive() {
445        let headers = test_encode(
446            CacheControl::new()
447                .with_no_cache()
448                .with_private()
449        );
450        assert_eq!(headers["cache-control"], "no-cache, private");
451
452        let headers = test_encode(
453            CacheControl::new()
454                .with_no_cache()
455                .with_max_age(Duration::from_secs(100))
456        );
457        assert_eq!(headers["cache-control"], "no-cache, max-age=100");
458    }
459}
460