Skip to main content

ferron/util/
cache_control.rs

1use std::time::Duration;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum Cachability {
5  NoCache,
6  Private,
7  Public,
8}
9
10#[derive(Debug, Clone, Default)]
11pub struct CacheControl {
12  pub no_store: bool,
13  pub no_cache: bool,
14  pub cachability: Option<Cachability>,
15  pub max_age: Option<Duration>,
16  pub s_max_age: Option<Duration>,
17}
18
19impl CacheControl {
20  pub fn from_value(value: &str) -> Option<Self> {
21    let mut cc = CacheControl::default();
22    let parts = value.split(',');
23
24    for part in parts {
25      let part = part.trim();
26      if part.is_empty() {
27        continue;
28      }
29
30      // Split by '=' to handle key=value
31      let mut kv = part.splitn(2, '=');
32      let key = kv.next()?.trim();
33      let value = kv.next().map(|v| v.trim());
34
35      if key.eq_ignore_ascii_case("no-store") {
36        cc.no_store = true;
37      } else if key.eq_ignore_ascii_case("no-cache") {
38        cc.no_cache = true;
39        cc.cachability = Some(Cachability::NoCache);
40      } else if key.eq_ignore_ascii_case("private") {
41        cc.cachability = Some(Cachability::Private);
42      } else if key.eq_ignore_ascii_case("public") {
43        cc.cachability = Some(Cachability::Public);
44      } else if key.eq_ignore_ascii_case("max-age") {
45        if let Some(secs) = value
46          .map(|v| v.trim_matches('"'))
47          .and_then(|v| v.parse::<u64>().ok())
48        {
49          cc.max_age = Some(Duration::from_secs(secs));
50        }
51      } else if key.eq_ignore_ascii_case("s-maxage") {
52        if let Some(secs) = value
53          .map(|v| v.trim_matches('"'))
54          .and_then(|v| v.parse::<u64>().ok())
55        {
56          cc.s_max_age = Some(Duration::from_secs(secs));
57        }
58      }
59    }
60
61    Some(cc)
62  }
63}
64
65#[cfg(test)]
66mod tests {
67  use super::*;
68
69  #[test]
70  fn test_parse_no_store() {
71    let cc = CacheControl::from_value("no-store").unwrap();
72    assert!(cc.no_store);
73  }
74
75  #[test]
76  fn test_parse_no_cache() {
77    let cc = CacheControl::from_value("no-cache").unwrap();
78    assert!(cc.no_cache);
79    assert_eq!(cc.cachability, Some(Cachability::NoCache));
80  }
81
82  #[test]
83  fn test_parse_public_max_age() {
84    let cc = CacheControl::from_value("public, max-age=3600").unwrap();
85    assert_eq!(cc.cachability, Some(Cachability::Public));
86    assert_eq!(cc.max_age, Some(Duration::from_secs(3600)));
87  }
88
89  #[test]
90  fn test_parse_s_maxage() {
91    let cc = CacheControl::from_value("s-maxage=600").unwrap();
92    assert_eq!(cc.s_max_age, Some(Duration::from_secs(600)));
93  }
94
95  #[test]
96  fn test_parse_complex() {
97    let cc = CacheControl::from_value("private, no-store, max-age=0").unwrap();
98    assert_eq!(cc.cachability, Some(Cachability::Private));
99    assert!(cc.no_store);
100    assert_eq!(cc.max_age, Some(Duration::from_secs(0)));
101  }
102
103  #[test]
104  fn test_parse_case_insensitive() {
105    let cc = CacheControl::from_value("PuBlIc, Max-Age=100").unwrap();
106    assert_eq!(cc.cachability, Some(Cachability::Public));
107    assert_eq!(cc.max_age, Some(Duration::from_secs(100)));
108  }
109
110  #[test]
111  fn test_parse_extra_whitespace() {
112    let cc = CacheControl::from_value(" public , max-age = 60 ").unwrap();
113    assert_eq!(cc.cachability, Some(Cachability::Public));
114    assert_eq!(cc.max_age, Some(Duration::from_secs(60)));
115  }
116}