cache_control/
lib.rs

1//! Rust crate to parse the HTTP Cache-Control header.
2//! # Example
3//! ```
4//! use cache_control::{Cachability, CacheControl};
5//! use std::time::Duration;
6//!
7//! let cache_control = CacheControl::from_header("Cache-Control: public, max-age=60").unwrap();
8//! assert_eq!(cache_control.cachability, Some(Cachability::Public));
9//! assert_eq!(cache_control.max_age, Some(Duration::from_secs(60)));
10//! ```
11
12use core::time::Duration;
13
14/// How the data may be cached.
15#[derive(Eq, PartialEq, Debug)]
16pub enum Cachability {
17    /// Any cache can cache this data.
18    Public,
19
20    /// Data cannot be cached in shared caches.
21    Private,
22
23    /// No one can cache this data.
24    NoCache,
25
26    /// Cache the data the first time, and use the cache from then on.
27    OnlyIfCached,
28}
29
30/// Represents a Cache-Control header
31#[derive(Eq, PartialEq, Debug, Default)]
32pub struct CacheControl {
33    pub cachability: Option<Cachability>,
34    /// The maximum amount of time a resource is considered fresh.
35    /// Unlike `Expires`, this directive is relative to the time of the request.
36    pub max_age: Option<Duration>,
37    /// Overrides max-age or the `Expires` header, but only for shared caches (e.g., proxies).
38    /// Ignored by private caches.
39    pub s_max_age: Option<Duration>,
40    /// Indicates the client will accept a stale response. An optional value in seconds
41    /// indicates the upper limit of staleness the client will accept.
42    pub max_stale: Option<Duration>,
43    /// Indicates the client wants a response that will still be fresh for at least
44    /// the specified number of seconds.
45    pub min_fresh: Option<Duration>,
46    /// Indicates that once a resource becomes stale, caches do not use their stale
47    /// copy without successful validation on the origin server.
48    pub must_revalidate: bool,
49    /// Like `must-revalidate`, but only for shared caches (e.g., proxies).
50    /// Ignored by private caches.
51    pub proxy_revalidate: bool,
52    /// Indicates that the response body **will not change** over time.
53    pub immutable: bool,
54    /// The response may not be stored in _any_ cache.
55    pub no_store: bool,
56    /// An intermediate cache or proxy cannot edit the response body, 
57    /// `Content-Encoding`, `Content-Range`, or `Content-Type`.
58    pub no_transform: bool,
59}
60
61impl CacheControl {
62    /// Parses the value of the Cache-Control header (i.e. everything after "Cache-Control:").
63    /// ```
64    /// use cache_control::{Cachability, CacheControl};
65    /// use std::time::Duration;
66    ///
67    /// let cache_control = CacheControl::from_value("public, max-age=60").unwrap();
68    /// assert_eq!(cache_control.cachability, Some(Cachability::Public));
69    /// assert_eq!(cache_control.max_age, Some(Duration::from_secs(60)));
70    /// ```
71    pub fn from_value(value: &str) -> Option<Self> {
72        let mut ret = Self::default();
73        for token in value.split(',') {
74            let (key, val) = {
75                let mut split = token.split('=').map(|s| s.trim());
76                (split.next().unwrap(), split.next())
77            };
78
79            match key {
80                "public" => ret.cachability = Some(Cachability::Public),
81                "private" => ret.cachability = Some(Cachability::Private),
82                "no-cache" => ret.cachability = Some(Cachability::NoCache),
83                "only-if-cached" => ret.cachability = Some(Cachability::OnlyIfCached),
84                "max-age" => match val.and_then(|v| v.parse().ok()) {
85                    Some(secs) => ret.max_age = Some(Duration::from_secs(secs)),
86                    None => return None,
87                },
88                "s-maxage" => match val.and_then(|v| v.parse().ok()) {
89                    Some(secs) => ret.s_max_age = Some(Duration::from_secs(secs)),
90                    None => return None,
91                },
92                "max-stale" => match val.and_then(|v| v.parse().ok()) {
93                    Some(secs) => ret.max_stale = Some(Duration::from_secs(secs)),
94                    None => return None,
95                },
96                "min-fresh" => match val.and_then(|v| v.parse().ok()) {
97                    Some(secs) => ret.min_fresh = Some(Duration::from_secs(secs)),
98                    None => return None,
99                },
100                "must-revalidate" => ret.must_revalidate = true,
101                "proxy-revalidate" => ret.proxy_revalidate = true,
102                "immutable" => ret.immutable = true,
103                "no-store" => ret.no_store = true,
104                "no-transform" => ret.no_transform = true,
105                _ => (),
106            };
107        }
108        Some(ret)
109    }
110
111    /// Parses a Cache-Control header.
112    pub fn from_header(value: &str) -> Option<Self> {
113        let (name, value) = value.split_once(':')?;
114        if !name.trim().eq_ignore_ascii_case("Cache-Control") {
115            return None;
116        }
117        Self::from_value(value)
118    }
119}
120
121#[cfg(test)]
122mod test {
123    use super::*;
124
125    #[test]
126    fn test_from_value() {
127        assert_eq!(
128            CacheControl::from_value("").unwrap(),
129            CacheControl::default()
130        );
131        assert_eq!(
132            CacheControl::from_value("private")
133                .unwrap()
134                .cachability
135                .unwrap(),
136            Cachability::Private
137        );
138        assert_eq!(
139            CacheControl::from_value("max-age=60")
140                .unwrap()
141                .max_age
142                .unwrap(),
143            Duration::from_secs(60)
144        );
145    }
146
147    #[test]
148    fn test_from_value_multi() {
149        let test1 = &CacheControl::from_value("no-cache, no-store, must-revalidate").unwrap();
150        assert_eq!(test1.cachability, Some(Cachability::NoCache));
151        assert!(test1.no_store);
152        assert!(test1.must_revalidate);
153        assert_eq!(
154            *test1,
155            CacheControl {
156                cachability: Some(Cachability::NoCache),
157                max_age: None,
158                s_max_age: None,
159                max_stale: None,
160                min_fresh: None,
161                must_revalidate: true,
162                proxy_revalidate: false,
163                immutable: false,
164                no_store: true,
165                no_transform: false,
166            }
167        );
168    }
169
170    #[test]
171    fn test_from_header() {
172        assert_eq!(
173            CacheControl::from_header("Cache-Control: ").unwrap(),
174            CacheControl::default()
175        );
176        assert_eq!(
177            CacheControl::from_header("Cache-Control: private")
178                .unwrap()
179                .cachability
180                .unwrap(),
181            Cachability::Private
182        );
183        assert_eq!(
184            CacheControl::from_header("Cache-Control: max-age=60")
185                .unwrap()
186                .max_age
187                .unwrap(),
188            Duration::from_secs(60)
189        );
190        assert_eq!(CacheControl::from_header("foo"), None);
191        assert_eq!(CacheControl::from_header("bar: max-age=60"), None);
192    }
193
194    #[test]
195    fn test_from_header_multi() {
196        let test1 = &CacheControl::from_header("Cache-Control: public, max-age=600").unwrap();
197        assert_eq!(test1.cachability, Some(Cachability::Public));
198        assert_eq!(test1.max_age, Some(Duration::from_secs(600)));
199        assert_eq!(
200            *test1,
201            CacheControl {
202                cachability: Some(Cachability::Public),
203                max_age: Some(Duration::from_secs(600)),
204                s_max_age: None,
205                max_stale: None,
206                min_fresh: None,
207                must_revalidate: false,
208                proxy_revalidate: false,
209                immutable: false,
210                no_store: false,
211                no_transform: false,
212            }
213        );
214    }
215}