coil-cache 0.1.1

Caching primitives for the Coil framework.
Documentation
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::time::Duration;

use crate::types::validate_token;
use crate::{CacheModelError, VariationKey};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CacheVisibility {
    Public,
    Private,
    NoStore,
}

impl fmt::Display for CacheVisibility {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Public => f.write_str("public"),
            Self::Private => f.write_str("private"),
            Self::NoStore => f.write_str("no_store"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheScope {
    visibility: CacheVisibility,
    tenant: Option<String>,
    site: Option<String>,
    locale: Option<String>,
    user: Option<String>,
    session: Option<String>,
    custom: BTreeMap<String, String>,
}

impl CacheScope {
    pub fn public() -> Self {
        Self::new(CacheVisibility::Public)
    }

    pub fn private() -> Self {
        Self::new(CacheVisibility::Private)
    }

    pub fn no_store() -> Self {
        Self::new(CacheVisibility::NoStore)
    }

    pub fn new(visibility: CacheVisibility) -> Self {
        Self {
            visibility,
            tenant: None,
            site: None,
            locale: None,
            user: None,
            session: None,
            custom: BTreeMap::new(),
        }
    }

    pub fn visibility(&self) -> CacheVisibility {
        self.visibility
    }

    pub fn tenant(&self) -> Option<&str> {
        self.tenant.as_deref()
    }

    pub fn site(&self) -> Option<&str> {
        self.site.as_deref()
    }

    pub fn locale(&self) -> Option<&str> {
        self.locale.as_deref()
    }

    pub fn user(&self) -> Option<&str> {
        self.user.as_deref()
    }

    pub fn session(&self) -> Option<&str> {
        self.session.as_deref()
    }

    pub fn custom(&self) -> &BTreeMap<String, String> {
        &self.custom
    }

    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Result<Self, CacheModelError> {
        self.tenant = Some(validate_token("tenant", tenant.into())?);
        Ok(self)
    }

    pub fn with_site(mut self, site: impl Into<String>) -> Result<Self, CacheModelError> {
        self.site = Some(validate_token("site", site.into())?);
        Ok(self)
    }

    pub fn with_locale(mut self, locale: impl Into<String>) -> Result<Self, CacheModelError> {
        self.locale = Some(validate_token("locale", locale.into())?);
        Ok(self)
    }

    pub fn with_user(mut self, user: impl Into<String>) -> Result<Self, CacheModelError> {
        if self.visibility == CacheVisibility::Public {
            return Err(CacheModelError::PublicScopeCannotVaryByUser);
        }

        self.user = Some(validate_token("user", user.into())?);
        Ok(self)
    }

    pub fn with_session(mut self, session: impl Into<String>) -> Result<Self, CacheModelError> {
        if self.visibility == CacheVisibility::Public {
            return Err(CacheModelError::PublicScopeCannotVaryBySession);
        }

        self.session = Some(validate_token("session", session.into())?);
        Ok(self)
    }

    pub fn with_custom_variation(
        mut self,
        name: impl Into<String>,
        value: impl Into<String>,
    ) -> Result<Self, CacheModelError> {
        let name = validate_token("variation_name", name.into())?;
        let value = validate_token("variation_value", value.into())?;
        self.custom.insert(name, value);
        Ok(self)
    }

    pub fn is_cacheable(&self) -> bool {
        self.visibility != CacheVisibility::NoStore
    }

    pub fn is_edge_cacheable(&self) -> bool {
        self.visibility == CacheVisibility::Public
    }

    pub fn variation_key(&self) -> Option<VariationKey> {
        self.variation_key_internal(false)
    }

    pub fn cache_partition_key(&self) -> Option<VariationKey> {
        self.variation_key_internal(true)
    }

    fn variation_key_internal(&self, include_visibility: bool) -> Option<VariationKey> {
        if self.visibility == CacheVisibility::NoStore {
            return None;
        }

        let mut parts = Vec::new();
        if include_visibility {
            parts.push(format!("visibility={}", self.visibility));
        }

        if let Some(tenant) = &self.tenant {
            parts.push(format!("tenant={tenant}"));
        }

        if let Some(site) = &self.site {
            parts.push(format!("site={site}"));
        }

        if let Some(locale) = &self.locale {
            parts.push(format!("locale={locale}"));
        }

        if let Some(user) = &self.user {
            parts.push(format!("user={user}"));
        }

        if let Some(session) = &self.session {
            parts.push(format!("session={session}"));
        }

        for (name, value) in &self.custom {
            parts.push(format!("x:{name}={value}"));
        }

        if parts.is_empty() {
            None
        } else {
            Some(VariationKey::new(parts.join("|")))
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct InvalidationSet {
    tags: BTreeSet<crate::InvalidationTag>,
}

impl InvalidationSet {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn from_tags(tags: impl IntoIterator<Item = crate::InvalidationTag>) -> Self {
        let mut set = Self::new();
        for tag in tags {
            set.insert(tag);
        }
        set
    }

    pub fn insert(&mut self, tag: crate::InvalidationTag) {
        self.tags.insert(tag);
    }

    pub fn len(&self) -> usize {
        self.tags.len()
    }

    pub fn is_empty(&self) -> bool {
        self.tags.is_empty()
    }

    pub fn iter(&self) -> impl Iterator<Item = &crate::InvalidationTag> {
        self.tags.iter()
    }

    pub fn header_value(&self) -> Option<String> {
        if self.tags.is_empty() {
            None
        } else {
            Some(
                self.tags
                    .iter()
                    .map(crate::InvalidationTag::as_str)
                    .collect::<Vec<_>>()
                    .join(" "),
            )
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FreshnessPolicy {
    ttl: Duration,
    stale_while_revalidate: Option<Duration>,
}

impl FreshnessPolicy {
    pub fn new(
        ttl: Duration,
        stale_while_revalidate: Option<Duration>,
    ) -> Result<Self, CacheModelError> {
        if ttl.is_zero() {
            return Err(CacheModelError::ZeroDuration { field: "ttl" });
        }

        if stale_while_revalidate.is_some_and(|value| value.is_zero()) {
            return Err(CacheModelError::ZeroDuration {
                field: "stale_while_revalidate",
            });
        }

        Ok(Self {
            ttl,
            stale_while_revalidate,
        })
    }

    pub fn ttl(&self) -> Duration {
        self.ttl
    }

    pub fn stale_while_revalidate(&self) -> Option<Duration> {
        self.stale_while_revalidate
    }

    pub fn ttl_seconds(&self) -> u64 {
        self.ttl.as_secs()
    }

    pub fn stale_while_revalidate_seconds(&self) -> Option<u64> {
        self.stale_while_revalidate.map(|value| value.as_secs())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ResponseValidators {
    pub etag: Option<crate::EntityTag>,
    pub last_modified_unix_seconds: Option<u64>,
}