koi-common 0.2.202603241449

Shared types, traits, and utilities for the Koi local network toolkit
Documentation
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::io;
use std::path::Path;

pub fn read_json<T: DeserializeOwned>(path: &Path) -> Result<T, io::Error> {
    let json = std::fs::read_to_string(path)?;
    serde_json::from_str(&json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

pub fn read_json_if_exists<T: DeserializeOwned>(path: &Path) -> Result<Option<T>, io::Error> {
    if !path.exists() {
        return Ok(None);
    }
    read_json(path).map(Some)
}

pub fn read_json_or_default<T: DeserializeOwned + Default>(path: &Path) -> Result<T, io::Error> {
    match read_json_if_exists(path)? {
        Some(value) => Ok(value),
        None => Ok(T::default()),
    }
}

pub fn write_json_pretty<T: Serialize>(path: &Path, value: &T) -> Result<(), io::Error> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let json = serde_json::to_string_pretty(value)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    let tmp = path.with_extension("json.tmp");
    std::fs::write(&tmp, json)?;
    std::fs::rename(&tmp, path)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_path(name: &str) -> std::path::PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        std::env::temp_dir().join(format!("koi-persist-{name}-{nanos}"))
    }

    #[test]
    fn read_json_invalid_returns_invalid_data() {
        let dir = temp_path("invalid");
        let path = dir.join("bad.json");
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(&path, "{broken json").unwrap();

        let err = read_json::<serde_json::Value>(&path).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn read_json_or_default_missing_returns_default() {
        let dir = temp_path("missing");
        let path = dir.join("missing.json");

        let value: Vec<String> = read_json_or_default(&path).unwrap();
        assert!(value.is_empty());
    }

    #[test]
    fn write_json_pretty_creates_parent_dir() {
        let path = temp_path("write").join("nested").join("value.json");
        write_json_pretty(&path, &vec!["a", "b"]).unwrap();
        assert!(path.exists());
    }

    #[test]
    fn write_json_pretty_fails_on_directory_path() {
        let dir = temp_path("dir");
        std::fs::create_dir_all(&dir).unwrap();

        let result = write_json_pretty(&dir, &vec!["a"]);
        assert!(result.is_err());
    }
}