elemental-shared 0.1.0

Lightweight, version-controlled data abstraction for persistent storage.
Documentation
use crate::{
    scope::Scope,
    version::{Persistor, VersionControlled},
};
use anyhow::Result;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::{io::ErrorKind, marker::PhantomData, path::PathBuf};
use tokio::fs::{read_to_string, write};

pub struct NoPersistor;

impl<V: VersionControlled> Persistor<V> for NoPersistor {
    async fn load(&self) -> Result<Option<V>> {
        Ok(Some(V::default()))
    }

    async fn save(&self, _value: &V) -> Result<()> {
        Ok(())
    }
}

pub trait PersistorIO<S> {
    fn read(path: PathBuf) -> impl Future<Output = Result<Option<S>>>;
    fn write(path: PathBuf, data: S) -> impl Future<Output = Result<()>>;
}

pub struct AsyncFileStringIO;

impl PersistorIO<String> for AsyncFileStringIO {
    async fn read(path: PathBuf) -> Result<Option<String>> {
        match read_to_string(&path).await {
            Ok(d) => Ok(Some(d)),
            Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    async fn write(path: PathBuf, data: String) -> Result<()> {
        write(&path, data).await?;
        Ok(())
    }
}

pub struct CustomPersistor<
    V: Serialize + DeserializeOwned,
    S,
    D: ?Sized,
    IO: PersistorIO<S>,
    Ser: Fn(&V) -> Result<S>,
    De: Fn(&D) -> Result<V>,
> {
    pub ser: Ser,
    pub de: De,
    pub id: String,
    pub scope: Scope,
    pub suffix: Option<String>,
    _marker: PhantomData<(IO, V, S, *const D)>,
}

impl<V, S, D, IO, Ser, De> CustomPersistor<V, S, D, IO, Ser, De>
where
    V: Serialize + DeserializeOwned,
    D: ?Sized,
    IO: PersistorIO<S>,
    Ser: Fn(&V) -> Result<S>,
    De: Fn(&D) -> Result<V>,
    S: AsRef<D>,
{
    pub fn new(ser: Ser, de: De, id: String, scope: Scope, suffix: Option<String>) -> Self {
        Self {
            ser,
            de,
            id,
            scope,
            suffix,
            _marker: PhantomData,
        }
    }
}

#[cfg(feature = "json")]
type JsonFilePersistor<V> = CustomPersistor<
    V,
    String,
    str,
    AsyncFileStringIO,
    fn(&V) -> Result<String>,
    fn(&str) -> Result<V>,
>;

#[cfg(feature = "json")]
fn serialize_json<V: Serialize>(value: &V) -> Result<String> {
    Ok(serde_json::to_string(value)?)
}

#[cfg(feature = "json")]
fn deserialize_json<V: DeserializeOwned>(value: &str) -> Result<V> {
    Ok(serde_json::from_str(value)?)
}

impl<V, S, D, IO, Ser, De> Persistor<V> for CustomPersistor<V, S, D, IO, Ser, De>
where
    V: VersionControlled + Serialize + DeserializeOwned,
    D: ?Sized,
    IO: PersistorIO<S>,
    Ser: Fn(&V) -> Result<S>,
    De: Fn(&D) -> Result<V>,
    S: AsRef<D>,
{
    async fn load(&self) -> Result<Option<V>> {
        let path = self
            .scope
            .get_full_path(&self.id, self.suffix.clone())
            .await?;
        let data = IO::read(path).await?;
        match data {
            Some(d) => Ok(Some((self.de)(d.as_ref())?)),
            None => Ok(None),
        }
    }

    async fn save(&self, value: &V) -> Result<()> {
        let data = (self.ser)(value)?;
        let path = self
            .scope
            .get_full_path(&self.id, self.suffix.clone())
            .await?;
        IO::write(path, data).await?;
        Ok(())
    }
}

#[cfg(feature = "json")]
pub fn json_persistor<V: Serialize + DeserializeOwned>(
    id: String,
    scope: Scope,
) -> JsonFilePersistor<V> {
    CustomPersistor::new(
        serialize_json::<V>,
        deserialize_json::<V>,
        id,
        scope,
        Some("json".to_string()),
    )
}

#[cfg(feature = "toml")]
type TomlFilePersistor<V> = CustomPersistor<
    V,
    String,
    str,
    AsyncFileStringIO,
    fn(&V) -> Result<String>,
    fn(&str) -> Result<V>,
>;

#[cfg(feature = "toml")]
fn serialize_toml<V: Serialize>(value: &V) -> Result<String> {
    Ok(toml::to_string(value)?)
}

#[cfg(feature = "toml")]
fn deserialize_toml<V: DeserializeOwned>(value: &str) -> Result<V> {
    Ok(toml::from_str(value)?)
}

#[cfg(feature = "toml")]
pub fn toml_persistor<V: Serialize + DeserializeOwned>(
    id: String,
    scope: Scope,
) -> TomlFilePersistor<V> {
    CustomPersistor::new(
        serialize_toml::<V>,
        deserialize_toml::<V>,
        id,
        scope,
        Some("toml".to_string()),
    )
}