liminal-server 0.24.9

Standalone server for the liminal messaging bus
Documentation
//! A store whose `config.json` predates a key the create arm writes opens with
//! that key written in. `retention` ends as the durable store's reclaim
//! (`DURABLE_RETENTION`): the adopted `keep_all` is replaced by the same open.

use std::error::Error;
use std::path::{Path, PathBuf};

use super::services::open_persistent_store;

/// Every `config.json` under `root`.
fn config_files(root: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let mut found = Vec::new();
    for entry in std::fs::read_dir(root)? {
        let path = entry?.path();
        if path.is_dir() {
            found.extend(config_files(&path)?);
        } else if path.file_name().is_some_and(|name| name == "config.json") {
            found.push(path);
        }
    }
    Ok(found)
}

fn stored(config: &Path) -> Result<serde_json::Map<String, serde_json::Value>, Box<dyn Error>> {
    let value: serde_json::Value = serde_json::from_slice(&std::fs::read(config)?)?;
    Ok(value
        .as_object()
        .ok_or("config.json is not a JSON object")?
        .clone())
}

#[test]
fn a_store_created_without_retention_opens_with_reclaim_written_in() -> Result<(), Box<dyn Error>> {
    let dir = tempfile::tempdir()?;
    drop(open_persistent_store(dir.path())?);
    let configs = config_files(dir.path())?;
    let [config] = configs.as_slice() else {
        return Err(format!("expected one config.json, found {configs:?}").into());
    };

    // The store as haematite 0.14.1 and earlier wrote it: no `retention` key.
    let mut older = stored(config)?;
    older.remove("retention");
    std::fs::write(config, serde_json::to_vec_pretty(&older)?)?;
    assert!(!stored(config)?.contains_key("retention"));

    drop(open_persistent_store(dir.path())?);

    let adopted = stored(config)?;
    assert_eq!(
        adopted.get("retention"),
        Some(&serde_json::to_value(haematite::Retention::reclaim(0))?)
    );
    assert_eq!(
        adopted.get("node_cache_budget"),
        older.get("node_cache_budget")
    );
    Ok(())
}

#[test]
fn a_store_created_without_either_key_opens_with_both_written_in() -> Result<(), Box<dyn Error>> {
    let dir = tempfile::tempdir()?;
    drop(open_persistent_store(dir.path())?);
    let configs = config_files(dir.path())?;
    let [config] = configs.as_slice() else {
        return Err(format!("expected one config.json, found {configs:?}").into());
    };

    let mut older = stored(config)?;
    older.remove("retention");
    older.remove("node_cache_budget");
    std::fs::write(config, serde_json::to_vec_pretty(&older)?)?;

    drop(open_persistent_store(dir.path())?);

    let adopted = stored(config)?;
    assert_eq!(
        adopted.get("retention"),
        Some(&serde_json::to_value(haematite::Retention::reclaim(0))?)
    );
    assert_eq!(
        adopted.get("node_cache_budget"),
        Some(&serde_json::Value::String("unlimited".to_owned()))
    );
    Ok(())
}