Skip to main content

ordinary_config/app/cache/
mod.rs

1// Copyright (C) 2026 The Ordinary Authors.
2//
3// SPDX-License-Identifier: BSD-3-Clause
4
5use crate::CompressionAlgorithm;
6use arrayvec::ArrayVec;
7use serde::{Deserialize, Serialize};
8use smallvec::SmallVec;
9
10#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
11#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
12#[derive(Deserialize, Serialize, Debug, Clone)]
13pub enum StoredCachePolicy {
14    /// No eviction on clean or write. Constrained only by overall storage limit or `max_size`
15    /// (if set). Will only be evicted if dependencies change and `evict_on_dependency_change`
16    /// is set.
17    Permanent,
18    /// uses the [`quick_cache`](https://crates.io/crates/quick_cache) library
19    QuickCache,
20}
21
22/// Render caching policy.
23///
24/// IMPORTANT: Very experimental, may not work as described. `policy: Permanent` is currently
25/// the most likely to behave correctly.
26#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
27#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
28#[derive(Deserialize, Serialize, Debug, Clone)]
29pub struct StoredCache {
30    pub policy: StoredCachePolicy,
31
32    /// Which compression results should be stored.
33    ///
34    /// if empty or not provided, only `Uncompressed` is cached.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    #[serde(default)]
37    pub compression: Option<CompressionAlgorithms>,
38
39    #[serde(skip)]
40    #[serde(default)]
41    pub internal_compressions: Option<ArrayVec<CompressionAlgorithm, 5>>,
42
43    /// Which data formats should be stored
44    ///
45    /// If none is supplied, defaults are `text/html` and `application/json`
46    #[serde(skip_serializing_if = "Option::is_none")]
47    #[serde(default)]
48    pub content_types: Option<SmallVec<[String; 2]>>,
49
50    #[serde(skip)]
51    #[serde(default)]
52    pub internal_content_types: Option<SmallVec<[String; 2]>>,
53
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[serde(default)]
56    pub key_on: Option<KeyOn>,
57
58    /// Upper limit on the cumulative size of all cached responses
59    /// for a given resource.
60    ///
61    /// Unit: bytes
62    #[serde(skip_serializing_if = "Option::is_none")]
63    #[serde(default)]
64    pub max_size: Option<u64>,
65
66    /// Upper limit on the number of cached responses stored at
67    /// a given time, for a given resource.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    #[serde(default)]
70    pub max_count: Option<usize>,
71
72    /// Whether a cached item should also track the of models and content
73    /// which it depends on, and evict when they are modified.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    #[serde(default)]
76    pub evict_on_dependency_change: Option<bool>,
77}
78
79#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
80#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
81#[derive(Deserialize, Serialize, Debug, Clone, Default)]
82pub struct CompressionAlgorithms(pub Vec<CompressionAlgorithm>);
83
84impl CompressionAlgorithms {
85    #[must_use]
86    pub(crate) fn get_list(&self) -> ArrayVec<CompressionAlgorithm, 5> {
87        let mut list = ArrayVec::<CompressionAlgorithm, 5>::new();
88
89        if self.0.is_empty() {
90            list.push(CompressionAlgorithm::Uncompressed);
91            return list;
92        }
93
94        let mut has_all = false;
95
96        for alg in &self.0 {
97            if *alg == CompressionAlgorithm::All {
98                has_all = true;
99            } else if !list.contains(alg) {
100                list.push(alg.clone());
101            }
102        }
103
104        if has_all {
105            for alg in [
106                CompressionAlgorithm::Brotli,
107                CompressionAlgorithm::Zstd { level: 17 },
108                CompressionAlgorithm::Deflate,
109                CompressionAlgorithm::Gzip,
110                CompressionAlgorithm::Uncompressed,
111            ] {
112                if !list.contains(&alg) {
113                    list.push(alg);
114                }
115            }
116        }
117
118        list
119    }
120}
121
122#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
123#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
124#[derive(Deserialize, Serialize, Debug, Clone)]
125pub struct KeyOn {
126    /// which params from the `path` to key on.
127    ///
128    /// i.e `/some/path/{key_on_me}` OR `/some/path/{*key_on_me}`
129    /// would use `{ "path_params": ["key_on_me"] }`
130    pub path_params: Option<SmallVec<[String; 2]>>,
131    /// which keys from the `query` to key on.
132    ///
133    /// i.e `?some=123`
134    /// `{ "query_keys": ["some"] }`
135    pub query_keys: Option<SmallVec<[String; 2]>>,
136    /// whether to key on a hash of the incoming
137    /// request body.
138    pub body_hash: Option<bool>,
139
140    /// whether to check/store the `ETag` before everything else.
141    ///
142    /// defaults to `true` if not passed.
143    pub etag: Option<bool>,
144}