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
14const 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#[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 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 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}