ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
};

use anyhow::Result;

use ghee_lang::Key;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::XATTR_TABLE_INFO;

#[derive(Error, Debug)]
enum TableInfoErr {
    #[error("An io error occurred")]
    IoError(std::io::Error),

    #[error("A json error occurred: {0}")]
    JsonError(serde_json::Error),

    #[error("Path {0:?} does not exist")]
    NoSuchPath(PathBuf),
}

#[derive(Clone, Serialize, Deserialize)]
pub struct TableInfo {
    key: Key,

    indices_abs: BTreeMap<Key, PathBuf>,
}
impl TableInfo {
    /// Initialize a new TableInfo for a table with primary key `key` located at path `path`.
    ///
    /// Alternate indices should be added subsequently using `add_index`
    pub fn new<P: AsRef<Path>>(key: Key, path: P) -> Self {
        let abs_path = path.as_ref().absolutize().unwrap().to_path_buf();

        let mut indices_abs = BTreeMap::new();
        indices_abs.insert(key.clone(), abs_path.clone());

        Self { key, indices_abs }
    }

    pub fn key(&self) -> &Key {
        &self.key
    }

    pub fn path_abs(&self) -> &PathBuf {
        &self.indices_abs[&self.key]
    }

    pub fn add_index(&mut self, key: Key, path: PathBuf) {
        let abs_path = path.absolutize().unwrap().to_path_buf();
        debug_assert!(abs_path.is_absolute());
        self.indices_abs.insert(key.clone(), abs_path.clone());
    }

    pub fn indices_abs(&self) -> &BTreeMap<Key, PathBuf> {
        debug_assert!(self.indices_abs.values().all(|p| p.is_absolute()));
        &self.indices_abs
    }

    pub fn index_path_abs(&self, key: &Key) -> &PathBuf {
        &self.indices_abs[key]
    }
}

/**
 * Get information about the table at path `dir` if one exists.
 * If no table info is found, or `dir` is actually a file, Ok(None) is returned.
 *
 */
pub fn table_info<P: AsRef<Path>>(dir: P) -> Result<Option<TableInfo>> {
    let dir = dir.as_ref();

    if !dir.exists() {
        return Err(TableInfoErr::NoSuchPath(dir.to_path_buf()).into());
    }

    if !dir.is_dir() {
        return Ok(None);
    }

    let raw = xattr::get(dir, XATTR_TABLE_INFO.to_osstring())
        .map_err(TableInfoErr::IoError)?
        .unwrap_or_default();

    if raw.is_empty() {
        return Ok(None);
    }

    let info: TableInfo =
        serde_json::from_slice(raw.as_slice()).map_err(TableInfoErr::JsonError)?;

    debug_assert!(info.indices_abs.contains_key(&info.key));
    debug_assert_eq!(info.path_abs(), &dir.absolutize().unwrap().to_path_buf());

    Ok(Some(info))
}

pub fn set_table_info<P: AsRef<Path>>(dir: P, info: &TableInfo) -> Result<()> {
    let json = serde_json::to_string(&info).map_err(TableInfoErr::JsonError)?;
    Ok(
        xattr::set(dir, XATTR_TABLE_INFO.to_osstring(), json.as_bytes())
            .map_err(TableInfoErr::IoError)?,
    )
}

/// If `path` itself is not initialized as a Ghee table, its ancestor directories
/// will be searched to see if it is part of an initialized table, in which case
/// the containing table's indices are returned.
pub fn containing_table_info<P: AsRef<Path>>(path: P) -> Result<Option<TableInfo>> {
    let path = path.as_ref();

    if !path.exists() {
        return Err(TableInfoErr::NoSuchPath(path.into()).into());
    }

    let mut path = path;
    if path.is_file() {
        path = path.parent().unwrap();
    }

    debug_assert!(path.is_dir());

    if let Some(table_info) = table_info(&path)? {
        return Ok(Some(table_info));
    }

    let mut abs_path = path.absolutize().unwrap().to_path_buf();
    loop {
        match abs_path.parent() {
            None => return Ok(None),
            Some(parent) => {
                match table_info(parent)? {
                    Some(parent_info) => return Ok(Some(parent_info)),
                    None => { /* do nothing; keep searching */ }
                };

                abs_path.pop();
            }
        }
    }
}

#[cfg(test)]
mod test {
    use std::{
        env::current_dir,
        fs::{create_dir_all, File},
    };

    use ghee_lang::{Key, Namespace, Xattr};

    use super::{containing_table_info, set_table_info, table_info, TableInfo};
    use crate::{
        cmd::init,
        test_support::{CurrentDirGuard, TempDirAuto},
    };

    #[test]
    fn test_key_roundtrip() {
        let dir = TempDirAuto::new("ghee-test-key-roundtrip");

        assert!(table_info(&dir).unwrap().is_none());

        let key_component = Xattr::new(Namespace::User, "pizza");

        let key = Key::new(vec![key_component]);

        let info = TableInfo::new(key.clone(), dir.to_path_buf());

        set_table_info(&dir, &info).unwrap();

        assert_eq!(table_info(&dir).unwrap().unwrap().key, key);
    }

    #[test]
    fn test_table_info() {
        let dir1 = TempDirAuto::new("ghee-test-table-info");
        let key1 = Key::from_string("region,city");

        init(&dir1, &key1, false).unwrap();

        let dir2 = {
            let mut dir = dir1.clone();
            dir.push("pnw");
            dir
        };

        let dir3 = {
            let mut dir = dir2.clone();
            dir.push("Portland");
            dir
        };

        create_dir_all(&dir3).unwrap();

        let dir4 = {
            let mut dir = dir3.clone();
            dir.push("blahblahblah");
            dir
        };

        {
            let info = table_info(&dir1).unwrap().unwrap();
            assert!(info.indices_abs.contains_key(&key1));
        }

        for dir in vec![&dir2, &dir3] {
            let info = table_info(dir).unwrap();
            assert!(info.is_none());
        }

        assert!(
            table_info(&dir4).is_err(),
            "dir4 doesn't exist and so should error when getting table info"
        );

        {
            let file_path = {
                let mut p = dir3.clone();
                p.push("README.md");
                p
            };

            File::create(&file_path).unwrap();

            assert!(
                table_info(&file_path).unwrap().is_none(),
                "Files per se have no table info and so should error"
            );
        }
    }

    #[test]
    fn test_containing_table_info() {
        let cur = current_dir().unwrap();

        let dir1 = TempDirAuto::new("ghee-test-containing-table-info");

        let prior = CurrentDirGuard::new(&dir1);

        let key1 = Key::from_string("region,city");

        init(&dir1, &key1, false).unwrap();

        let dir2 = {
            let mut dir = dir1.clone();
            dir.push("pnw");
            dir
        };

        let dir3 = {
            let mut dir = dir2.clone();
            dir.push("Portland");
            dir
        };

        create_dir_all(&dir3).unwrap();

        let file = {
            let mut p = dir3.clone();
            p.push("README.md");
            File::create(&p).unwrap();
            p
        };

        let dir4 = {
            let mut dir = dir3.clone();
            dir.push("blahblahblah");
            dir
        };

        for dir in vec![&dir1, &dir2, &dir3, &file] {
            let info = containing_table_info(dir).unwrap().unwrap();
            assert_eq!(info.key, key1);
            assert!(info.indices_abs.contains_key(&key1));
        }

        assert!(
            containing_table_info(&dir4).is_err(),
            "dir4 doesn't exist and so should error when getting table info"
        );

        assert_eq!(prior.prior_dir, cur);
    }
}