codebase 0.11.0

Manage your codebase like a boss!
Documentation
// `cargo test` show as unused import for std::path::Path
// This is not an unused import at all (since used for the generic type)
#![allow(unused_imports)]

use std::collections::BTreeMap;
use std::fs;
use std::io::Write;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::project::Project;

pub const MANIFEST_FILE: &str = "manifest.json";
pub const MANIFEST_VERSION: &str = "0.1.0";

/// Represent the codebase manifest (manifest.json)
#[derive(Serialize, Deserialize)]
pub struct Manifest {
    pub projects: BTreeMap<String, Project>,
    pub version: String,
}

impl Manifest {
    pub fn new() -> Self {
        Manifest {
            projects: BTreeMap::new(),
            version: MANIFEST_VERSION.to_string(),
        }
    }

    pub fn read_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Manifest> {
        let json = fs::read_to_string(&path)?;
        let json: Manifest = serde_json::from_str(&json)?;

        Ok(json)
    }

    pub fn write_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
        let json = serde_json::to_string_pretty(&self)?;

        let mut file = fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(&path)?;
        file.write_all(json.as_ref())?;

        Ok(())
    }

    pub fn find_project<P: AsRef<Path>>(&self, path: P) -> Option<Project> {
        let path = path.as_ref().to_str().unwrap();
        self.projects.get(path).cloned()
    }

    pub fn add_project<P: AsRef<Path>>(&mut self, path: P, project: Project) -> anyhow::Result<()> {
        let path = path.as_ref().to_str().unwrap();

        if self.projects.contains_key(path) {
            return Err(anyhow::anyhow!(format!(
                "Project already exist at {}",
                path
            )));
        }

        self.projects.insert(path.to_string(), project);
        Ok(())
    }

    pub fn remove_project<P: AsRef<Path>>(&mut self, path: P) -> anyhow::Result<()> {
        let path = path.as_ref().to_str().unwrap();
        if !self.projects.contains_key(path) {
            return Err(anyhow::anyhow!(format!("No project found at {}", path)));
        }

        self.projects.remove(path);
        Ok(())
    }

    pub fn update_project<P: AsRef<Path>>(
        &mut self,
        path: P,
        project: Project,
    ) -> anyhow::Result<()> {
        self.remove_project(&path)?;
        self.add_project(&path, project)?;
        Ok(())
    }

    pub fn move_project<A: AsRef<Path>, B: AsRef<Path>>(
        &mut self,
        src: A,
        dst: B,
    ) -> anyhow::Result<()> {
        let project = self.find_project(&src).ok_or_else(|| {
            anyhow::anyhow!(format!("No project found at {}", src.as_ref().display()))
        })?;

        if self.projects.contains_key(dst.as_ref().to_str().unwrap()) {
            return Err(anyhow::anyhow!(format!(
                "Project already exist at {}",
                dst.as_ref().display()
            )));
        }

        self.remove_project(&src)?;
        self.add_project(&dst, project)?;

        Ok(())
    }
}

impl Default for Manifest {
    fn default() -> Self {
        Manifest::new()
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::path::Path;

    use crate::manifest::{Manifest, MANIFEST_VERSION};
    use crate::project::Project;

    #[test]
    fn test_new() {
        let manifest = Manifest::new();
        assert_eq!(manifest.version, MANIFEST_VERSION.to_string());
        assert_eq!(manifest.projects.len(), 0);
    }

    #[test]
    fn test_add_project() {
        let mut manifest = Manifest::new();

        let project = Project::new("", "", BTreeMap::new(), BTreeMap::new());
        let res = manifest.add_project("test/test", project.clone());
        assert!(res.is_ok());

        let res = manifest.add_project("test/test", project);
        assert!(res.is_err());
    }

    #[test]
    fn test_find_project() {
        let mut manifest = Manifest::new();

        let project = Project::new("a", "b", BTreeMap::new(), BTreeMap::new());
        manifest.add_project("test/test", project.clone()).unwrap();

        let project = manifest.find_project("test/test");
        assert!(&project.is_some());
        let project = project.unwrap();
        assert_eq!(project.remote, "a");
        assert_eq!(project.branch, "b");

        let project = manifest.find_project("not-exist");
        assert!(project.is_none());
    }

    #[test]
    fn test_remove_project() {
        let mut manifest = Manifest::new();

        let project = Project::new("", "", BTreeMap::new(), BTreeMap::new());
        manifest.add_project("test/test", project.clone()).unwrap();

        let res = manifest.remove_project("test/test");
        assert!(res.is_ok());

        let res = manifest.remove_project("test/test");
        assert!(res.is_err());
    }

    #[test]
    fn test_update_project() {
        let mut manifest = Manifest::new();

        let mut project = Project::new("a", "b", BTreeMap::new(), BTreeMap::new());
        manifest.add_project("test/test", project.clone()).unwrap();

        project.remote = "c".to_string();
        project.branch = "d".to_string();
        let res = manifest.update_project("test/test", project);
        assert!(res.is_ok());

        let project = manifest.find_project("test/test");
        assert!(project.is_some());

        let project = project.unwrap();
        assert_eq!(project.remote, "c");
        assert_eq!(project.branch, "d");
    }

    #[test]
    fn test_move_project() {
        let mut manifest = Manifest::new();

        let project = Project::new("", "", BTreeMap::new(), BTreeMap::new());
        manifest.add_project("test/test", project.clone()).unwrap();

        let res = manifest.move_project("test/test", "test/test-2");
        assert!(res.is_ok());

        let project = manifest.find_project("test/test");
        assert!(project.is_none());

        let project = manifest.find_project("test/test-2");
        assert!(project.is_some());
    }
}