Skip to main content

hydracache_db/
policy.rs

1use std::time::Duration;
2
3use hydracache::{CacheKeyBuilder, CacheOptions, TagSet};
4
5use crate::CacheEntity;
6
7/// Reusable cache metadata for one database query result.
8///
9/// `QueryCachePolicy` contains the database-neutral parts of query result
10/// caching: diagnostic name, logical key, invalidation tags, and optional TTL.
11/// It is intentionally independent of SQLx, Diesel, SeaORM, or any other
12/// database client.
13///
14/// # Example
15///
16/// ```rust
17/// use std::time::Duration;
18///
19/// use hydracache_db::QueryCachePolicy;
20///
21/// let policy = QueryCachePolicy::named("load-user")
22///     .key("user:42")
23///     .tag("user:42")
24///     .ttl(Duration::from_secs(60));
25///
26/// assert_eq!(policy.name(), Some("load-user"));
27/// assert_eq!(policy.key_value(), Some("user:42"));
28/// assert_eq!(policy.tags_value(), &["user:42".to_owned()]);
29/// assert_eq!(policy.ttl_value(), Some(Duration::from_secs(60)));
30/// ```
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub struct QueryCachePolicy {
33    name: Option<String>,
34    key: Option<String>,
35    tags: TagSet,
36    ttl: Option<Duration>,
37}
38
39impl QueryCachePolicy {
40    /// Create an empty cache policy.
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Create a cache policy with a diagnostic operation name.
46    pub fn named(name: impl Into<String>) -> Self {
47        Self::new().with_name(name)
48    }
49
50    /// Return the optional diagnostic operation name.
51    pub fn name(&self) -> Option<&str> {
52        self.name.as_deref()
53    }
54
55    /// Return the logical key, if one has been configured.
56    pub fn key_value(&self) -> Option<&str> {
57        self.key.as_deref()
58    }
59
60    /// Return configured invalidation tags.
61    pub fn tags_value(&self) -> &[String] {
62        self.tags.as_slice()
63    }
64
65    /// Return the optional per-entry TTL.
66    pub fn ttl_value(&self) -> Option<Duration> {
67        self.ttl
68    }
69
70    /// Set or replace the diagnostic operation name.
71    pub fn with_name(mut self, name: impl Into<String>) -> Self {
72        self.name = Some(name.into());
73        self
74    }
75
76    /// Set the logical cache key.
77    pub fn key(mut self, key: impl Into<String>) -> Self {
78        self.key = Some(key.into());
79        self
80    }
81
82    /// Set the logical cache key from a segmented key builder.
83    pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
84        self.key(key.build_string())
85    }
86
87    /// Set the logical key and add the same entity invalidation tag.
88    pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
89        let key = entity_key(kind, id);
90        self.key = Some(key.clone());
91        self.tags = self.tags.tag(key);
92        self
93    }
94
95    /// Set the logical key and tags from [`CacheEntity`] metadata.
96    pub fn for_cache_entity<T>(mut self, id: T::Id) -> Self
97    where
98        T: CacheEntity,
99    {
100        let key = T::cache_key_for(&id);
101        self.key = Some(key);
102        self.tags = self.tags.tag(T::entity_tag_for(&id));
103        self.tags = append_optional_tag(self.tags, T::collection_tag());
104        self
105    }
106
107    /// Set the logical key and invalidation tag for a collection result.
108    pub fn collection(mut self, name: impl ToString) -> Self {
109        let tag = collection_tag(name);
110        self.key = Some(tag.clone());
111        self.tags = self.tags.tag(tag);
112        self
113    }
114
115    /// Add one invalidation tag.
116    pub fn tag(mut self, tag: impl Into<String>) -> Self {
117        self.tags = self.tags.tag(tag);
118        self
119    }
120
121    /// Add a collection invalidation tag from one escaped key segment.
122    pub fn collection_tag(mut self, name: impl ToString) -> Self {
123        self.tags = self.tags.tag(collection_tag(name));
124        self
125    }
126
127    /// Add several invalidation tags.
128    pub fn tags<I, S>(mut self, tags: I) -> Self
129    where
130        I: IntoIterator<Item = S>,
131        S: Into<String>,
132    {
133        self.tags = self.tags.tags(tags);
134        self
135    }
136
137    /// Replace invalidation tags from a reusable [`TagSet`].
138    pub fn tag_set(mut self, tags: TagSet) -> Self {
139        self.tags = tags;
140        self
141    }
142
143    /// Set a per-entry TTL.
144    pub fn ttl(mut self, ttl: Duration) -> Self {
145        self.ttl = Some(ttl);
146        self
147    }
148
149    pub(crate) fn cache_options(&self) -> CacheOptions {
150        let mut options = CacheOptions::new().tag_set(self.tags.clone());
151        if let Some(ttl) = self.ttl {
152            options = options.ttl(ttl);
153        }
154        options
155    }
156}
157
158pub(crate) fn entity_key(kind: impl ToString, id: impl ToString) -> String {
159    CacheKeyBuilder::new().entity(kind, id).build_string()
160}
161
162pub(crate) fn collection_tag(name: impl ToString) -> String {
163    CacheKeyBuilder::from_segment(name).build_string()
164}
165
166fn append_optional_tag(tags: TagSet, tag: Option<String>) -> TagSet {
167    match tag {
168        Some(tag) => tags.tag(tag),
169        None => tags,
170    }
171}