fire_http/fs/
caching.rs

1use crate::header::{
2	RequestHeader, ResponseHeader, StatusCode, CACHE_CONTROL, ETAG,
3	IF_NONE_MATCH,
4};
5use crate::into::IntoResponse;
6use crate::Response;
7
8use std::fmt;
9use std::time::Duration;
10
11use rand::distributions::Alphanumeric;
12use rand::{thread_rng, Rng};
13
14// == 1day
15const DEFAULT_MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24);
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Etag(String);
19
20impl Etag {
21	pub fn new() -> Self {
22		let rand_str: String = thread_rng()
23			.sample_iter(&Alphanumeric)
24			.map(char::from)
25			.take(30)
26			.collect();
27
28		Self(rand_str)
29	}
30
31	pub fn as_str(&self) -> &str {
32		self.0.as_str()
33	}
34}
35
36impl fmt::Display for Etag {
37	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38		self.0.fmt(f)
39	}
40}
41
42impl From<Etag> for String {
43	fn from(e: Etag) -> Self {
44		e.0
45	}
46}
47
48impl PartialEq<&str> for Etag {
49	fn eq(&self, other: &&str) -> bool {
50		self.as_str() == *other
51	}
52}
53
54/// Controls if caching information should be sent.
55///
56/// The Caching struct contains an Etag which stores a random tag which
57/// indentifies a specific file. The Etag gets generated when the struct gets
58/// created.
59///
60/// ## Example
61/// ```ignore
62/// # use fire_http as fire;
63/// use fire::{get, Request};
64/// use fire::fs::Caching;
65/// use fire::into::IntoResponse;
66/// use std::cell::LazyCell;
67///
68///
69/// const INDEX_CACHE: LazyCell<Caching> = LazyCell::new(|| {
70/// 	Caching::default()
71/// });
72///
73/// #[get("/")]
74/// fn index(req: &mut Request) -> Response {
75/// 	let cache = INDEX_CACHE.clone();
76/// 	if cache.if_none_match(req.header()) {
77/// 		return cache.into_response()
78/// 	}
79///
80/// 	let mut resp = Response::html("<h1>Hello, World!</h1>");
81/// 	cache.complete_header(&mut resp.header);
82///
83/// 	resp
84/// }
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Caching {
87	max_age: Duration,
88	etag: Etag,
89}
90
91impl Caching {
92	pub fn new(max_age: Duration) -> Self {
93		Self {
94			max_age,
95			etag: Etag::new(),
96		}
97	}
98
99	// defaults to 1 day
100	pub fn default() -> Self {
101		Self::new(DEFAULT_MAX_AGE)
102	}
103
104	pub fn if_none_match(&self, header: &RequestHeader) -> bool {
105		header
106			.value(IF_NONE_MATCH)
107			.map(|none_match| none_match.len() == 30 && self.etag == none_match)
108			.unwrap_or(false)
109	}
110
111	fn cache_control_string(&self) -> String {
112		format!("max-age={}, public", self.max_age.as_secs())
113	}
114
115	pub fn complete_header(self, header: &mut ResponseHeader) {
116		header
117			.values
118			.insert(CACHE_CONTROL, self.cache_control_string());
119
120		// etag makes only sense with files not 404
121		if header.status_code == StatusCode::OK {
122			header.values.insert(ETAG, String::from(self.etag));
123		}
124	}
125}
126
127impl IntoResponse for Caching {
128	fn into_response(self) -> Response {
129		Response::builder()
130			.status_code(StatusCode::NOT_MODIFIED)
131			.header(CACHE_CONTROL, self.cache_control_string())
132			.header(ETAG, String::from(self.etag))
133			.build()
134	}
135}