Skip to content

Commit 8a4d158

Browse files
feat: Fallible CachePolicy construction
closes #14
1 parent 8124846 commit 8a4d158

File tree

10 files changed

+486
-410
lines changed

10 files changed

+486
-410
lines changed

src/lib.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ impl Default for CacheOptions {
147147
}
148148
}
149149

150+
/// A [`CachePolicy`] that MUST NOT have either the request or response stored.
151+
///
152+
/// Policies returned as `NotStorable` are policies where [`CachePolicy::is_storable()`] returns
153+
/// `true`.
154+
#[derive(Debug, Clone)]
155+
pub struct NotStorable(pub CachePolicy);
156+
150157
/// Identifies when responses can be reused from a cache, taking into account
151158
/// HTTP RFC 7234 rules for user agents and shared caches. It's aware of many
152159
/// tricky details such as the Vary header, proxy revalidation, and
@@ -173,6 +180,72 @@ pub struct CachePolicy {
173180
impl CachePolicy {
174181
/// Cacheability of an HTTP response depends on how it was requested, so
175182
/// both request and response are required to create the policy.
183+
///
184+
/// This constructor returns a [`Result`] to indicate if the cache policy is considered
185+
/// storable. In the common case of disregarding these policies you can just ignore the
186+
/// `Err(_)` case like so
187+
///
188+
/// ```
189+
/// # use http_cache_semantics::CachePolicy;
190+
/// # let req = http::Request::<()>::default();
191+
/// # let res = http::Response::<()>::default();
192+
/// # let mut cache = std::collections::HashMap::new();
193+
/// # let url = ();
194+
/// if let Ok(policy) = CachePolicy::try_new(&req, &res) {
195+
/// cache.insert(url, policy);
196+
/// }
197+
/// ```
198+
///
199+
/// and for the uncommon case of wanting to inspect cache policies regardless of whether or not
200+
/// they should be stored then you can easily merge the `Ok(_)` and `Err(_)` cases together to
201+
/// treat them the same
202+
///
203+
/// ```
204+
/// # use http_cache_semantics::CachePolicy;
205+
/// # let req = http::Request::<()>::default();
206+
/// # let res = http::Response::<()>::default();
207+
/// # let mut cache = std::collections::HashMap::new();
208+
/// # let url = ();
209+
/// let policy = CachePolicy::try_new(&req, &res).unwrap_or_else(|err| err.0);
210+
/// // ... do something with `policy`
211+
/// if policy.is_storable() {
212+
/// cache.insert(url, policy);
213+
/// }
214+
/// ```
215+
#[inline]
216+
pub fn try_new<Req: RequestLike, Res: ResponseLike>(
217+
req: &Req,
218+
res: &Res,
219+
) -> Result<Self, NotStorable> {
220+
Self::try_new_with_options(req, res, SystemTime::now(), Default::default())
221+
}
222+
223+
/// Caching with customized behavior. See [`CacheOptions`] for details.
224+
///
225+
/// `response_time` is a timestamp when the response has been received, usually `SystemTime::now()`.
226+
#[inline]
227+
pub fn try_new_with_options<Req: RequestLike, Res: ResponseLike>(
228+
req: &Req,
229+
res: &Res,
230+
response_time: SystemTime,
231+
opts: CacheOptions,
232+
) -> Result<Self, NotStorable> {
233+
let uri = req.uri();
234+
let status = res.status();
235+
let method = req.method().clone();
236+
let res = res.headers().clone();
237+
let req = req.headers().clone();
238+
let policy = Self::from_details(uri, method, status, req, res, response_time, opts);
239+
if policy.is_storable() {
240+
Ok(policy)
241+
} else {
242+
Err(NotStorable(policy))
243+
}
244+
}
245+
246+
/// Cacheability of an HTTP response depends on how it was requested, so
247+
/// both request and response are required to create the policy.
248+
#[deprecated(note = "replaced with `CachePolicy::try_new`")]
176249
#[inline]
177250
pub fn new<Req: RequestLike, Res: ResponseLike>(req: &Req, res: &Res) -> Self {
178251
let uri = req.uri();
@@ -194,6 +267,7 @@ impl CachePolicy {
194267
/// Caching with customized behavior. See `CacheOptions` for details.
195268
///
196269
/// `response_time` is a timestamp when the response has been received, usually `SystemTime::now()`.
270+
#[deprecated(note = "replaced with `CachePolicy::try_new_with_options`")]
197271
#[inline]
198272
pub fn new_options<Req: RequestLike, Res: ResponseLike>(
199273
req: &Req,

tests/okhttp.rs

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ fn assert_cached(should_put: bool, response_code: u16) {
4242

4343
let request = request_parts(Request::get("/"));
4444

45-
let policy = CachePolicy::new_options(&request, &response, now, options);
45+
let policy = CachePolicy::try_new_with_options(&request, &response, now, options)
46+
.unwrap_or_else(|n_s| n_s.0);
4647

4748
assert_eq!(should_put, policy.is_storable());
4849
}
@@ -106,7 +107,7 @@ fn test_default_expiration_date_fully_cached_for_less_than_24_hours() {
106107
..Default::default()
107108
};
108109

109-
let policy = CachePolicy::new_options(
110+
let policy = CachePolicy::try_new_with_options(
110111
&request_parts(Request::get("/")),
111112
&response_parts(
112113
Response::builder()
@@ -115,7 +116,7 @@ fn test_default_expiration_date_fully_cached_for_less_than_24_hours() {
115116
),
116117
now,
117118
options,
118-
);
119+
).unwrap();
119120

120121
assert!(policy.time_to_live(now).as_millis() > 4000);
121122
}
@@ -128,7 +129,7 @@ fn test_default_expiration_date_fully_cached_for_more_than_24_hours() {
128129
..Default::default()
129130
};
130131

131-
let policy = CachePolicy::new_options(
132+
let policy = CachePolicy::try_new_with_options(
132133
&request_parts(Request::get("/")),
133134
&response_parts(
134135
Response::builder()
@@ -137,7 +138,7 @@ fn test_default_expiration_date_fully_cached_for_more_than_24_hours() {
137138
),
138139
now,
139140
options,
140-
);
141+
).unwrap();
141142

142143
assert!((policy.time_to_live(now) + policy.age(now)).as_secs() >= 10 * 3600 * 24);
143144
assert!(policy.time_to_live(now).as_millis() + 1000 >= 5 * 3600 * 24);
@@ -159,7 +160,7 @@ fn test_max_age_in_the_past_with_date_header_but_no_last_modified_header() {
159160
.header(header::AGE, 120)
160161
.header(header::CACHE_CONTROL, "max-age=60"),
161162
);
162-
let policy = CachePolicy::new_options(&request, &response, now, options);
163+
let policy = CachePolicy::try_new_with_options(&request, &response, now, options).unwrap();
163164

164165
assert!(policy.is_stale(now));
165166
}
@@ -172,7 +173,7 @@ fn test_max_age_preferred_over_lower_shared_max_age() {
172173
..Default::default()
173174
};
174175

175-
let policy = CachePolicy::new_options(
176+
let policy = CachePolicy::try_new_with_options(
176177
&request_parts(Request::builder()),
177178
&response_parts(
178179
Response::builder()
@@ -181,7 +182,7 @@ fn test_max_age_preferred_over_lower_shared_max_age() {
181182
),
182183
now,
183184
options,
184-
);
185+
).unwrap();
185186

186187
assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 180);
187188
}
@@ -200,7 +201,7 @@ fn test_max_age_preferred_over_higher_max_age() {
200201
.header(header::AGE, 3 * 60)
201202
.header(header::CACHE_CONTROL, "s-maxage=60, max-age=180"),
202203
);
203-
let policy = CachePolicy::new_options(&request, &response, now, options);
204+
let policy = CachePolicy::try_new_with_options(&request, &response, now, options).unwrap();
204205

205206
assert!(policy.is_stale(now));
206207
}
@@ -219,7 +220,8 @@ fn request_method_not_cached(method: &str) {
219220
let response =
220221
response_parts(Response::builder().header(header::EXPIRES, format_date(1, 3600)));
221222

222-
let policy = CachePolicy::new_options(&request, &response, now, options);
223+
let not_storable = CachePolicy::try_new_with_options(&request, &response, now, options).unwrap_err();
224+
let policy = not_storable.0;
223225

224226
assert!(policy.is_stale(now));
225227
}
@@ -252,7 +254,7 @@ fn test_etag_and_expiration_date_in_the_future() {
252254
..Default::default()
253255
};
254256

255-
let policy = CachePolicy::new_options(
257+
let policy = CachePolicy::try_new_with_options(
256258
&request_parts(Request::builder()),
257259
&response_parts(
258260
Response::builder()
@@ -262,7 +264,7 @@ fn test_etag_and_expiration_date_in_the_future() {
262264
),
263265
now,
264266
options,
265-
);
267+
).unwrap();
266268

267269
assert!(policy.time_to_live(now).as_millis() > 0);
268270
}
@@ -275,14 +277,12 @@ fn test_client_side_no_store() {
275277
..Default::default()
276278
};
277279

278-
let policy = CachePolicy::new_options(
280+
CachePolicy::try_new_with_options(
279281
&request_parts(Request::builder().header(header::CACHE_CONTROL, "no-store")),
280282
&response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=60")),
281283
now,
282284
options,
283-
);
284-
285-
assert!(!policy.is_storable());
285+
).unwrap_err();
286286
}
287287

288288
#[test]
@@ -297,15 +297,15 @@ fn test_request_max_age() {
297297
.header(header::EXPIRES, format_date(1, 3600)),
298298
);
299299

300-
let policy = CachePolicy::new_options(
300+
let policy = CachePolicy::try_new_with_options(
301301
&first_request,
302302
&response,
303303
now,
304304
CacheOptions {
305305
shared: false,
306306
..Default::default()
307307
},
308-
);
308+
).unwrap();
309309

310310
assert_eq!(policy.age(now).as_secs(), 60);
311311
assert_eq!(policy.time_to_live(now).as_secs(), 3000);
@@ -335,7 +335,7 @@ fn test_request_min_fresh() {
335335
let response = response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=60"));
336336

337337
let policy =
338-
CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options);
338+
CachePolicy::try_new_with_options(&request_parts(Request::builder()), &response, now, options).unwrap();
339339

340340
assert!(!policy.is_stale(now));
341341

@@ -368,8 +368,12 @@ fn test_request_max_stale() {
368368
.header(header::AGE, 4 * 60),
369369
);
370370

371-
let policy =
372-
CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options);
371+
let policy = CachePolicy::try_new_with_options(
372+
&request_parts(Request::builder()),
373+
&response,
374+
now,
375+
options,
376+
).unwrap();
373377

374378
assert!(policy.is_stale(now));
375379

@@ -410,8 +414,12 @@ fn test_request_max_stale_not_honored_with_must_revalidate() {
410414
.header(header::AGE, 4 * 60),
411415
);
412416

413-
let policy =
414-
CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options);
417+
let policy = CachePolicy::try_new_with_options(
418+
&request_parts(Request::builder()),
419+
&response,
420+
now,
421+
options,
422+
).unwrap();
415423

416424
assert!(policy.is_stale(now));
417425

@@ -433,14 +441,14 @@ fn test_request_max_stale_not_honored_with_must_revalidate() {
433441
#[test]
434442
fn test_get_headers_deletes_cached_100_level_warnings() {
435443
let now = SystemTime::now();
436-
let policy = CachePolicy::new(
444+
let policy = CachePolicy::try_new(
437445
&request_parts(Request::builder().header("cache-control", "max-stale")),
438446
&response_parts(
439447
Response::builder()
440448
.header("cache-control", "immutable")
441449
.header(header::WARNING, "199 test danger, 200 ok ok"),
442450
),
443-
);
451+
).unwrap();
444452

445453
assert_eq!(
446454
"200 ok ok",
@@ -451,15 +459,16 @@ fn test_get_headers_deletes_cached_100_level_warnings() {
451459

452460
#[test]
453461
fn test_do_not_cache_partial_response() {
454-
let policy = CachePolicy::new(
462+
let not_storable = CachePolicy::try_new(
455463
&request_parts(Request::builder()),
456464
&response_parts(
457465
Response::builder()
458466
.status(206)
459467
.header(header::CONTENT_RANGE, "bytes 100-100/200")
460468
.header(header::CACHE_CONTROL, "max-age=60"),
461469
),
462-
);
470+
).unwrap_err();
471+
let policy = not_storable.0;
463472

464473
assert!(!policy.is_storable());
465474
}

0 commit comments

Comments
 (0)