gen-file-database 1.0.6

File based minimal database
Documentation
use std::collections::BTreeSet;
use std::io;
use std::path::PathBuf;
use convert_case::Case;
use convert_case::Casing;
use gen_file::folder;
use serde::Deserialize;
use serde::Serialize;
use uuid::Uuid;
use crate::index;
use crate::temporary;

pub fn init_db_folder(target_folder: &TargetFolder) -> io::Result<()> {
    let path: PathBuf = target_folder.path.join(temporary::FOLDER_NAME);
    folder::remove_if_exists(&path)?;
    folder::create(&path)?;
    Ok(())
}

pub fn delete_db_folder(target_folder: &TargetFolder) -> io::Result<()> {
    folder::remove_if_exists(&target_folder.path)?;
    Ok(())
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TargetFolder {
    pub path: PathBuf
}

impl TargetFolder {
    pub fn from(path: &PathBuf) -> TargetFolder {
        TargetFolder {
            path: path.to_path_buf()
        }
    }
}

#[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize, Deserialize)]
pub struct Capsule<T> {
    pub inner: T,
    pub id: Uuid,
    pub key: String,
    pub folder: TargetFolder,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Persist<T> {
    pub inner: T,
}

pub trait Indexable {
    fn index_keys(&self) -> std::collections::BTreeSet<String>;
}

pub trait FileDbKey {
    fn file_db_key() -> String;
}

pub fn save_capsule<T: Serialize + Clone + for<'a> Deserialize<'a> + FileDbKey>(capsule: Capsule<T>, index_keys: BTreeSet<String>) -> io::Result<Capsule<T>> {
    if let Some(id) = if !index_keys.is_empty()
    { index::index(index_keys, &capsule.key, &capsule.id, &capsule.folder)? } else { None } {
        Capsule::<T>::get_by_id(&id, &capsule.folder)
    } else {
        temporary::save(capsule)
    }
}

pub fn upsert_capsule<T: Serialize + Clone + for<'a> Deserialize<'a> + FileDbKey>(capsule: Capsule<T>, index_keys: BTreeSet<String>) -> io::Result<Capsule<T>> {
    if let Some(id) = if !index_keys.is_empty()
    { index::index(index_keys, &capsule.key, &capsule.id, &capsule.folder)? } else { None } {
        let mut found = Capsule::<T>::get_by_id(&id, &capsule.folder)?;
        found.inner = capsule.inner;
        temporary::save(found)
    } else {
        temporary::save(capsule)
    }
}

pub fn save_object<T: Serialize + Clone + FileDbKey>(data: &T, target_folder: &TargetFolder) -> io::Result<T> {
    temporary::create(target_folder, &T::file_db_key().to_case(Case::Snake), data)?;
    Ok(data.clone())
}


impl <T: Serialize + for<'a> Deserialize<'a> + Clone + FileDbKey> Capsule<T> {
    pub fn update(&self) -> io::Result<Capsule<T>>
    where
        T: Indexable,
    {
        let capsule = self.clone();
        if !self.inner.index_keys().is_empty() {
            let _ = index::update(self.inner.index_keys(), &capsule.key, &capsule.id, &capsule.folder);
        }
        temporary::save(capsule)
    }


    pub fn get_by_id(id: &Uuid, target_folder: &TargetFolder) -> io::Result<Capsule<T>> {
        temporary::select::<Capsule<T>>(target_folder, &format!("{}_{}", T::file_db_key().to_case(Case::Snake), id))
            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("cannot find {} {} in database", T::file_db_key(), id)))
    }

    pub fn get_all(target_folder: &TargetFolder) -> io::Result<Vec<Capsule<T>>> {
        let id_vec = index::get_all_by_type(&T::file_db_key().to_case(Case::Snake), target_folder)?;

        Ok(id_vec
            .into_iter()
            .flat_map(|id| Capsule::<T>::get_by_id(&id, target_folder))
            .collect())
    }

    pub fn find_by_index(keys: &[&str], target_folder: &TargetFolder) -> io::Result<Vec<Capsule<T>>> {
        let id_vec = index::find_by_type(&T::file_db_key().to_case(Case::Snake), keys, target_folder)?;

        Ok(id_vec
            .into_iter()
            .flat_map(|id| Capsule::<T>::get_by_id(&id, target_folder))
            .collect())
    }
}

impl <T: Serialize + for<'a> Deserialize<'a> + Clone + FileDbKey> Persist<T> {
    pub fn get(target_folder: &TargetFolder) -> io::Result<T> {
        temporary::select::<T>(target_folder, &T::file_db_key().to_case(Case::Snake))
            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("cannot find {} in database", T::file_db_key())))
    }
}

#[cfg(test)]
#[path = "../tests/unit_tests/persistence.rs"]
pub mod test;