Skip to main content

ordinary_config/http/
cache.rs

1// Copyright (C) 2026 The Ordinary Authors.
2//
3// SPDX-License-Identifier: BSD-3-Clause
4
5use crate::StoredCache;
6use serde::{Deserialize, Serialize};
7use std::fmt::Write;
8
9#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
10#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
11#[derive(Deserialize, Serialize, Debug, Clone)]
12pub enum XXH3Variation {
13    Bit64,
14    Bit128,
15}
16
17/// Hashing algorithm options for generating etags.
18///
19/// `AHash` is the default if none is selected.
20#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
21#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
22#[derive(Deserialize, Serialize, Debug, Clone)]
23pub enum HttpEtagAlgorithm {
24    AHash,
25    XXH3(XXH3Variation),
26    Rustc,
27    Blake3,
28    Foldhash,
29}
30
31/// HTTP Cache-Control.
32/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control>
33#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
34#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
35#[derive(Deserialize, Serialize, Debug, Clone)]
36pub struct HttpEtag {
37    #[serde(skip_serializing_if = "Option::is_none")]
38    #[serde(default)]
39    pub alg: Option<HttpEtagAlgorithm>,
40}
41
42/// HTTP Cache-Control.
43/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control>
44#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
45#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
46#[derive(Deserialize, Serialize, Debug, Clone, Default)]
47pub struct HttpCacheControl {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    #[serde(default)]
50    pub max_age: Option<usize>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    #[serde(default)]
53    pub s_maxage: Option<usize>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[serde(default)]
56    pub no_cache: Option<bool>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    #[serde(default)]
59    pub no_store: Option<bool>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    #[serde(default)]
62    pub no_transform: Option<bool>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[serde(default)]
65    pub must_revalidate: Option<bool>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    #[serde(default)]
68    pub proxy_revalidate: Option<bool>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    #[serde(default)]
71    pub private: Option<bool>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    #[serde(default)]
74    pub public: Option<bool>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    #[serde(default)]
77    pub immutable: Option<bool>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    #[serde(default)]
80    pub stale_while_revalidate: Option<usize>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[serde(default)]
83    pub stale_if_error: Option<usize>,
84}
85
86impl HttpCacheControl {
87    pub fn header_value(&self, cache_control: &mut String, default: &str) -> anyhow::Result<()> {
88        if let Some(max_age) = self.max_age {
89            write!(cache_control, "max-age={max_age}, ")?;
90        }
91        if let Some(s_maxage) = self.s_maxage {
92            write!(cache_control, "s-maxage={s_maxage}, ")?;
93        }
94        if let Some(no_cache) = self.no_cache
95            && no_cache
96        {
97            write!(cache_control, "no-cache, ")?;
98        }
99        if let Some(no_store) = self.no_store
100            && no_store
101        {
102            write!(cache_control, "no-store, ")?;
103        }
104        if let Some(no_transform) = self.no_transform
105            && no_transform
106        {
107            write!(cache_control, "no-transform, ")?;
108        }
109        if let Some(must_revalidate) = self.must_revalidate
110            && must_revalidate
111        {
112            write!(cache_control, "must-revalidate, ")?;
113        }
114        if let Some(proxy_revalidate) = self.proxy_revalidate
115            && proxy_revalidate
116        {
117            write!(cache_control, "proxy-revalidate, ")?;
118        }
119        if let Some(stale_while_revalidate) = self.stale_while_revalidate {
120            write!(
121                cache_control,
122                "stale-while-revalidate={stale_while_revalidate}, "
123            )?;
124        }
125        if let Some(private) = self.private
126            && private
127        {
128            write!(cache_control, "private, ")?;
129        }
130        if let Some(public) = self.public
131            && public
132        {
133            write!(cache_control, "public, ")?;
134        }
135        if let Some(immutable) = self.immutable
136            && immutable
137        {
138            write!(cache_control, "immutable, ")?;
139        }
140        if let Some(stale_if_error) = self.stale_if_error {
141            write!(cache_control, "stale-if-error={stale_if_error}, ")?;
142        }
143
144        if cache_control.is_empty() {
145            cache_control.push_str(default);
146        } else {
147            cache_control.pop();
148            cache_control.pop();
149        }
150
151        Ok(())
152    }
153}
154
155#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
156#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
157#[derive(Deserialize, Serialize, Debug, Clone, Default)]
158pub struct HttpCache {
159    #[serde(skip_serializing_if = "Option::is_none")]
160    #[serde(default)]
161    pub cache_control: Option<HttpCacheControl>,
162
163    /// Corresponds to the HTTP Expires header.
164    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Expires>
165    ///
166    /// Unit: seconds.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    #[serde(default)]
169    pub expires: Option<u64>,
170
171    #[serde(skip_serializing_if = "Option::is_none")]
172    #[serde(default)]
173    pub stored: Option<StoredCache>,
174
175    #[serde(skip_serializing_if = "Option::is_none")]
176    #[serde(default)]
177    pub etag: Option<HttpEtag>,
178}