axum-cache-fred 0.2.0

Axum middleware for response caching with fred
Documentation
//!
//! This module defines the trait and different strategies used to create a cache key for a
//! response
//!
use fred::types::Key;
use http::Request;
use tracing::log::*;

use std::time::Duration;

/// Trait used for extensible definition of caching strategies for the middleware
pub trait CacheStrategy<B> {
    /// The default time to live for all cached entries
    ///
    /// This can be overridden by different strategies but defaults to **1 day**
    fn ttl(&self) -> Duration {
        Duration::from_secs(86_400)
    }

    /// Compute the key for caching.
    ///
    /// This function will receive the [Request] sent into the handler so that it may use
    /// attributes of the incoming request for caching
    fn computed_key(&self, req: &Request<B>) -> (Duration, Key);
}

/// A caching strategy which uses the default ttl for all cache keys and uses the (method, path) of
/// the request route to compute a key.
///
/// This caching strategy should only be used in cases where _all_ requests to a given route should
/// receive the same response(s) and should **not** be used for user-specific or other protected
/// routes.
#[derive(Debug, Default, Clone)]
pub struct RouteKey {}

impl<B> CacheStrategy<B> for RouteKey {
    fn computed_key(&self, req: &Request<B>) -> (Duration, Key) {
        let key = format!("{}-{}", req.method(), req.uri().path().to_lowercase());
        trace!("computed a cache key of `{key}`");
        (<RouteKey as CacheStrategy<B>>::ttl(self), key.into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn test_route_key() -> anyhow::Result<()> {
        let strategy = RouteKey::default();
        let req: Request<()> = Request::builder()
            .method(http::method::Method::GET)
            .uri("https://example.com/healthz")
            .body(())?;
        let (_, key) = strategy.computed_key(&req);
        assert_eq!(key, "GET-/healthz".into());
        Ok(())
    }

    #[test]
    fn test_case_insensitive_key() -> anyhow::Result<()> {
        let strategy = RouteKey::default();
        let req: Request<()> = Request::builder()
            .method(http::method::Method::GET)
            .uri("https://example.com/HealthZ")
            .body(())?;
        let (_, key) = strategy.computed_key(&req);
        assert_eq!(key, "GET-/healthz".into());
        Ok(())
    }

    #[test]
    fn test_key_at_root() -> anyhow::Result<()> {
        let strategy = RouteKey::default();
        let req: Request<()> = Request::builder()
            .method(http::method::Method::GET)
            .uri("https://example.com/")
            .body(())?;
        let (_, key) = strategy.computed_key(&req);
        assert_eq!(key, "GET-/".into());
        Ok(())
    }
}