#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::{format, string::String, vec::Vec};
use core::{fmt, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[allow(clippy::struct_excessive_bools)]
#[non_exhaustive]
pub struct CacheControl {
pub public: bool,
pub private: bool,
pub no_cache: bool,
pub no_store: bool,
pub no_transform: bool,
pub must_revalidate: bool,
pub proxy_revalidate: bool,
pub immutable: bool,
pub max_age: Option<u64>,
pub s_maxage: Option<u64>,
pub stale_while_revalidate: Option<u64>,
pub stale_if_error: Option<u64>,
pub only_if_cached: bool,
pub max_stale: Option<u64>,
pub min_fresh: Option<u64>,
}
impl CacheControl {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn public(mut self) -> Self {
self.public = true;
self
}
#[must_use]
pub fn private(mut self) -> Self {
self.private = true;
self
}
#[must_use]
pub fn no_cache(mut self) -> Self {
self.no_cache = true;
self
}
#[must_use]
pub fn no_store(mut self) -> Self {
self.no_store = true;
self
}
#[must_use]
pub fn no_transform(mut self) -> Self {
self.no_transform = true;
self
}
#[must_use]
pub fn must_revalidate(mut self) -> Self {
self.must_revalidate = true;
self
}
#[must_use]
pub fn proxy_revalidate(mut self) -> Self {
self.proxy_revalidate = true;
self
}
#[must_use]
pub fn immutable(mut self) -> Self {
self.immutable = true;
self
}
#[must_use]
pub fn max_age(mut self, seconds: u64) -> Self {
self.max_age = Some(seconds);
self
}
#[must_use]
pub fn s_maxage(mut self, seconds: u64) -> Self {
self.s_maxage = Some(seconds);
self
}
#[must_use]
pub fn stale_while_revalidate(mut self, seconds: u64) -> Self {
self.stale_while_revalidate = Some(seconds);
self
}
#[must_use]
pub fn stale_if_error(mut self, seconds: u64) -> Self {
self.stale_if_error = Some(seconds);
self
}
#[must_use]
pub fn only_if_cached(mut self) -> Self {
self.only_if_cached = true;
self
}
#[must_use]
pub fn max_stale(mut self, seconds: u64) -> Self {
self.max_stale = Some(seconds);
self
}
#[must_use]
pub fn min_fresh(mut self, seconds: u64) -> Self {
self.min_fresh = Some(seconds);
self
}
#[must_use]
pub fn no_caching() -> Self {
Self::new().no_store()
}
#[must_use]
pub fn private_no_cache() -> Self {
Self::new().private().no_cache().no_store()
}
}
impl fmt::Display for CacheControl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts: Vec<&str> = Vec::new();
if self.public {
parts.push("public");
}
if self.private {
parts.push("private");
}
if self.no_cache {
parts.push("no-cache");
}
if self.no_store {
parts.push("no-store");
}
if self.no_transform {
parts.push("no-transform");
}
if self.must_revalidate {
parts.push("must-revalidate");
}
if self.proxy_revalidate {
parts.push("proxy-revalidate");
}
if self.immutable {
parts.push("immutable");
}
if self.only_if_cached {
parts.push("only-if-cached");
}
for (i, p) in parts.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str(p)?;
}
let numeric: [Option<(&str, u64)>; 6] = [
self.max_age.map(|v| ("max-age", v)),
self.s_maxage.map(|v| ("s-maxage", v)),
self.stale_while_revalidate
.map(|v| ("stale-while-revalidate", v)),
self.stale_if_error.map(|v| ("stale-if-error", v)),
self.max_stale.map(|v| ("max-stale", v)),
self.min_fresh.map(|v| ("min-fresh", v)),
];
let mut need_sep = !parts.is_empty();
for (name, v) in numeric.iter().flatten() {
if need_sep {
f.write_str(", ")?;
}
write!(f, "{name}={v}")?;
need_sep = true;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseCacheControlError(String);
impl fmt::Display for ParseCacheControlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid Cache-Control header: {}", self.0)
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseCacheControlError {}
impl FromStr for CacheControl {
type Err = ParseCacheControlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut cc = Self::new();
for token in s.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
let (name, value) = if let Some(eq) = token.find('=') {
(&token[..eq], Some(token[eq + 1..].trim()))
} else {
(token, None)
};
let name = name.trim().to_lowercase();
let parse_u64 = |v: Option<&str>| -> Result<u64, ParseCacheControlError> {
v.ok_or_else(|| ParseCacheControlError(format!("{name} requires a value")))?
.parse::<u64>()
.map_err(|_| {
ParseCacheControlError(format!("{name} value is not a valid integer"))
})
};
match name.as_str() {
"public" => cc.public = true,
"private" => cc.private = true,
"no-cache" => cc.no_cache = true,
"no-store" => cc.no_store = true,
"no-transform" => cc.no_transform = true,
"must-revalidate" => cc.must_revalidate = true,
"proxy-revalidate" => cc.proxy_revalidate = true,
"immutable" => cc.immutable = true,
"only-if-cached" => cc.only_if_cached = true,
"max-age" => cc.max_age = Some(parse_u64(value)?),
"s-maxage" => cc.s_maxage = Some(parse_u64(value)?),
"stale-while-revalidate" => cc.stale_while_revalidate = Some(parse_u64(value)?),
"stale-if-error" => cc.stale_if_error = Some(parse_u64(value)?),
"max-stale" => cc.max_stale = Some(value.and_then(|v| v.parse().ok()).unwrap_or(0)),
"min-fresh" => cc.min_fresh = Some(parse_u64(value)?),
_ => {} }
}
Ok(cc)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_empty() {
let cc = CacheControl::new();
assert_eq!(cc.to_string(), "");
}
#[test]
fn builder_public_max_age_immutable() {
let cc = CacheControl::new().public().max_age(31_536_000).immutable();
assert_eq!(cc.to_string(), "public, immutable, max-age=31536000");
}
#[test]
fn builder_no_store() {
let cc = CacheControl::no_caching();
assert_eq!(cc.to_string(), "no-store");
}
#[test]
fn builder_private_no_cache() {
let cc = CacheControl::private_no_cache();
assert!(cc.private);
assert!(cc.no_cache);
assert!(cc.no_store);
}
#[test]
fn parse_simple_flags() {
let cc: CacheControl = "no-store, no-cache".parse().unwrap();
assert!(cc.no_store);
assert!(cc.no_cache);
}
#[test]
fn parse_numeric_directives() {
let cc: CacheControl = "public, max-age=3600, s-maxage=7200".parse().unwrap();
assert!(cc.public);
assert_eq!(cc.max_age, Some(3600));
assert_eq!(cc.s_maxage, Some(7200));
}
#[test]
fn parse_unknown_directive_ignored() {
let cc: CacheControl = "no-store, x-custom-thing=42".parse().unwrap();
assert!(cc.no_store);
}
#[test]
fn roundtrip_complex() {
let original = CacheControl::new()
.public()
.max_age(600)
.must_revalidate()
.stale_if_error(86_400);
let s = original.to_string();
let parsed: CacheControl = s.parse().unwrap();
assert_eq!(parsed.public, original.public);
assert_eq!(parsed.max_age, original.max_age);
assert_eq!(parsed.must_revalidate, original.must_revalidate);
assert_eq!(parsed.stale_if_error, original.stale_if_error);
}
#[test]
fn parse_case_insensitive() {
let cc: CacheControl = "No-Store, Max-Age=60".parse().unwrap();
assert!(cc.no_store);
assert_eq!(cc.max_age, Some(60));
}
#[test]
fn parse_max_stale_no_value() {
let cc: CacheControl = "max-stale".parse().unwrap();
assert_eq!(cc.max_stale, Some(0));
}
#[test]
fn parse_max_stale_with_value() {
let cc: CacheControl = "max-stale=300".parse().unwrap();
assert_eq!(cc.max_stale, Some(300));
}
#[test]
fn builder_private() {
let cc = CacheControl::new().private();
assert!(cc.private);
assert_eq!(cc.to_string(), "private");
}
#[test]
fn builder_no_cache() {
let cc = CacheControl::new().no_cache();
assert!(cc.no_cache);
assert_eq!(cc.to_string(), "no-cache");
}
#[test]
fn builder_no_transform() {
let cc = CacheControl::new().no_transform();
assert!(cc.no_transform);
assert_eq!(cc.to_string(), "no-transform");
}
#[test]
fn builder_must_revalidate() {
let cc = CacheControl::new().must_revalidate();
assert!(cc.must_revalidate);
assert_eq!(cc.to_string(), "must-revalidate");
}
#[test]
fn builder_proxy_revalidate() {
let cc = CacheControl::new().proxy_revalidate();
assert!(cc.proxy_revalidate);
assert_eq!(cc.to_string(), "proxy-revalidate");
}
#[test]
fn builder_s_maxage() {
let cc = CacheControl::new().s_maxage(7200);
assert_eq!(cc.s_maxage, Some(7200));
assert_eq!(cc.to_string(), "s-maxage=7200");
}
#[test]
fn builder_stale_while_revalidate() {
let cc = CacheControl::new().stale_while_revalidate(60);
assert_eq!(cc.stale_while_revalidate, Some(60));
assert_eq!(cc.to_string(), "stale-while-revalidate=60");
}
#[test]
fn builder_stale_if_error() {
let cc = CacheControl::new().stale_if_error(86_400);
assert_eq!(cc.stale_if_error, Some(86_400));
assert_eq!(cc.to_string(), "stale-if-error=86400");
}
#[test]
fn builder_only_if_cached() {
let cc = CacheControl::new().only_if_cached();
assert!(cc.only_if_cached);
assert_eq!(cc.to_string(), "only-if-cached");
}
#[test]
fn builder_max_stale() {
let cc = CacheControl::new().max_stale(120);
assert_eq!(cc.max_stale, Some(120));
assert_eq!(cc.to_string(), "max-stale=120");
}
#[test]
fn builder_min_fresh() {
let cc = CacheControl::new().min_fresh(30);
assert_eq!(cc.min_fresh, Some(30));
assert_eq!(cc.to_string(), "min-fresh=30");
}
#[test]
fn parse_cache_control_error_display() {
let err = ParseCacheControlError("max-age requires a value".into());
let s = err.to_string();
assert!(s.contains("invalid Cache-Control header"));
assert!(s.contains("max-age requires a value"));
}
#[test]
fn parse_numeric_missing_value_is_error() {
let result = "max-age".parse::<CacheControl>();
assert!(result.is_err());
}
#[test]
fn parse_numeric_bad_integer_is_error() {
let result = "max-age=abc".parse::<CacheControl>();
assert!(result.is_err());
}
#[test]
fn parse_all_boolean_directives() {
let cc: CacheControl = "public, private, no-cache, no-store, no-transform, must-revalidate, proxy-revalidate, immutable, only-if-cached"
.parse()
.unwrap();
assert!(cc.public);
assert!(cc.private);
assert!(cc.no_cache);
assert!(cc.no_store);
assert!(cc.no_transform);
assert!(cc.must_revalidate);
assert!(cc.proxy_revalidate);
assert!(cc.immutable);
assert!(cc.only_if_cached);
}
#[test]
fn parse_all_numeric_directives() {
let cc: CacheControl =
"max-age=10, s-maxage=20, stale-while-revalidate=30, stale-if-error=40, max-stale=50, min-fresh=60"
.parse()
.unwrap();
assert_eq!(cc.max_age, Some(10));
assert_eq!(cc.s_maxage, Some(20));
assert_eq!(cc.stale_while_revalidate, Some(30));
assert_eq!(cc.stale_if_error, Some(40));
assert_eq!(cc.max_stale, Some(50));
assert_eq!(cc.min_fresh, Some(60));
}
#[test]
fn display_mixed_boolean_and_numeric_with_only_if_cached() {
let cc = CacheControl::new()
.only_if_cached()
.max_stale(0)
.min_fresh(10);
let s = cc.to_string();
assert!(s.contains("only-if-cached"));
assert!(s.contains("max-stale=0"));
assert!(s.contains("min-fresh=10"));
}
}