1use std::time::{Duration, SystemTime};
7
8use crate::{
9 date,
10 headers::{Headers, header_keys},
11};
12
13#[derive(Debug, Default, Clone, PartialEq, Eq)]
15pub struct CacheControl {
16 pub no_store: bool,
17 pub no_cache: bool,
18 pub must_revalidate: bool,
19 pub max_age: Option<u64>,
20 pub other: Vec<String>,
21}
22
23#[derive(Debug, Default, Clone, PartialEq, Eq)]
25pub struct CachePolicy {
26 pub control: Option<CacheControl>,
27 pub age: Option<u64>,
28 pub expires: Option<SystemTime>,
29 pub vary: Vec<String>,
30}
31
32pub fn parse_cache_control(value: &str) -> CacheControl {
34 let mut control = CacheControl::default();
35 for directive in value.split(',') {
36 let directive = directive.trim();
37 if directive.is_empty() {
38 continue;
39 }
40 let mut parts = directive.splitn(2, '=');
41 let token = parts.next().unwrap().trim().to_ascii_lowercase();
42 let param = parts.next().map(|p| p.trim_matches('"'));
43 match token.as_str() {
44 "no-store" => control.no_store = true,
45 "no-cache" => control.no_cache = true,
46 "must-revalidate" => control.must_revalidate = true,
47 "max-age" => {
48 if let Some(value) = param.and_then(|p| p.parse::<u64>().ok()) {
49 control.max_age = Some(value);
50 }
51 }
52 _ => control.other.push(directive.to_owned()),
53 }
54 }
55 control
56}
57
58pub fn policy_from_headers(headers: &Headers) -> CachePolicy {
60 let control = headers
61 .get(header_keys::CACHE_CONTROL)
62 .map(parse_cache_control);
63
64 let age = headers
65 .get(header_keys::AGE)
66 .and_then(|value| value.parse::<u64>().ok());
67
68 let expires = headers
69 .get(header_keys::EXPIRES)
70 .and_then(date::parse_http_date);
71
72 let vary = headers
73 .get(header_keys::VARY)
74 .map(|value| value.split(',').map(|v| v.trim().to_string()).collect())
75 .unwrap_or_default();
76
77 CachePolicy {
78 control,
79 age,
80 expires,
81 vary,
82 }
83}
84
85pub fn freshness_lifetime(policy: &CachePolicy, date: Option<SystemTime>) -> Option<Duration> {
87 if let Some(control) = &policy.control {
88 if let Some(max_age) = control.max_age {
89 return Some(Duration::from_secs(max_age));
90 }
91 }
92
93 if let (Some(expires), Some(date_value)) = (policy.expires, date) {
94 if let Ok(delta) = expires.duration_since(date_value) {
95 return Some(delta);
96 }
97 }
98
99 None
100}
101
102pub fn ensure_age_header(headers: &mut Headers) {
104 if !headers.contains(header_keys::AGE) {
105 headers.insert(header_keys::AGE, "0");
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn parse_cache_control_recognises_common_directives() {
115 let control = parse_cache_control("max-age=60, must-revalidate, no-store");
116 assert_eq!(control.max_age, Some(60));
117 assert!(control.must_revalidate);
118 assert!(control.no_store);
119 assert!(!control.no_cache);
120 }
121
122 #[test]
123 fn policy_extracts_age_and_vary() {
124 let mut headers = Headers::new();
125 headers.insert(header_keys::CACHE_CONTROL, "max-age=120");
126 headers.insert(header_keys::AGE, "15");
127 headers.insert(header_keys::VARY, "Accept-Encoding, Accept-Language");
128 let policy = policy_from_headers(&headers);
129 assert_eq!(policy.control.unwrap().max_age, Some(120));
130 assert_eq!(policy.age, Some(15));
131 assert_eq!(policy.vary.len(), 2);
132 }
133}