1use {
4 chrono::{DateTime, Duration, Utc},
5 serde::{Deserialize, Serialize},
6};
7
8#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
10pub struct HttpData {
11 pub content_length: Option<u64>,
12 pub content_type: Option<String>,
13 pub scheme: Option<String>,
14 pub host: String,
15 pub expires: Option<String>,
16 pub cache_control: Option<String>,
17 pub received: DateTime<Utc>,
18 pub status_code: u16,
19 pub location: Option<String>,
20 pub access_control_allow_origin: Option<String>,
21 pub access_control_allow_credentials: Option<String>,
22 pub strict_transport_security: Option<String>,
23 pub retry_after: Option<String>,
24 pub request_uri: Option<String>,
25}
26
27#[buildstructor::buildstructor]
28impl HttpData {
29 #[builder(visibility = "pub")]
30 #[allow(clippy::too_many_arguments)]
31 fn new(
32 content_length: Option<u64>,
33 content_type: Option<String>,
34 scheme: Option<String>,
35 host: String,
36 expires: Option<String>,
37 cache_control: Option<String>,
38 status_code: u16,
39 location: Option<String>,
40 access_control_allow_origin: Option<String>,
41 access_control_allow_credentials: Option<String>,
42 strict_transport_security: Option<String>,
43 retry_after: Option<String>,
44 received: DateTime<Utc>,
45 request_uri: Option<String>,
46 ) -> Self {
47 Self {
48 content_length,
49 content_type,
50 scheme,
51 host,
52 expires,
53 cache_control,
54 received,
55 status_code,
56 location,
57 access_control_allow_origin,
58 access_control_allow_credentials,
59 strict_transport_security,
60 retry_after,
61 request_uri,
62 }
63 }
64
65 #[builder(entry = "now", visibility = "pub")]
66 #[allow(clippy::too_many_arguments)]
67 fn new_now(
68 content_length: Option<u64>,
69 content_type: Option<String>,
70 scheme: String,
71 host: String,
72 expires: Option<String>,
73 cache_control: Option<String>,
74 status_code: Option<u16>,
75 location: Option<String>,
76 access_control_allow_origin: Option<String>,
77 access_control_allow_credentials: Option<String>,
78 strict_transport_security: Option<String>,
79 retry_after: Option<String>,
80 request_uri: Option<String>,
81 ) -> Self {
82 Self {
83 content_length,
84 content_type,
85 scheme: Some(scheme),
86 host,
87 expires,
88 cache_control,
89 received: Utc::now(),
90 status_code: status_code.unwrap_or(200),
91 location,
92 access_control_allow_origin,
93 access_control_allow_credentials,
94 strict_transport_security,
95 retry_after,
96 request_uri,
97 }
98 }
99
100 #[builder(entry = "example", visibility = "pub")]
101 #[allow(clippy::too_many_arguments)]
102 fn new_example(
103 content_length: Option<u64>,
104 content_type: Option<String>,
105 expires: Option<String>,
106 cache_control: Option<String>,
107 status_code: Option<u16>,
108 location: Option<String>,
109 access_control_allow_origin: Option<String>,
110 access_control_allow_credentials: Option<String>,
111 strict_transport_security: Option<String>,
112 retry_after: Option<String>,
113 request_uri: Option<String>,
114 ) -> Self {
115 Self {
116 content_length,
117 content_type,
118 scheme: Some("http".to_string()),
119 host: "example.com".to_string(),
120 expires,
121 cache_control,
122 received: Utc::now(),
123 status_code: status_code.unwrap_or(200),
124 location,
125 access_control_allow_origin,
126 access_control_allow_credentials,
127 strict_transport_security,
128 retry_after,
129 request_uri,
130 }
131 }
132
133 pub fn is_expired(&self, max_age: i64) -> bool {
134 let now = Utc::now();
135 if now >= self.received + Duration::seconds(max_age) {
136 return true;
137 }
138 if let Some(cache_control) = &self.cache_control {
139 let cc_max_age = cache_control
140 .split(',')
141 .map(|s| s.trim())
142 .find(|s| s.starts_with("max-age="));
143 if let Some(cc_max_age) = cc_max_age {
144 let cc_max_age = cc_max_age.trim_start_matches("max-age=").parse::<i64>();
145 if let Ok(cc_max_age) = cc_max_age {
146 return now >= self.received + Duration::seconds(cc_max_age);
147 }
148 }
149 }
150 if let Some(expires) = &self.expires {
151 let expire_time = DateTime::parse_from_rfc2822(expires);
152 return if let Ok(expire_time) = expire_time {
153 now >= expire_time
154 } else {
155 false
156 };
157 }
158 false
159 }
160
161 pub fn should_cache(&self) -> bool {
162 if let Some(cache_control) = &self.cache_control {
163 return !cache_control
164 .split(',')
165 .map(|s| s.trim())
166 .any(|s| s.eq("no-store") || s.eq("no-cache"));
167 }
168 true
169 }
170
171 pub fn from_lines(lines: &[String]) -> Result<(Self, &[String]), serde_json::Error> {
172 let count = lines.iter().take_while(|s| !s.starts_with("---")).count();
173 let cache_data = lines
174 .iter()
175 .take(count)
176 .cloned()
177 .collect::<Vec<String>>()
178 .join("");
179 let cache_data = serde_json::from_str(&cache_data)?;
180 Ok((cache_data, &lines[count + 1..]))
181 }
182
183 pub fn to_lines(&self, data: &str) -> Result<String, serde_json::Error> {
184 let mut lines = serde_json::to_string(self)?;
185 lines.push_str("\n---\n");
186 lines.push_str(data);
187 Ok(lines)
188 }
189
190 pub fn content_length(&self) -> Option<u64> {
191 self.content_length
192 }
193
194 pub fn content_type(&self) -> Option<&str> {
195 self.content_type.as_deref()
196 }
197
198 pub fn scheme(&self) -> Option<&str> {
199 self.scheme.as_deref()
200 }
201
202 pub fn host(&self) -> &str {
203 &self.host
204 }
205
206 pub fn expires(&self) -> Option<&str> {
207 self.expires.as_deref()
208 }
209
210 pub fn cache_control(&self) -> Option<&str> {
211 self.cache_control.as_deref()
212 }
213
214 pub fn received(&self) -> &DateTime<Utc> {
215 &self.received
216 }
217
218 pub fn status_code(&self) -> u16 {
219 self.status_code
220 }
221
222 pub fn location(&self) -> Option<&str> {
223 self.location.as_deref()
224 }
225
226 pub fn access_control_allow_origin(&self) -> Option<&str> {
227 self.access_control_allow_origin.as_deref()
228 }
229
230 pub fn access_control_allow_credentials(&self) -> Option<&str> {
231 self.access_control_allow_credentials.as_deref()
232 }
233
234 pub fn strict_transport_security(&self) -> Option<&str> {
235 self.strict_transport_security.as_deref()
236 }
237
238 pub fn retry_after(&self) -> Option<&str> {
239 self.retry_after.as_deref()
240 }
241
242 pub fn request_uri(&self) -> Option<&str> {
243 self.request_uri.as_deref()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use {
250 super::HttpData,
251 chrono::{Duration, Utc},
252 rstest::rstest,
253 };
254
255 #[rstest]
256 #[case(HttpData::example().cache_control("max-age=0").build(), 100, true)]
257 #[case(HttpData::example().cache_control("max-age=100").build(), 0, true)]
258 #[case(HttpData::example().cache_control("max-age=100").build(), 50, false)]
259 #[case(HttpData::example().build(), 0, true)]
260 #[case(HttpData::example().build(), 100, false)]
261 #[case(HttpData::example().expires(Utc::now().to_rfc2822()).build(), 100, true)]
262 #[case(HttpData::example().expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, false)]
263 #[case(HttpData::example().expires((Utc::now() + Duration::seconds(100)).to_rfc2822()).build(), 50, false)]
264 #[case(HttpData::example().cache_control("max-age=100").expires(Utc::now().to_rfc2822()).build(), 100, false)]
265 #[case(HttpData::example().cache_control("max-age=0").expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, true)]
266 fn test_cache_data_and_max_age_is_expired(
267 #[case] cache_data: HttpData,
268 #[case] max_age: i64,
269 #[case] expected: bool,
270 ) {
271 let actual = cache_data.is_expired(max_age);
275
276 assert_eq!(actual, expected);
278 }
279
280 #[rstest]
281 #[case(HttpData::example().cache_control("no-cache").build(), false)]
282 #[case(HttpData::example().cache_control("no-store").build(), false)]
283 #[case(HttpData::example().cache_control("max-age=40").build(), true)]
284 fn test_cache_control(#[case] cache_data: HttpData, #[case] expected: bool) {
285 let actual = cache_data.should_cache();
289
290 assert_eq!(actual, expected);
292 }
293
294 #[test]
295 fn test_data_and_data_cache_to_lines() {
296 let data = "foo";
298 let cache_data = HttpData::example().content_length(14).build();
299
300 let actual = cache_data.to_lines(data).unwrap();
302
303 let expected = format!("{}\n---\nfoo", serde_json::to_string(&cache_data).unwrap());
305 assert_eq!(actual, expected);
306 }
307
308 #[test]
309 fn test_from_lines() {
310 let data = "foo";
312 let cache_data = HttpData::example().content_length(14).build();
313 let lines = cache_data
314 .to_lines(data)
315 .unwrap()
316 .split('\n')
317 .map(|s| s.to_string())
318 .collect::<Vec<String>>();
319
320 let actual = HttpData::from_lines(&lines).expect("parsing lines");
322
323 assert_eq!(cache_data, actual.0);
325 assert_eq!(vec![data], actual.1);
326 }
327}