hydracache_db/
prepared.rs1use std::time::Duration;
2
3use hydracache::{CacheKeyBuilder, TagSet};
4
5use crate::policy::collection_tag;
6use crate::{CacheEntity, QueryCachePolicy};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct PreparedQueryPolicy {
43 name: Option<String>,
44 key: PreparedQueryKey,
45 tags: TagSet,
46 ttl: Option<Duration>,
47}
48
49impl Default for PreparedQueryPolicy {
50 fn default() -> Self {
51 Self {
52 name: None,
53 key: PreparedQueryKey::Missing,
54 tags: TagSet::new(),
55 ttl: None,
56 }
57 }
58}
59
60impl PreparedQueryPolicy {
61 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn named(name: impl Into<String>) -> Self {
68 Self::new().with_name(name)
69 }
70
71 pub fn for_entity(kind: impl ToString) -> Self {
76 Self::new().entity(kind)
77 }
78
79 pub fn for_cache_entity<T>() -> Self
83 where
84 T: CacheEntity,
85 {
86 let mut policy = Self::for_entity(T::ENTITY);
87 if let Some(tag) = T::COLLECTION {
88 policy = policy.collection_tag(tag);
89 }
90 policy
91 }
92
93 pub fn name(&self) -> Option<&str> {
95 self.name.as_deref()
96 }
97
98 pub fn requires_id(&self) -> bool {
100 matches!(self.key, PreparedQueryKey::EntityPrefix(_))
101 }
102
103 pub fn static_key_value(&self) -> Option<&str> {
105 match &self.key {
106 PreparedQueryKey::Static(key) => Some(key),
107 PreparedQueryKey::Missing | PreparedQueryKey::EntityPrefix(_) => None,
108 }
109 }
110
111 pub fn entity_key_prefix(&self) -> Option<&str> {
113 match &self.key {
114 PreparedQueryKey::EntityPrefix(prefix) => Some(prefix),
115 PreparedQueryKey::Missing | PreparedQueryKey::Static(_) => None,
116 }
117 }
118
119 pub fn tags_value(&self) -> &[String] {
121 self.tags.as_slice()
122 }
123
124 pub fn ttl_value(&self) -> Option<Duration> {
126 self.ttl
127 }
128
129 pub fn with_name(mut self, name: impl Into<String>) -> Self {
131 self.name = Some(name.into());
132 self
133 }
134
135 pub fn key(mut self, key: impl Into<String>) -> Self {
137 self.key = PreparedQueryKey::Static(key.into());
138 self
139 }
140
141 pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
143 self.key(key.build_string())
144 }
145
146 pub fn entity(mut self, kind: impl ToString) -> Self {
148 self.key = PreparedQueryKey::EntityPrefix(escaped_segment(kind));
149 self
150 }
151
152 pub fn collection(mut self, name: impl ToString) -> Self {
154 let tag = collection_tag(name);
155 self.key = PreparedQueryKey::Static(tag.clone());
156 self.tags = self.tags.tag(tag);
157 self
158 }
159
160 pub fn tag(mut self, tag: impl Into<String>) -> Self {
162 self.tags = self.tags.tag(tag);
163 self
164 }
165
166 pub fn collection_tag(mut self, name: impl ToString) -> Self {
168 self.tags = self.tags.tag(collection_tag(name));
169 self
170 }
171
172 pub fn tags<I, S>(mut self, tags: I) -> Self
174 where
175 I: IntoIterator<Item = S>,
176 S: Into<String>,
177 {
178 self.tags = self.tags.tags(tags);
179 self
180 }
181
182 pub fn tag_set(mut self, tags: TagSet) -> Self {
184 self.tags = tags;
185 self
186 }
187
188 pub fn ttl(mut self, ttl: Duration) -> Self {
190 self.ttl = Some(ttl);
191 self
192 }
193
194 pub fn to_policy(&self) -> QueryCachePolicy {
199 let mut policy = self.base_policy();
200 if let PreparedQueryKey::Static(key) = &self.key {
201 policy = policy.key(key.clone());
202 }
203 policy
204 }
205
206 pub fn bind_id(&self, id: impl ToString) -> QueryCachePolicy {
212 let mut policy = self.to_policy();
213 if let PreparedQueryKey::EntityPrefix(prefix) = &self.key {
214 let key = format!("{prefix}:{}", escaped_segment(id));
215 policy = policy.key(key.clone()).tag(key);
216 }
217 policy
218 }
219
220 fn base_policy(&self) -> QueryCachePolicy {
221 let mut policy = QueryCachePolicy::new().tag_set(self.tags.clone());
222 if let Some(name) = &self.name {
223 policy = policy.with_name(name.clone());
224 }
225 if let Some(ttl) = self.ttl {
226 policy = policy.ttl(ttl);
227 }
228 policy
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233enum PreparedQueryKey {
234 Missing,
235 Static(String),
236 EntityPrefix(String),
237}
238
239fn escaped_segment(segment: impl ToString) -> String {
240 CacheKeyBuilder::from_segment(segment).build_string()
241}
242
243#[cfg(test)]
244mod tests {
245 use std::time::Duration;
246
247 use hydracache::TagSet;
248
249 use crate::{CacheEntity, PreparedQueryPolicy};
250
251 struct User;
252
253 impl CacheEntity for User {
254 type Id = i64;
255
256 const ENTITY: &'static str = "user";
257 const COLLECTION: Option<&'static str> = Some("users");
258 }
259
260 #[test]
261 fn prepared_static_policy_builds_reusable_runtime_policy() {
262 let prepared = PreparedQueryPolicy::named("list-users")
263 .collection("users:active")
264 .ttl(Duration::from_secs(30));
265
266 assert!(!prepared.requires_id());
267 assert_eq!(prepared.name(), Some("list-users"));
268 assert_eq!(prepared.static_key_value(), Some("users%3Aactive"));
269 assert_eq!(prepared.entity_key_prefix(), None);
270 assert_eq!(prepared.tags_value(), &["users%3Aactive".to_owned()]);
271 assert_eq!(prepared.ttl_value(), Some(Duration::from_secs(30)));
272
273 let policy = prepared.to_policy();
274 assert_eq!(policy.key_value(), Some("users%3Aactive"));
275 assert_eq!(policy.tags_value(), &["users%3Aactive".to_owned()]);
276 assert_eq!(policy.ttl_value(), Some(Duration::from_secs(30)));
277 }
278
279 #[test]
280 fn prepared_entity_policy_precomputes_prefix_and_binds_id() {
281 let prepared = PreparedQueryPolicy::for_entity("account:user")
282 .with_name("load-account-user")
283 .collection_tag("users:active");
284
285 assert!(prepared.requires_id());
286 assert_eq!(prepared.static_key_value(), None);
287 assert_eq!(prepared.entity_key_prefix(), Some("account%3Auser"));
288 assert_eq!(prepared.tags_value(), &["users%3Aactive".to_owned()]);
289
290 let policy = prepared.bind_id("42%beta");
291 assert_eq!(policy.name(), Some("load-account-user"));
292 assert_eq!(policy.key_value(), Some("account%3Auser:42%25beta"));
293 assert_eq!(
294 policy.tags_value(),
295 &[
296 "users%3Aactive".to_owned(),
297 "account%3Auser:42%25beta".to_owned()
298 ]
299 );
300 }
301
302 #[test]
303 fn prepared_cache_entity_policy_reuses_entity_metadata() {
304 let prepared = PreparedQueryPolicy::for_cache_entity::<User>()
305 .with_name("load-user")
306 .ttl(Duration::from_secs(60));
307
308 assert_eq!(prepared.entity_key_prefix(), Some("user"));
309 assert_eq!(prepared.tags_value(), &["users".to_owned()]);
310
311 let policy = prepared.bind_id(42);
312 assert_eq!(policy.name(), Some("load-user"));
313 assert_eq!(policy.key_value(), Some("user:42"));
314 assert_eq!(
315 policy.tags_value(),
316 &["users".to_owned(), "user:42".to_owned()]
317 );
318 assert_eq!(policy.ttl_value(), Some(Duration::from_secs(60)));
319 }
320
321 #[test]
322 fn prepared_policy_can_use_custom_static_key_and_tag_set() {
323 let prepared = PreparedQueryPolicy::new()
324 .key("tenant:7:users")
325 .tag_set(TagSet::new().tag("tenant:7").tag("users"));
326
327 let policy = prepared.to_policy();
328 assert_eq!(policy.key_value(), Some("tenant:7:users"));
329 assert_eq!(
330 policy.tags_value(),
331 &["tenant:7".to_owned(), "users".to_owned()]
332 );
333 }
334}