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///
32/// The [`query_cache_policy!`](crate::query_cache_policy) macro provides a
33/// shorter declarative form when the policy is known at the call site.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct QueryCachePolicy {
36    name: Option<String>,
37    key: Option<String>,
38    tags: TagSet,
39    ttl: Option<Duration>,
40}
41
42impl QueryCachePolicy {
43    /// Create an empty cache policy.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Create a cache policy with a diagnostic operation name.
49    pub fn named(name: impl Into<String>) -> Self {
50        Self::new().with_name(name)
51    }
52
53    /// Return the optional diagnostic operation name.
54    pub fn name(&self) -> Option<&str> {
55        self.name.as_deref()
56    }
57
58    /// Return the logical key, if one has been configured.
59    pub fn key_value(&self) -> Option<&str> {
60        self.key.as_deref()
61    }
62
63    /// Return configured invalidation tags.
64    pub fn tags_value(&self) -> &[String] {
65        self.tags.as_slice()
66    }
67
68    /// Return the optional per-entry TTL.
69    pub fn ttl_value(&self) -> Option<Duration> {
70        self.ttl
71    }
72
73    /// Set or replace the diagnostic operation name.
74    pub fn with_name(mut self, name: impl Into<String>) -> Self {
75        self.name = Some(name.into());
76        self
77    }
78
79    /// Set the logical cache key.
80    pub fn key(mut self, key: impl Into<String>) -> Self {
81        self.key = Some(key.into());
82        self
83    }
84
85    /// Set the logical cache key from a segmented key builder.
86    pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
87        self.key(key.build_string())
88    }
89
90    /// Set the logical key and add the same entity invalidation tag.
91    pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
92        let key = entity_key(kind, id);
93        self.key = Some(key.clone());
94        self.tags = self.tags.tag(key);
95        self
96    }
97
98    /// Set the logical key and tags from [`CacheEntity`] metadata.
99    pub fn for_cache_entity<T>(mut self, id: T::Id) -> Self
100    where
101        T: CacheEntity,
102    {
103        let key = T::cache_key_for(&id);
104        self.key = Some(key);
105        self.tags = self.tags.tag(T::entity_tag_for(&id));
106        self.tags = append_optional_tag(self.tags, T::collection_tag());
107        self
108    }
109
110    /// Set the logical key and invalidation tag for a collection result.
111    pub fn collection(mut self, name: impl ToString) -> Self {
112        let tag = collection_tag(name);
113        self.key = Some(tag.clone());
114        self.tags = self.tags.tag(tag);
115        self
116    }
117
118    /// Add one invalidation tag.
119    pub fn tag(mut self, tag: impl Into<String>) -> Self {
120        self.tags = self.tags.tag(tag);
121        self
122    }
123
124    /// Add a collection invalidation tag from one escaped key segment.
125    pub fn collection_tag(mut self, name: impl ToString) -> Self {
126        self.tags = self.tags.tag(collection_tag(name));
127        self
128    }
129
130    /// Add several invalidation tags.
131    pub fn tags<I, S>(mut self, tags: I) -> Self
132    where
133        I: IntoIterator<Item = S>,
134        S: Into<String>,
135    {
136        self.tags = self.tags.tags(tags);
137        self
138    }
139
140    /// Replace invalidation tags from a reusable [`TagSet`].
141    pub fn tag_set(mut self, tags: TagSet) -> Self {
142        self.tags = tags;
143        self
144    }
145
146    /// Set a per-entry TTL.
147    pub fn ttl(mut self, ttl: Duration) -> Self {
148        self.ttl = Some(ttl);
149        self
150    }
151
152    pub(crate) fn cache_options(&self) -> CacheOptions {
153        let mut options = CacheOptions::new().tag_set(self.tags.clone());
154        if let Some(ttl) = self.ttl {
155            options = options.ttl(ttl);
156        }
157        options
158    }
159}
160
161pub(crate) fn entity_key(kind: impl ToString, id: impl ToString) -> String {
162    CacheKeyBuilder::new().entity(kind, id).build_string()
163}
164
165pub(crate) fn collection_tag(name: impl ToString) -> String {
166    CacheKeyBuilder::from_segment(name).build_string()
167}
168
169fn append_optional_tag(tags: TagSet, tag: Option<String>) -> TagSet {
170    match tag {
171        Some(tag) => tags.tag(tag),
172        None => tags,
173    }
174}