ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use anyhow::Result;
use ghee_lang::{Key, Namespace};
use serde::{Deserialize, Serialize};

use std::{
    env::current_dir,
    ffi::OsString,
    path::{Component, Path, PathBuf},
};

use thiserror::Error;

use crate::{GheeErr, HIDDEN_PREFIX};

#[derive(Error, Debug)]
pub enum RelativizeErr {
    #[error("Base path {base:?} does not prefix path {path:?}")]
    DoesNotPrefix { base: PathBuf, path: PathBuf },
}

pub trait PathBufExt {
    fn relative_to_curdir(&self) -> Result<PathBuf> {
        let cur = current_dir().unwrap();
        self.relativize(cur)
    }

    /// Relativize relative to the current directory
    fn relative_to_curdir_if_possible(&self) -> PathBuf {
        let cur = current_dir().unwrap();
        self.relativize_if_possible(cur)
    }

    /** Relativize this absolute path against an absolute base path `base` */
    fn relativize<P: AsRef<Path>>(&self, base: P) -> Result<PathBuf>;

    /// Replace this relative path's initial `Component::CurDir` with another path `new_curdir`.
    ///
    /// `new_curdir` can be relative or absolute
    fn resolve_curdir<P: AsRef<Path>>(&self, new_curdir: P) -> Result<PathBuf>;

    fn relativize_if_possible<P: AsRef<Path>>(&self, base: P) -> PathBuf;

    fn resolve_curdir_if_possible<P: AsRef<Path>>(&self, new_curdir: P) -> PathBuf;
}

impl PathBufExt for PathBuf {
    fn relativize<P: AsRef<Path>>(&self, base: P) -> Result<PathBuf> {
        if !self.is_absolute() {
            return Err(GheeErr::PathNotAbsolute(self.clone()).into());
        }

        let base = base.as_ref();
        if !base.is_absolute() {
            return Err(GheeErr::PathNotAbsolute(base.to_path_buf()).into());
        }

        let mut path_comps_it = self.components();

        for base_comp in base.components() {
            let path_comp = path_comps_it
                .next()
                .ok_or_else(|| RelativizeErr::DoesNotPrefix {
                    base: base.to_path_buf(),
                    path: self.clone(),
                })?;
            if path_comp != base_comp {
                return Err(RelativizeErr::DoesNotPrefix {
                    base: base.to_path_buf(),
                    path: self.clone(),
                }
                .into());
            }
        }

        let relpath_it = std::iter::once(Component::CurDir).chain(path_comps_it);

        Ok(PathBuf::from_iter(relpath_it))
    }

    fn resolve_curdir<P: AsRef<Path>>(&self, new_curdir: P) -> Result<PathBuf> {
        if !self.is_relative() {
            return Err(GheeErr::PathNotRelative(self.clone()).into());
        }
        let it = new_curdir
            .as_ref()
            .components()
            .chain(self.components().skip(1));
        Ok(PathBuf::from_iter(it))
    }

    fn relativize_if_possible<P: AsRef<Path>>(&self, base: P) -> PathBuf {
        self.relativize(base).unwrap_or_else(|_e| self.clone())
    }

    fn resolve_curdir_if_possible<P: AsRef<Path>>(&self, new_curdir: P) -> PathBuf {
        self.resolve_curdir(new_curdir)
            .unwrap_or_else(|_e| self.clone())
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct PathBufs {
    pub rel: Option<PathBuf>,
    pub abs: PathBuf,
    pub curdir: PathBuf,
}
impl PathBufs {
    pub fn new(rel: Option<PathBuf>, abs: PathBuf) -> Self {
        debug_assert!(abs.is_absolute());
        let curdir = abs.relative_to_curdir().unwrap();
        Self { rel, abs, curdir }
    }

    pub fn rel_else_abs(&self) -> &PathBuf {
        self.rel.as_ref().unwrap_or(&self.abs)
    }
}

#[derive(Error, Debug)]
pub enum SubIdxPathErr {
    #[error("The provided key was empty")]
    EmptyKey,
}

/// The path of a sub-index: an index nested inside another (primary) index
pub fn sub_idx_path(view_of: &PathBuf, key: &Key) -> Result<PathBuf> {
    if key.is_empty() {
        return Err(SubIdxPathErr::EmptyKey.into());
    }

    let mut sub_idx_dirname = OsString::new();

    for k in key.iter() {
        sub_idx_dirname.push(HIDDEN_PREFIX);
        if k.namespace == Namespace::User {
            sub_idx_dirname.push(k.attr.clone());
        } else {
            sub_idx_dirname.push(k.to_string());
        }
    }

    let mut sub_idx_path = view_of.clone();
    sub_idx_path.push(sub_idx_dirname);

    Ok(sub_idx_path)
}

/// The path to place table snapshots in
pub fn table_snapshots_path(table: &PathBuf) -> PathBuf {
    let mut snapshot_path = table.clone();

    snapshot_path.push(format!("{}snapshots", HIDDEN_PREFIX));

    snapshot_path
}

pub fn table_snapshot_path<S: ToString>(table: &PathBuf, snapshot_uuid: S) -> PathBuf {
    let mut p = table_snapshots_path(table);

    p.push(snapshot_uuid.to_string());

    p
}

#[cfg(test)]
mod test {
    use std::path::PathBuf;

    use crate::paths::PathBufExt;

    #[test]
    fn test_relativize() {
        let p1 = PathBuf::from("/the/quick/brown/fox");
        let p2 = PathBuf::from("/the/quick/brown/fox/jumped/over/the/lazy/dog");

        let p3 = p2.relativize(&p1).unwrap();

        assert_eq!(p3, PathBuf::from("./jumped/over/the/lazy/dog"));
    }

    #[test]
    fn test_relativize_relative_base() {
        let p1 = PathBuf::from("./the/quick/brown/fox");
        let p2 = PathBuf::from("/the/quick/brown/fox/jumped/over/the/lazy/dog");

        assert!(p2.relativize(&p1).is_err());
    }

    #[test]
    fn test_relativize_relative_path() {
        let p1 = PathBuf::from("/the/quick/brown/fox");
        let p2 = PathBuf::from("./the/quick/brown/fox/jumped/over/the/lazy/dog");

        assert!(p2.relativize(&p1).is_err());
    }

    #[test]
    fn test_resolve_curdir() {
        let p1 = PathBuf::from("/the/quick/brown/fox");
        let p2 = PathBuf::from("/the/quick/brown/fox/jumped/over/the/lazy/dog");

        let p3 = PathBuf::from("/jumped/over/the/lazy/dog");

        let p4 = PathBuf::from("./jumped/over/the/lazy/dog");

        assert!(p3.resolve_curdir(&p1).is_err());

        assert_eq!(p4.resolve_curdir(p1).unwrap(), p2);
    }
}