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<bool>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[serde(default)]
83    pub stale_if_error: Option<bool>,
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            && stale_while_revalidate
121        {
122            write!(cache_control, "stale-while-revalidate, ")?;
123        }
124        if let Some(private) = self.private
125            && private
126        {
127            write!(cache_control, "private, ")?;
128        }
129        if let Some(public) = self.public
130            && public
131        {
132            write!(cache_control, "public, ")?;
133        }
134        if let Some(immutable) = self.immutable
135            && immutable
136        {
137            write!(cache_control, "immutable, ")?;
138        }
139        if let Some(stale_if_error) = self.stale_if_error
140            && stale_if_error
141        {
142            write!(cache_control, "stale-if-error, ")?;
143        }
144
145        if cache_control.is_empty() {
146            cache_control.push_str(default);
147        } else {
148            cache_control.pop();
149            cache_control.pop();
150        }
151
152        Ok(())
153    }
154}
155
156#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
157#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
158#[derive(Deserialize, Serialize, Debug, Clone, Default)]
159pub struct HttpCache {
160    #[serde(skip_serializing_if = "Option::is_none")]
161    #[serde(default)]
162    pub cache_control: Option<HttpCacheControl>,
163
164    /// Corresponds to the HTTP Expires header.
165    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Expires>
166    ///
167    /// Unit: seconds.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    #[serde(default)]
170    pub expires: Option<u64>,
171
172    #[serde(skip_serializing_if = "Option::is_none")]
173    #[serde(default)]
174    pub stored: Option<StoredCache>,
175
176    #[serde(skip_serializing_if = "Option::is_none")]
177    #[serde(default)]
178    pub etag: Option<HttpEtag>,
179}