use std::time::Duration;
use hydracache::{CacheKeyBuilder, TagSet};
use crate::policy::collection_tag;
use crate::{CacheEntity, QueryCachePolicy};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedQueryPolicy {
name: Option<String>,
key: PreparedQueryKey,
tags: TagSet,
ttl: Option<Duration>,
}
impl Default for PreparedQueryPolicy {
fn default() -> Self {
Self {
name: None,
key: PreparedQueryKey::Missing,
tags: TagSet::new(),
ttl: None,
}
}
}
impl PreparedQueryPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn named(name: impl Into<String>) -> Self {
Self::new().with_name(name)
}
pub fn for_entity(kind: impl ToString) -> Self {
Self::new().entity(kind)
}
pub fn for_cache_entity<T>() -> Self
where
T: CacheEntity,
{
let mut policy = Self::for_entity(T::ENTITY);
if let Some(tag) = T::COLLECTION {
policy = policy.collection_tag(tag);
}
policy
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn requires_id(&self) -> bool {
matches!(self.key, PreparedQueryKey::EntityPrefix(_))
}
pub fn static_key_value(&self) -> Option<&str> {
match &self.key {
PreparedQueryKey::Static(key) => Some(key),
PreparedQueryKey::Missing | PreparedQueryKey::EntityPrefix(_) => None,
}
}
pub fn entity_key_prefix(&self) -> Option<&str> {
match &self.key {
PreparedQueryKey::EntityPrefix(prefix) => Some(prefix),
PreparedQueryKey::Missing | PreparedQueryKey::Static(_) => None,
}
}
pub fn tags_value(&self) -> &[String] {
self.tags.as_slice()
}
pub fn ttl_value(&self) -> Option<Duration> {
self.ttl
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = PreparedQueryKey::Static(key.into());
self
}
pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
self.key(key.build_string())
}
pub fn entity(mut self, kind: impl ToString) -> Self {
self.key = PreparedQueryKey::EntityPrefix(escaped_segment(kind));
self
}
pub fn collection(mut self, name: impl ToString) -> Self {
let tag = collection_tag(name);
self.key = PreparedQueryKey::Static(tag.clone());
self.tags = self.tags.tag(tag);
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags = self.tags.tag(tag);
self
}
pub fn collection_tag(mut self, name: impl ToString) -> Self {
self.tags = self.tags.tag(collection_tag(name));
self
}
pub fn tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = self.tags.tags(tags);
self
}
pub fn tag_set(mut self, tags: TagSet) -> Self {
self.tags = tags;
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn to_policy(&self) -> QueryCachePolicy {
let mut policy = self.base_policy();
if let PreparedQueryKey::Static(key) = &self.key {
policy = policy.key(key.clone());
}
policy
}
pub fn bind_id(&self, id: impl ToString) -> QueryCachePolicy {
let mut policy = self.to_policy();
if let PreparedQueryKey::EntityPrefix(prefix) = &self.key {
let key = format!("{prefix}:{}", escaped_segment(id));
policy = policy.key(key.clone()).tag(key);
}
policy
}
fn base_policy(&self) -> QueryCachePolicy {
let mut policy = QueryCachePolicy::new().tag_set(self.tags.clone());
if let Some(name) = &self.name {
policy = policy.with_name(name.clone());
}
if let Some(ttl) = self.ttl {
policy = policy.ttl(ttl);
}
policy
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PreparedQueryKey {
Missing,
Static(String),
EntityPrefix(String),
}
fn escaped_segment(segment: impl ToString) -> String {
CacheKeyBuilder::from_segment(segment).build_string()
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use hydracache::TagSet;
use crate::{CacheEntity, PreparedQueryPolicy};
struct User;
impl CacheEntity for User {
type Id = i64;
const ENTITY: &'static str = "user";
const COLLECTION: Option<&'static str> = Some("users");
}
#[test]
fn prepared_static_policy_builds_reusable_runtime_policy() {
let prepared = PreparedQueryPolicy::named("list-users")
.collection("users:active")
.ttl(Duration::from_secs(30));
assert!(!prepared.requires_id());
assert_eq!(prepared.name(), Some("list-users"));
assert_eq!(prepared.static_key_value(), Some("users%3Aactive"));
assert_eq!(prepared.entity_key_prefix(), None);
assert_eq!(prepared.tags_value(), &["users%3Aactive".to_owned()]);
assert_eq!(prepared.ttl_value(), Some(Duration::from_secs(30)));
let policy = prepared.to_policy();
assert_eq!(policy.key_value(), Some("users%3Aactive"));
assert_eq!(policy.tags_value(), &["users%3Aactive".to_owned()]);
assert_eq!(policy.ttl_value(), Some(Duration::from_secs(30)));
}
#[test]
fn prepared_entity_policy_precomputes_prefix_and_binds_id() {
let prepared = PreparedQueryPolicy::for_entity("account:user")
.with_name("load-account-user")
.collection_tag("users:active");
assert!(prepared.requires_id());
assert_eq!(prepared.static_key_value(), None);
assert_eq!(prepared.entity_key_prefix(), Some("account%3Auser"));
assert_eq!(prepared.tags_value(), &["users%3Aactive".to_owned()]);
let policy = prepared.bind_id("42%beta");
assert_eq!(policy.name(), Some("load-account-user"));
assert_eq!(policy.key_value(), Some("account%3Auser:42%25beta"));
assert_eq!(
policy.tags_value(),
&[
"users%3Aactive".to_owned(),
"account%3Auser:42%25beta".to_owned()
]
);
}
#[test]
fn prepared_cache_entity_policy_reuses_entity_metadata() {
let prepared = PreparedQueryPolicy::for_cache_entity::<User>()
.with_name("load-user")
.ttl(Duration::from_secs(60));
assert_eq!(prepared.entity_key_prefix(), Some("user"));
assert_eq!(prepared.tags_value(), &["users".to_owned()]);
let policy = prepared.bind_id(42);
assert_eq!(policy.name(), Some("load-user"));
assert_eq!(policy.key_value(), Some("user:42"));
assert_eq!(
policy.tags_value(),
&["users".to_owned(), "user:42".to_owned()]
);
assert_eq!(policy.ttl_value(), Some(Duration::from_secs(60)));
}
#[test]
fn prepared_policy_can_use_custom_static_key_and_tag_set() {
let prepared = PreparedQueryPolicy::new()
.key("tenant:7:users")
.tag_set(TagSet::new().tag("tenant:7").tag("users"));
let policy = prepared.to_policy();
assert_eq!(policy.key_value(), Some("tenant:7:users"));
assert_eq!(
policy.tags_value(),
&["tenant:7".to_owned(), "users".to_owned()]
);
}
}