use std::fmt::{self, Display};
use std::time::Instant;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EntityTag {
value: String,
}
impl EntityTag {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
}
}
pub fn entity(entity: &str) -> Self {
Self::new(format!("entity:{}", entity))
}
pub fn record<I: Display>(entity: &str, id: I) -> Self {
Self::new(format!("record:{}:{}", entity, id))
}
pub fn tenant(tenant: &str) -> Self {
Self::new(format!("tenant:{}", tenant))
}
pub fn query(name: &str) -> Self {
Self::new(format!("query:{}", name))
}
pub fn relation(from: &str, to: &str) -> Self {
Self::new(format!("rel:{}:{}", from, to))
}
pub fn value(&self) -> &str {
&self.value
}
pub fn matches(&self, pattern: &str) -> bool {
if pattern.contains('*') {
super::key::KeyPattern::new(pattern).matches_str(&self.value)
} else {
self.value == pattern
}
}
}
impl Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl From<&str> for EntityTag {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for EntityTag {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone)]
pub struct InvalidationEvent {
pub event_type: InvalidationEventType,
pub entity: String,
pub record_id: Option<String>,
pub timestamp: Instant,
pub tags: Vec<EntityTag>,
pub metadata: Option<String>,
}
impl InvalidationEvent {
pub fn new(event_type: InvalidationEventType, entity: impl Into<String>) -> Self {
Self {
event_type,
entity: entity.into(),
record_id: None,
timestamp: Instant::now(),
tags: Vec::new(),
metadata: None,
}
}
pub fn insert(entity: impl Into<String>) -> Self {
Self::new(InvalidationEventType::Insert, entity)
}
pub fn update(entity: impl Into<String>) -> Self {
Self::new(InvalidationEventType::Update, entity)
}
pub fn delete(entity: impl Into<String>) -> Self {
Self::new(InvalidationEventType::Delete, entity)
}
pub fn with_record<I: Display>(mut self, id: I) -> Self {
self.record_id = Some(id.to_string());
self
}
pub fn with_tag(mut self, tag: impl Into<EntityTag>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_tags(mut self, tags: impl IntoIterator<Item = EntityTag>) -> Self {
self.tags.extend(tags);
self
}
pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
self.metadata = Some(metadata.into());
self
}
pub fn all_tags(&self) -> Vec<EntityTag> {
let mut tags = self.tags.clone();
tags.push(EntityTag::entity(&self.entity));
if let Some(ref id) = self.record_id {
tags.push(EntityTag::record(&self.entity, id));
}
tags
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidationEventType {
Insert,
Update,
Delete,
Bulk,
SchemaChange,
Manual,
}
impl Display for InvalidationEventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Insert => write!(f, "insert"),
Self::Update => write!(f, "update"),
Self::Delete => write!(f, "delete"),
Self::Bulk => write!(f, "bulk"),
Self::SchemaChange => write!(f, "schema_change"),
Self::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Default)]
pub enum InvalidationStrategy {
#[default]
Immediate,
Delayed { delay_ms: u64 },
EventBased { events: Vec<InvalidationEventType> },
TagBased { tags: Vec<EntityTag> },
TtlOnly,
Custom { name: String },
}
impl InvalidationStrategy {
pub fn immediate() -> Self {
Self::Immediate
}
pub fn delayed(delay_ms: u64) -> Self {
Self::Delayed { delay_ms }
}
pub fn on_events(events: Vec<InvalidationEventType>) -> Self {
Self::EventBased { events }
}
pub fn for_tags(tags: Vec<EntityTag>) -> Self {
Self::TagBased { tags }
}
pub fn ttl_only() -> Self {
Self::TtlOnly
}
pub fn should_invalidate(&self, event: &InvalidationEvent) -> bool {
match self {
Self::Immediate => true,
Self::Delayed { .. } => true, Self::EventBased { events } => events.contains(&event.event_type),
Self::TagBased { tags } => event.all_tags().iter().any(|t| tags.contains(t)),
Self::TtlOnly => false,
Self::Custom { .. } => true, }
}
}
pub trait InvalidationHandler: Send + Sync + 'static {
fn handle(
&self,
event: &InvalidationEvent,
) -> impl std::future::Future<Output = super::CacheResult<()>> + Send;
}
pub struct FnHandler<F>(pub F);
impl<F, Fut> InvalidationHandler for FnHandler<F>
where
F: Fn(&InvalidationEvent) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = super::CacheResult<()>> + Send,
{
async fn handle(&self, event: &InvalidationEvent) -> super::CacheResult<()> {
(self.0)(event).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entity_tag() {
let tag = EntityTag::entity("User");
assert_eq!(tag.value(), "entity:User");
let record_tag = EntityTag::record("User", 123);
assert_eq!(record_tag.value(), "record:User:123");
}
#[test]
fn test_invalidation_event() {
let event = InvalidationEvent::insert("User")
.with_record(123)
.with_tag("custom_tag");
assert_eq!(event.entity, "User");
assert_eq!(event.record_id, Some("123".to_string()));
assert_eq!(event.event_type, InvalidationEventType::Insert);
let tags = event.all_tags();
assert!(tags.iter().any(|t| t.value() == "entity:User"));
assert!(tags.iter().any(|t| t.value() == "record:User:123"));
}
#[test]
fn test_invalidation_strategy() {
let immediate = InvalidationStrategy::immediate();
let event = InvalidationEvent::update("User");
assert!(immediate.should_invalidate(&event));
let events_only = InvalidationStrategy::on_events(vec![InvalidationEventType::Delete]);
assert!(!events_only.should_invalidate(&event));
let delete_event = InvalidationEvent::delete("User");
assert!(events_only.should_invalidate(&delete_event));
}
#[test]
fn test_tag_matching() {
let tag = EntityTag::new("entity:User");
assert!(tag.matches("entity:User"));
assert!(tag.matches("entity:*"));
assert!(!tag.matches("entity:Post"));
}
}