1#[cfg(all(not(feature = "std"), feature = "alloc"))]
26use alloc::{format, string::String, vec::Vec};
27use core::{fmt, str::FromStr};
28#[cfg(feature = "serde")]
29use serde::{Deserialize, Serialize};
30
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
42#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43#[allow(clippy::struct_excessive_bools)]
44#[non_exhaustive]
45pub struct CacheControl {
46 pub public: bool,
51 pub private: bool,
54 pub no_cache: bool,
56 pub no_store: bool,
58 pub no_transform: bool,
60 pub must_revalidate: bool,
62 pub proxy_revalidate: bool,
64 pub immutable: bool,
66 pub max_age: Option<u64>,
68 pub s_maxage: Option<u64>,
70 pub stale_while_revalidate: Option<u64>,
72 pub stale_if_error: Option<u64>,
74
75 pub only_if_cached: bool,
80 pub max_stale: Option<u64>,
84 pub min_fresh: Option<u64>,
86}
87
88impl CacheControl {
89 #[must_use]
91 pub fn new() -> Self {
92 Self::default()
93 }
94
95 #[must_use]
101 pub fn public(mut self) -> Self {
102 self.public = true;
103 self
104 }
105
106 #[must_use]
108 pub fn private(mut self) -> Self {
109 self.private = true;
110 self
111 }
112
113 #[must_use]
115 pub fn no_cache(mut self) -> Self {
116 self.no_cache = true;
117 self
118 }
119
120 #[must_use]
122 pub fn no_store(mut self) -> Self {
123 self.no_store = true;
124 self
125 }
126
127 #[must_use]
129 pub fn no_transform(mut self) -> Self {
130 self.no_transform = true;
131 self
132 }
133
134 #[must_use]
136 pub fn must_revalidate(mut self) -> Self {
137 self.must_revalidate = true;
138 self
139 }
140
141 #[must_use]
143 pub fn proxy_revalidate(mut self) -> Self {
144 self.proxy_revalidate = true;
145 self
146 }
147
148 #[must_use]
150 pub fn immutable(mut self) -> Self {
151 self.immutable = true;
152 self
153 }
154
155 #[must_use]
157 pub fn max_age(mut self, seconds: u64) -> Self {
158 self.max_age = Some(seconds);
159 self
160 }
161
162 #[must_use]
164 pub fn s_maxage(mut self, seconds: u64) -> Self {
165 self.s_maxage = Some(seconds);
166 self
167 }
168
169 #[must_use]
171 pub fn stale_while_revalidate(mut self, seconds: u64) -> Self {
172 self.stale_while_revalidate = Some(seconds);
173 self
174 }
175
176 #[must_use]
178 pub fn stale_if_error(mut self, seconds: u64) -> Self {
179 self.stale_if_error = Some(seconds);
180 self
181 }
182
183 #[must_use]
189 pub fn only_if_cached(mut self) -> Self {
190 self.only_if_cached = true;
191 self
192 }
193
194 #[must_use]
196 pub fn max_stale(mut self, seconds: u64) -> Self {
197 self.max_stale = Some(seconds);
198 self
199 }
200
201 #[must_use]
203 pub fn min_fresh(mut self, seconds: u64) -> Self {
204 self.min_fresh = Some(seconds);
205 self
206 }
207
208 #[must_use]
222 pub fn no_caching() -> Self {
223 Self::new().no_store()
224 }
225
226 #[must_use]
235 pub fn private_no_cache() -> Self {
236 Self::new().private().no_cache().no_store()
237 }
238}
239
240impl fmt::Display for CacheControl {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 let mut parts: Vec<&str> = Vec::new();
247 if self.public {
252 parts.push("public");
253 }
254 if self.private {
255 parts.push("private");
256 }
257 if self.no_cache {
258 parts.push("no-cache");
259 }
260 if self.no_store {
261 parts.push("no-store");
262 }
263 if self.no_transform {
264 parts.push("no-transform");
265 }
266 if self.must_revalidate {
267 parts.push("must-revalidate");
268 }
269 if self.proxy_revalidate {
270 parts.push("proxy-revalidate");
271 }
272 if self.immutable {
273 parts.push("immutable");
274 }
275 if self.only_if_cached {
277 parts.push("only-if-cached");
278 }
279
280 for (i, p) in parts.iter().enumerate() {
282 if i > 0 {
283 f.write_str(", ")?;
284 }
285 f.write_str(p)?;
286 }
287
288 let numeric: [Option<(&str, u64)>; 6] = [
290 self.max_age.map(|v| ("max-age", v)),
291 self.s_maxage.map(|v| ("s-maxage", v)),
292 self.stale_while_revalidate
293 .map(|v| ("stale-while-revalidate", v)),
294 self.stale_if_error.map(|v| ("stale-if-error", v)),
295 self.max_stale.map(|v| ("max-stale", v)),
296 self.min_fresh.map(|v| ("min-fresh", v)),
297 ];
298
299 let mut need_sep = !parts.is_empty();
300 for (name, v) in numeric.iter().flatten() {
301 if need_sep {
302 f.write_str(", ")?;
303 }
304 write!(f, "{name}={v}")?;
305 need_sep = true;
306 }
307
308 Ok(())
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct ParseCacheControlError(String);
319
320impl fmt::Display for ParseCacheControlError {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 write!(f, "invalid Cache-Control header: {}", self.0)
323 }
324}
325
326#[cfg(feature = "std")]
327impl std::error::Error for ParseCacheControlError {}
328
329impl FromStr for CacheControl {
334 type Err = ParseCacheControlError;
335
336 fn from_str(s: &str) -> Result<Self, Self::Err> {
350 let mut cc = Self::new();
351 for token in s.split(',') {
352 let token = token.trim();
353 if token.is_empty() {
354 continue;
355 }
356 let (name, value) = if let Some(eq) = token.find('=') {
357 (&token[..eq], Some(token[eq + 1..].trim()))
358 } else {
359 (token, None)
360 };
361 let name = name.trim().to_lowercase();
362
363 let parse_u64 = |v: Option<&str>| -> Result<u64, ParseCacheControlError> {
364 v.ok_or_else(|| ParseCacheControlError(format!("{name} requires a value")))?
365 .parse::<u64>()
366 .map_err(|_| {
367 ParseCacheControlError(format!("{name} value is not a valid integer"))
368 })
369 };
370
371 match name.as_str() {
372 "public" => cc.public = true,
373 "private" => cc.private = true,
374 "no-cache" => cc.no_cache = true,
375 "no-store" => cc.no_store = true,
376 "no-transform" => cc.no_transform = true,
377 "must-revalidate" => cc.must_revalidate = true,
378 "proxy-revalidate" => cc.proxy_revalidate = true,
379 "immutable" => cc.immutable = true,
380 "only-if-cached" => cc.only_if_cached = true,
381 "max-age" => cc.max_age = Some(parse_u64(value)?),
382 "s-maxage" => cc.s_maxage = Some(parse_u64(value)?),
383 "stale-while-revalidate" => cc.stale_while_revalidate = Some(parse_u64(value)?),
384 "stale-if-error" => cc.stale_if_error = Some(parse_u64(value)?),
385 "max-stale" => cc.max_stale = Some(value.and_then(|v| v.parse().ok()).unwrap_or(0)),
386 "min-fresh" => cc.min_fresh = Some(parse_u64(value)?),
387 _ => {} }
389 }
390 Ok(cc)
391 }
392}
393
394#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn default_is_empty() {
404 let cc = CacheControl::new();
405 assert_eq!(cc.to_string(), "");
406 }
407
408 #[test]
409 fn builder_public_max_age_immutable() {
410 let cc = CacheControl::new().public().max_age(31_536_000).immutable();
411 assert_eq!(cc.to_string(), "public, immutable, max-age=31536000");
413 }
414
415 #[test]
416 fn builder_no_store() {
417 let cc = CacheControl::no_caching();
418 assert_eq!(cc.to_string(), "no-store");
419 }
420
421 #[test]
422 fn builder_private_no_cache() {
423 let cc = CacheControl::private_no_cache();
424 assert!(cc.private);
425 assert!(cc.no_cache);
426 assert!(cc.no_store);
427 }
428
429 #[test]
430 fn parse_simple_flags() {
431 let cc: CacheControl = "no-store, no-cache".parse().unwrap();
432 assert!(cc.no_store);
433 assert!(cc.no_cache);
434 }
435
436 #[test]
437 fn parse_numeric_directives() {
438 let cc: CacheControl = "public, max-age=3600, s-maxage=7200".parse().unwrap();
439 assert!(cc.public);
440 assert_eq!(cc.max_age, Some(3600));
441 assert_eq!(cc.s_maxage, Some(7200));
442 }
443
444 #[test]
445 fn parse_unknown_directive_ignored() {
446 let cc: CacheControl = "no-store, x-custom-thing=42".parse().unwrap();
447 assert!(cc.no_store);
448 }
449
450 #[test]
451 fn roundtrip_complex() {
452 let original = CacheControl::new()
453 .public()
454 .max_age(600)
455 .must_revalidate()
456 .stale_if_error(86_400);
457 let s = original.to_string();
458 let parsed: CacheControl = s.parse().unwrap();
459 assert_eq!(parsed.public, original.public);
460 assert_eq!(parsed.max_age, original.max_age);
461 assert_eq!(parsed.must_revalidate, original.must_revalidate);
462 assert_eq!(parsed.stale_if_error, original.stale_if_error);
463 }
464
465 #[test]
466 fn parse_case_insensitive() {
467 let cc: CacheControl = "No-Store, Max-Age=60".parse().unwrap();
468 assert!(cc.no_store);
469 assert_eq!(cc.max_age, Some(60));
470 }
471
472 #[test]
473 fn parse_max_stale_no_value() {
474 let cc: CacheControl = "max-stale".parse().unwrap();
475 assert_eq!(cc.max_stale, Some(0));
476 }
477
478 #[test]
479 fn parse_max_stale_with_value() {
480 let cc: CacheControl = "max-stale=300".parse().unwrap();
481 assert_eq!(cc.max_stale, Some(300));
482 }
483
484 #[test]
485 fn builder_private() {
486 let cc = CacheControl::new().private();
487 assert!(cc.private);
488 assert_eq!(cc.to_string(), "private");
489 }
490
491 #[test]
492 fn builder_no_cache() {
493 let cc = CacheControl::new().no_cache();
494 assert!(cc.no_cache);
495 assert_eq!(cc.to_string(), "no-cache");
496 }
497
498 #[test]
499 fn builder_no_transform() {
500 let cc = CacheControl::new().no_transform();
501 assert!(cc.no_transform);
502 assert_eq!(cc.to_string(), "no-transform");
503 }
504
505 #[test]
506 fn builder_must_revalidate() {
507 let cc = CacheControl::new().must_revalidate();
508 assert!(cc.must_revalidate);
509 assert_eq!(cc.to_string(), "must-revalidate");
510 }
511
512 #[test]
513 fn builder_proxy_revalidate() {
514 let cc = CacheControl::new().proxy_revalidate();
515 assert!(cc.proxy_revalidate);
516 assert_eq!(cc.to_string(), "proxy-revalidate");
517 }
518
519 #[test]
520 fn builder_s_maxage() {
521 let cc = CacheControl::new().s_maxage(7200);
522 assert_eq!(cc.s_maxage, Some(7200));
523 assert_eq!(cc.to_string(), "s-maxage=7200");
524 }
525
526 #[test]
527 fn builder_stale_while_revalidate() {
528 let cc = CacheControl::new().stale_while_revalidate(60);
529 assert_eq!(cc.stale_while_revalidate, Some(60));
530 assert_eq!(cc.to_string(), "stale-while-revalidate=60");
531 }
532
533 #[test]
534 fn builder_stale_if_error() {
535 let cc = CacheControl::new().stale_if_error(86_400);
536 assert_eq!(cc.stale_if_error, Some(86_400));
537 assert_eq!(cc.to_string(), "stale-if-error=86400");
538 }
539
540 #[test]
541 fn builder_only_if_cached() {
542 let cc = CacheControl::new().only_if_cached();
543 assert!(cc.only_if_cached);
544 assert_eq!(cc.to_string(), "only-if-cached");
545 }
546
547 #[test]
548 fn builder_max_stale() {
549 let cc = CacheControl::new().max_stale(120);
550 assert_eq!(cc.max_stale, Some(120));
551 assert_eq!(cc.to_string(), "max-stale=120");
552 }
553
554 #[test]
555 fn builder_min_fresh() {
556 let cc = CacheControl::new().min_fresh(30);
557 assert_eq!(cc.min_fresh, Some(30));
558 assert_eq!(cc.to_string(), "min-fresh=30");
559 }
560
561 #[test]
562 fn parse_cache_control_error_display() {
563 let err = ParseCacheControlError("max-age requires a value".into());
564 let s = err.to_string();
565 assert!(s.contains("invalid Cache-Control header"));
566 assert!(s.contains("max-age requires a value"));
567 }
568
569 #[test]
570 fn parse_numeric_missing_value_is_error() {
571 let result = "max-age".parse::<CacheControl>();
573 assert!(result.is_err());
574 }
575
576 #[test]
577 fn parse_numeric_bad_integer_is_error() {
578 let result = "max-age=abc".parse::<CacheControl>();
579 assert!(result.is_err());
580 }
581
582 #[test]
583 fn parse_all_boolean_directives() {
584 let cc: CacheControl = "public, private, no-cache, no-store, no-transform, must-revalidate, proxy-revalidate, immutable, only-if-cached"
585 .parse()
586 .unwrap();
587 assert!(cc.public);
588 assert!(cc.private);
589 assert!(cc.no_cache);
590 assert!(cc.no_store);
591 assert!(cc.no_transform);
592 assert!(cc.must_revalidate);
593 assert!(cc.proxy_revalidate);
594 assert!(cc.immutable);
595 assert!(cc.only_if_cached);
596 }
597
598 #[test]
599 fn parse_all_numeric_directives() {
600 let cc: CacheControl =
601 "max-age=10, s-maxage=20, stale-while-revalidate=30, stale-if-error=40, max-stale=50, min-fresh=60"
602 .parse()
603 .unwrap();
604 assert_eq!(cc.max_age, Some(10));
605 assert_eq!(cc.s_maxage, Some(20));
606 assert_eq!(cc.stale_while_revalidate, Some(30));
607 assert_eq!(cc.stale_if_error, Some(40));
608 assert_eq!(cc.max_stale, Some(50));
609 assert_eq!(cc.min_fresh, Some(60));
610 }
611
612 #[test]
613 fn display_mixed_boolean_and_numeric_with_only_if_cached() {
614 let cc = CacheControl::new()
615 .only_if_cached()
616 .max_stale(0)
617 .min_fresh(10);
618 let s = cc.to_string();
619 assert!(s.contains("only-if-cached"));
620 assert!(s.contains("max-stale=0"));
621 assert!(s.contains("min-fresh=10"));
622 }
623}