gen-core 0.1.31

Core structs and representations for the gen sequence graph and version control system.
Documentation
use std::{
    env, fs,
    path::{Path, PathBuf},
};

use crate::{HashId, errors::ConfigError};

pub const CHANGESET_DIR_NAME: &str = "changesets";

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Workspace {
    base_dir: PathBuf,
}

impl Workspace {
    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
        Self {
            base_dir: base_dir.into(),
        }
    }

    pub fn from_current_dir() -> Self {
        Self::new(env::current_dir().unwrap())
    }

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

    pub fn ensure_gen_dir(&self) -> PathBuf {
        let gen_path = self.base_dir.join(".gen");
        ensure_dir(&gen_path);
        let changesets = gen_path.join(CHANGESET_DIR_NAME);
        ensure_dir(&changesets);
        gen_path
    }

    pub fn find_gen_dir(&self) -> Option<PathBuf> {
        let mut cur_dir = self.base_dir.as_path();
        let mut gen_path = cur_dir.join(".gen");
        while !gen_path.is_dir() {
            cur_dir = cur_dir.parent()?;
            gen_path = cur_dir.join(".gen");
        }
        Some(gen_path)
    }

    pub fn repo_root(&self) -> Result<PathBuf, ConfigError> {
        let gen_dir = self
            .find_gen_dir()
            .ok_or(ConfigError::GenDirectoryNotFound)?;

        gen_dir
            .parent()
            .map(Path::to_path_buf)
            .ok_or(ConfigError::RepoRootNotFound)
    }

    pub fn gen_db_path(&self) -> Result<PathBuf, ConfigError> {
        self.find_gen_dir()
            .map(|dir| dir.join("gen.db"))
            .ok_or(ConfigError::GenDirectoryNotFound)
    }

    pub fn changeset_path(&self, hash: &HashId) -> PathBuf {
        let path = self
            .ensure_gen_dir()
            .join(CHANGESET_DIR_NAME)
            .join(format!("{hash}"));
        ensure_dir(&path);
        path
    }
}

fn ensure_dir(path: &Path) {
    if !path.is_dir() {
        fs::create_dir_all(path).unwrap();
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn ensure_gen_dir_creates_directory() {
        let tmp_dir = tempdir().unwrap();
        let tmp_dir_path = tmp_dir.path().to_path_buf();
        let workspace = Workspace::new(&tmp_dir_path);

        let gen_dir = workspace.ensure_gen_dir();

        assert_eq!(gen_dir, tmp_dir_path.join(".gen"));
        assert!(gen_dir.is_dir());
    }

    #[test]
    fn find_gen_dir_walks_up_tree() {
        let tmp_dir = tempdir().unwrap();
        let tmp_dir_path = tmp_dir.path().to_path_buf();
        let root_workspace = Workspace::new(&tmp_dir_path);
        let gen_dir = root_workspace.ensure_gen_dir();

        let nested_dir = tmp_dir_path.join("nested").join("deep");
        fs::create_dir_all(&nested_dir).unwrap();
        let nested_workspace = Workspace::new(&nested_dir);

        assert_eq!(nested_workspace.find_gen_dir(), Some(gen_dir));
    }

    #[test]
    fn repo_root_returns_parent_of_gen_dir() {
        let tmp_dir = tempdir().unwrap();
        let tmp_dir_path = tmp_dir.path().to_path_buf();
        let workspace = Workspace::new(&tmp_dir_path);
        workspace.ensure_gen_dir();

        assert_eq!(workspace.repo_root().unwrap(), tmp_dir_path);
    }

    #[test]
    fn repo_root_errors_when_missing_gen_dir() {
        let tmp_dir = tempdir().unwrap();
        let tmp_dir_path = tmp_dir.path().to_path_buf();
        let workspace = Workspace::new(&tmp_dir_path);

        assert_eq!(
            Err(ConfigError::GenDirectoryNotFound),
            workspace.repo_root()
        );
    }

    #[test]
    fn gen_db_path_resolves_inside_gen_dir() {
        let tmp_dir = tempdir().unwrap();
        let tmp_dir_path = tmp_dir.path().to_path_buf();
        let workspace = Workspace::new(&tmp_dir_path);
        let gen_dir = workspace.ensure_gen_dir();

        assert_eq!(workspace.gen_db_path().unwrap(), gen_dir.join("gen.db"));
    }

    #[test]
    fn changeset_path_creates_directory_for_hash() {
        let tmp_dir = tempdir().unwrap();
        let tmp_dir_path = tmp_dir.path().to_path_buf();
        let workspace = Workspace::new(&tmp_dir_path);
        let hash = HashId([0; 32]);

        let path = workspace.changeset_path(&hash);

        assert_eq!(
            path,
            tmp_dir_path
                .join(".gen")
                .join(CHANGESET_DIR_NAME)
                .join(format!("{hash}"))
        );
        assert!(path.is_dir());
    }
}