1use buildstructor::Builder;
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Serialize, Deserialize, Clone, Debug, Builder, PartialEq, Eq)]
8pub struct HttpData {
9 pub content_length: Option<u64>,
10 pub content_type: Option<String>,
11 pub scheme: Option<String>,
12 pub host: String,
13 pub expires: Option<String>,
14 pub cache_control: Option<String>,
15 pub received: DateTime<Utc>,
16 pub status_code: u16,
17 pub location: Option<String>,
18 pub access_control_allow_origin: Option<String>,
19 pub access_control_allow_credentials: Option<String>,
20 pub strict_transport_security: Option<String>,
21 pub retry_after: Option<String>,
22}
23
24#[buildstructor::buildstructor]
25impl HttpData {
26 #[builder(entry = "now")]
27 #[allow(clippy::too_many_arguments)]
28 pub fn new_now(
29 content_length: Option<u64>,
30 content_type: Option<String>,
31 scheme: String,
32 host: String,
33 expires: Option<String>,
34 cache_control: Option<String>,
35 status_code: Option<u16>,
36 location: Option<String>,
37 access_control_allow_origin: Option<String>,
38 access_control_allow_credentials: Option<String>,
39 strict_transport_security: Option<String>,
40 retry_after: Option<String>,
41 ) -> Self {
42 Self {
43 content_length,
44 content_type,
45 scheme: Some(scheme),
46 host,
47 expires,
48 cache_control,
49 received: Utc::now(),
50 status_code: status_code.unwrap_or(200),
51 location,
52 access_control_allow_origin,
53 access_control_allow_credentials,
54 strict_transport_security,
55 retry_after,
56 }
57 }
58
59 #[builder(entry = "example")]
60 #[allow(clippy::too_many_arguments)]
61 pub fn new_example(
62 content_length: Option<u64>,
63 content_type: Option<String>,
64 expires: Option<String>,
65 cache_control: Option<String>,
66 status_code: Option<u16>,
67 location: Option<String>,
68 access_control_allow_origin: Option<String>,
69 access_control_allow_credentials: Option<String>,
70 strict_transport_security: Option<String>,
71 retry_after: Option<String>,
72 ) -> Self {
73 Self {
74 content_length,
75 content_type,
76 scheme: Some("http".to_string()),
77 host: "example.com".to_string(),
78 expires,
79 cache_control,
80 received: Utc::now(),
81 status_code: status_code.unwrap_or(200),
82 location,
83 access_control_allow_origin,
84 access_control_allow_credentials,
85 strict_transport_security,
86 retry_after,
87 }
88 }
89
90 pub fn is_expired(&self, max_age: i64) -> bool {
91 let now = Utc::now();
92 if now >= self.received + Duration::seconds(max_age) {
93 return true;
94 }
95 if let Some(cache_control) = &self.cache_control {
96 let cc_max_age = cache_control
97 .split(',')
98 .map(|s| s.trim())
99 .find(|s| s.starts_with("max-age="));
100 if let Some(cc_max_age) = cc_max_age {
101 let cc_max_age = cc_max_age.trim_start_matches("max-age=").parse::<i64>();
102 if let Ok(cc_max_age) = cc_max_age {
103 return now >= self.received + Duration::seconds(cc_max_age);
104 }
105 }
106 }
107 if let Some(expires) = &self.expires {
108 let expire_time = DateTime::parse_from_rfc2822(expires);
109 if let Ok(expire_time) = expire_time {
110 return now >= expire_time;
111 } else {
112 return false;
113 }
114 }
115 false
116 }
117
118 pub fn should_cache(&self) -> bool {
119 if let Some(cache_control) = &self.cache_control {
120 return !cache_control
121 .split(',')
122 .map(|s| s.trim())
123 .any(|s| s.eq("no-store") || s.eq("no-cache"));
124 }
125 true
126 }
127
128 pub fn from_lines(lines: &[String]) -> Result<(Self, &[String]), serde_json::Error> {
129 let count = lines.iter().take_while(|s| !s.starts_with("---")).count();
130 let cache_data = lines
131 .iter()
132 .take(count)
133 .cloned()
134 .collect::<Vec<String>>()
135 .join("");
136 let cache_data = serde_json::from_str(&cache_data)?;
137 Ok((cache_data, &lines[count + 1..]))
138 }
139
140 pub fn to_lines(&self, data: &str) -> Result<String, serde_json::Error> {
141 let mut lines = serde_json::to_string(self)?;
142 lines.push_str("\n---\n");
143 lines.push_str(data);
144 Ok(lines)
145 }
146}
147
148#[cfg(test)]
149#[allow(non_snake_case)]
150mod tests {
151 use super::HttpData;
152 use chrono::Duration;
153 use chrono::Utc;
154 use rstest::rstest;
155
156 #[rstest]
157 #[case(HttpData::example().cache_control("max-age=0").build(), 100, true)]
158 #[case(HttpData::example().cache_control("max-age=100").build(), 0, true)]
159 #[case(HttpData::example().cache_control("max-age=100").build(), 50, false)]
160 #[case(HttpData::example().build(), 0, true)]
161 #[case(HttpData::example().build(), 100, false)]
162 #[case(HttpData::example().expires(Utc::now().to_rfc2822()).build(), 100, true)]
163 #[case(HttpData::example().expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, false)]
164 #[case(HttpData::example().expires((Utc::now() + Duration::seconds(100)).to_rfc2822()).build(), 50, false)]
165 #[case(HttpData::example().cache_control("max-age=100").expires(Utc::now().to_rfc2822()).build(), 100, false)]
166 #[case(HttpData::example().cache_control("max-age=0").expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, true)]
167 fn GIVEN_cache_data_and_max_age_WHEN_is_expired_THEN_correct(
168 #[case] cache_data: HttpData,
169 #[case] max_age: i64,
170 #[case] expected: bool,
171 ) {
172 let actual = cache_data.is_expired(max_age);
176
177 assert_eq!(actual, expected);
179 }
180
181 #[rstest]
182 #[case(HttpData::example().cache_control("no-cache").build(), false)]
183 #[case(HttpData::example().cache_control("no-store").build(), false)]
184 #[case(HttpData::example().cache_control("max-age=40").build(), true)]
185 fn GIVEN_cache_control_WHEN_should_cache_THEN_correc(
186 #[case] cache_data: HttpData,
187 #[case] expected: bool,
188 ) {
189 let actual = cache_data.should_cache();
193
194 assert_eq!(actual, expected);
196 }
197
198 #[test]
199 fn GIVEN_data_and_data_cache_WHEN_to_lines_THEN_format_correct() {
200 let data = "foo";
202 let cache_data = HttpData::example().content_length(14).build();
203
204 let actual = cache_data.to_lines(data).unwrap();
206
207 let expected = format!("{}\n---\nfoo", serde_json::to_string(&cache_data).unwrap());
209 assert_eq!(actual, expected);
210 }
211
212 #[test]
213 fn GIVEN_lines_WHEN_from_lines_THEN_parse_correctly() {
214 let data = "foo";
216 let cache_data = HttpData::example().content_length(14).build();
217 let lines = cache_data
218 .to_lines(data)
219 .unwrap()
220 .split('\n')
221 .map(|s| s.to_string())
222 .collect::<Vec<String>>();
223
224 let actual = HttpData::from_lines(&lines).expect("parsing lines");
226
227 assert_eq!(cache_data, actual.0);
229 assert_eq!(vec![data], actual.1);
230 }
231}