1use crate::response::Response;
2use http::Method;
3use std::time::{Duration, SystemTime};
4
5#[derive(Debug, Clone)]
6pub struct CacheEntry {
7 pub response: Response,
8 pub expires: SystemTime,
9 pub etag: Option<String>,
10 pub last_modified: Option<String>,
11}
12
13#[derive(Debug)]
14pub enum CacheStatus {
15 Fresh(Response),
17 Revalidate(Response, Option<String>, Option<String>),
20 Miss,
22}
23
24pub struct HttpCache {
25 entries: std::collections::HashMap<String, CacheEntry>,
27}
28
29impl Default for HttpCache {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl HttpCache {
36 pub fn new() -> Self {
37 Self {
38 entries: std::collections::HashMap::new(),
39 }
40 }
41
42 pub fn get(&self, method: &Method, url: &str) -> CacheStatus {
43 if method != Method::GET {
44 return CacheStatus::Miss;
45 }
46
47 if let Some(entry) = self.entries.get(url) {
48 if entry.expires > SystemTime::now() {
49 return CacheStatus::Fresh(entry.response.clone());
50 } else {
51 if entry.etag.is_some() || entry.last_modified.is_some() {
53 return CacheStatus::Revalidate(
54 entry.response.clone(),
55 entry.etag.clone(),
56 entry.last_modified.clone(),
57 );
58 }
59 }
60 }
61 CacheStatus::Miss
62 }
63
64 pub fn store(&mut self, url: &str, response: &Response) {
65 if let Some(cc) = response.get_header("cache-control") {
67 if cc.contains("no-store") {
68 return;
69 }
70
71 let ttl = if let Some(pos) = cc.find("max-age=") {
74 let start = pos + 8;
75 let end = cc[start..].find(',').map(|i| start + i).unwrap_or(cc.len());
76 cc[start..end].trim().parse::<u64>().unwrap_or(0)
77 } else {
78 0
79 };
80
81 if ttl == 0 {
82 return;
84 }
85
86 let expires = SystemTime::now() + Duration::from_secs(ttl);
87
88 let etag = response.get_header("etag").map(|s| s.to_string());
89 let last_modified = response.get_header("last-modified").map(|s| s.to_string());
90
91 let entry = CacheEntry {
92 response: response.clone(),
93 expires,
94 etag,
95 last_modified,
96 };
97 self.entries.insert(url.to_string(), entry);
98 }
99 }
100}