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