coil-cache 0.1.1

Caching primitives for the Coil framework.
Documentation
use crate::{CacheModelError, CacheScope, FreshnessPolicy, InvalidationSet, ResponseValidators};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApplicationCachePolicy {
    scope: CacheScope,
    freshness: FreshnessPolicy,
    tags: InvalidationSet,
}

impl ApplicationCachePolicy {
    pub fn new(
        scope: CacheScope,
        freshness: FreshnessPolicy,
        tags: InvalidationSet,
    ) -> Result<Self, CacheModelError> {
        if !scope.is_cacheable() {
            return Err(CacheModelError::UncacheableApplicationScope);
        }

        Ok(Self {
            scope,
            freshness,
            tags,
        })
    }

    pub fn scope(&self) -> &CacheScope {
        &self.scope
    }

    pub fn freshness(&self) -> FreshnessPolicy {
        self.freshness
    }

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpCachePolicy {
    scope: CacheScope,
    freshness: Option<FreshnessPolicy>,
    validators: ResponseValidators,
    surrogate_tags: InvalidationSet,
}

impl HttpCachePolicy {
    pub fn new(
        scope: CacheScope,
        freshness: Option<FreshnessPolicy>,
        validators: ResponseValidators,
        surrogate_tags: InvalidationSet,
    ) -> Result<Self, CacheModelError> {
        match (scope.is_cacheable(), freshness) {
            (true, None) => Err(CacheModelError::MissingHttpFreshness),
            (false, Some(_)) => Err(CacheModelError::NoStoreCannotDefineFreshness),
            _ => Ok(Self {
                scope,
                freshness,
                validators,
                surrogate_tags,
            }),
        }
    }

    pub fn scope(&self) -> &CacheScope {
        &self.scope
    }

    pub fn freshness(&self) -> Option<FreshnessPolicy> {
        self.freshness
    }

    pub fn validators(&self) -> &ResponseValidators {
        &self.validators
    }

    pub fn surrogate_tags(&self) -> &InvalidationSet {
        &self.surrogate_tags
    }

    pub fn cache_control_value(&self) -> String {
        match (self.scope.visibility(), self.freshness) {
            (crate::CacheVisibility::NoStore, _) => "no-store".to_string(),
            (crate::CacheVisibility::Public, Some(freshness)) => {
                cache_control_value("public", freshness)
            }
            (crate::CacheVisibility::Private, Some(freshness)) => {
                cache_control_value("private", freshness)
            }
            (_, None) => "no-store".to_string(),
        }
    }
}

fn cache_control_value(visibility: &str, freshness: FreshnessPolicy) -> String {
    let mut directives = vec![format!("{visibility}, max-age={}", freshness.ttl_seconds())];

    if let Some(swr) = freshness.stale_while_revalidate_seconds() {
        directives.push(format!("stale-while-revalidate={swr}"));
    }

    directives.join(", ")
}