this-me 0.3.2

Rust ground for the modern .me semantic kernel.
Documentation
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};

use serde_json::Value as JsonValue;

use crate::kernel::{snapshot_from_json, snapshot_to_json, Kernel, KernelError, Snapshot};

#[derive(Debug)]
pub enum StorageError {
    Io(std::io::Error),
    Json(serde_json::Error),
    Codec(crate::kernel::JsonCodecError),
    Kernel(KernelError),
}

impl fmt::Display for StorageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(error) => write!(f, "{error}"),
            Self::Json(error) => write!(f, "{error}"),
            Self::Codec(error) => write!(f, "{error}"),
            Self::Kernel(error) => write!(f, "{error}"),
        }
    }
}

impl std::error::Error for StorageError {}

impl From<std::io::Error> for StorageError {
    fn from(error: std::io::Error) -> Self {
        Self::Io(error)
    }
}

impl From<serde_json::Error> for StorageError {
    fn from(error: serde_json::Error) -> Self {
        Self::Json(error)
    }
}

impl From<crate::kernel::JsonCodecError> for StorageError {
    fn from(error: crate::kernel::JsonCodecError) -> Self {
        Self::Codec(error)
    }
}

impl From<KernelError> for StorageError {
    fn from(error: KernelError) -> Self {
        Self::Kernel(error)
    }
}

pub trait MemoryStore {
    fn load_snapshot(&self) -> Result<Option<Snapshot>, StorageError>;
    fn save_snapshot(&self, snapshot: &Snapshot) -> Result<(), StorageError>;

    fn load_kernel(&self) -> Result<Kernel, StorageError> {
        match self.load_snapshot()? {
            Some(snapshot) => Ok(Kernel::hydrate(snapshot)?),
            None => Ok(Kernel::new()),
        }
    }

    fn save_kernel(&self, kernel: &Kernel) -> Result<(), StorageError> {
        self.save_snapshot(&kernel.export_snapshot())
    }
}

#[derive(Debug, Clone)]
pub struct JsonFileStore {
    path: PathBuf,
}

impl JsonFileStore {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }
}

impl MemoryStore for JsonFileStore {
    fn load_snapshot(&self) -> Result<Option<Snapshot>, StorageError> {
        if !self.path.exists() {
            return Ok(None);
        }

        let raw = fs::read_to_string(&self.path)?;
        if raw.trim().is_empty() {
            return Ok(None);
        }

        let json = serde_json::from_str::<JsonValue>(&raw)?;
        Ok(Some(snapshot_from_json(&json)?))
    }

    fn save_snapshot(&self, snapshot: &Snapshot) -> Result<(), StorageError> {
        if let Some(parent) = self.path.parent() {
            fs::create_dir_all(parent)?;
        }

        let json = snapshot_to_json(snapshot);
        let raw = serde_json::to_string_pretty(&json)?;
        let tmp_path = self.path.with_extension(format!(
            "{}tmp",
            self.path
                .extension()
                .and_then(|extension| extension.to_str())
                .map(|extension| format!("{extension}."))
                .unwrap_or_default()
        ));

        fs::write(&tmp_path, raw)?;
        fs::rename(&tmp_path, &self.path)?;
        Ok(())
    }
}

impl<S> MemoryStore for Option<S>
where
    S: MemoryStore,
{
    fn load_snapshot(&self) -> Result<Option<Snapshot>, StorageError> {
        match self {
            Some(store) => store.load_snapshot(),
            None => Ok(None),
        }
    }

    fn save_snapshot(&self, snapshot: &Snapshot) -> Result<(), StorageError> {
        match self {
            Some(store) => store.save_snapshot(snapshot),
            None => Ok(()),
        }
    }
}