capp-cache 0.7.2

Common things i use to build Rust CLI tools for web crawlers.
Documentation
use std::collections::HashMap;

use capp_cache::{CacheEntryState, FileHttpCache, HttpCache, HttpCachePayload};
use tempfile::tempdir;

fn make_payload(body: &str) -> HttpCachePayload {
    let mut headers = HashMap::new();
    headers.insert("content-type".to_string(), "text/plain".to_string());

    let mut metadata = HashMap::new();
    metadata.insert("source".to_string(), "test".to_string());

    HttpCachePayload {
        body: body.to_string(),
        headers,
        metadata: Some(metadata),
    }
}

#[tokio::test]
async fn set_get_contains_and_remove_work() {
    let dir = tempdir().unwrap();
    let cache = FileHttpCache::<HttpCachePayload>::open(dir.path()).unwrap();
    let key = "https://example.com/page";

    cache
        .set(key, make_payload("hello"), 200, CacheEntryState::Fresh)
        .await
        .unwrap();

    assert!(cache.contains(key).await.unwrap());

    let entry = cache.get(key).await.unwrap().unwrap();
    assert_eq!(entry.key, key);
    assert_eq!(entry.status_code, 200);
    assert_eq!(entry.value.body, "hello");
    assert_eq!(entry.state, CacheEntryState::Fresh);
    assert!(entry.last_accessed >= entry.created_at);

    cache.remove(key).await.unwrap();
    assert!(!cache.contains(key).await.unwrap());
    assert!(cache.get(key).await.unwrap().is_none());
}

#[tokio::test]
async fn update_state_persists_to_disk() {
    let dir = tempdir().unwrap();
    let cache = FileHttpCache::<HttpCachePayload>::open(dir.path()).unwrap();
    let key = "https://example.com/state";

    cache
        .set(key, make_payload("state"), 200, CacheEntryState::Fresh)
        .await
        .unwrap();
    cache
        .update_state(key, CacheEntryState::Stale)
        .await
        .unwrap();

    let entry = cache.get(key).await.unwrap().unwrap();
    assert_eq!(entry.state, CacheEntryState::Stale);
}

#[tokio::test]
async fn cleanup_removes_invalid_and_repeated_errors() {
    let dir = tempdir().unwrap();
    let cache = FileHttpCache::<HttpCachePayload>::open(dir.path()).unwrap();

    cache
        .set("fresh", make_payload("fresh"), 200, CacheEntryState::Fresh)
        .await
        .unwrap();
    cache
        .set(
            "invalid",
            make_payload("invalid"),
            404,
            CacheEntryState::Invalid,
        )
        .await
        .unwrap();
    cache
        .set("error", make_payload("error"), 500, CacheEntryState::Fresh)
        .await
        .unwrap();

    for _ in 0..4 {
        cache
            .update_state("error", CacheEntryState::Error)
            .await
            .unwrap();
    }

    let deleted = cache.cleanup().await.unwrap();
    assert_eq!(deleted, 2);
    assert!(cache.contains("fresh").await.unwrap());
    assert!(!cache.contains("invalid").await.unwrap());
    assert!(!cache.contains("error").await.unwrap());
}

#[tokio::test]
async fn clear_all_removes_everything() {
    let dir = tempdir().unwrap();
    let cache = FileHttpCache::<HttpCachePayload>::open(dir.path()).unwrap();

    cache
        .set("one", make_payload("1"), 200, CacheEntryState::Fresh)
        .await
        .unwrap();
    cache
        .set("two", make_payload("2"), 200, CacheEntryState::Fresh)
        .await
        .unwrap();

    let deleted = cache.clear_all().await.unwrap();
    assert_eq!(deleted, 2);
    assert!(!cache.contains("one").await.unwrap());
    assert!(!cache.contains("two").await.unwrap());
}

#[tokio::test]
async fn cache_files_are_stored_in_date_segmented_directories() {
    let dir = tempdir().unwrap();
    let cache = FileHttpCache::<HttpCachePayload>::open(dir.path()).unwrap();

    cache
        .set(
            "https://example.com/segmented",
            make_payload("segmented"),
            200,
            CacheEntryState::Fresh,
        )
        .await
        .unwrap();

    let files_dir = dir.path().join("files");
    let years = std::fs::read_dir(&files_dir)
        .unwrap()
        .map(|item| item.unwrap().path())
        .collect::<Vec<_>>();
    assert_eq!(years.len(), 1);
    assert!(years[0].is_dir());

    let months = std::fs::read_dir(&years[0])
        .unwrap()
        .map(|item| item.unwrap().path())
        .collect::<Vec<_>>();
    assert_eq!(months.len(), 1);
    assert!(months[0].is_dir());

    let days = std::fs::read_dir(&months[0])
        .unwrap()
        .map(|item| item.unwrap().path())
        .collect::<Vec<_>>();
    assert_eq!(days.len(), 1);
    assert!(days[0].is_dir());

    let files = std::fs::read_dir(&days[0])
        .unwrap()
        .map(|item| item.unwrap().path())
        .collect::<Vec<_>>();
    assert_eq!(files.len(), 1);
    assert!(files[0].is_file());
}