robe 0.0.8

Wear your configs: a tiny CLI for saving and switching between dotfile configurations
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::errors::RobeError;

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Registry {
    pub base_path: PathBuf,
    pub targets: HashMap<String, TargetRegistry>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TargetRegistry {
    pub name: String,
    pub real_path: PathBuf,
    pub profiles: Vec<String>,
}

impl TargetRegistry {
    pub fn new(name: &str, meta: &TargetMetadata, profiles: &[PathBuf]) -> Self {
        let mut prof: Vec<String> = Vec::new();
        for path in profiles {
            if let Some(name) = path.file_name().map(|f| f.to_string_lossy().to_string()) {
                prof.push(name);
            }
        }
        Self {
            name: name.to_string(),
            real_path: PathBuf::from(meta.real_path.clone()),
            profiles: prof,
        }
    }

    pub fn assert_profile_exists(&self, profile: &str) -> Result<(), RobeError> {
        if self.profiles.iter().any(|p| p == profile) {
            Ok(())
        } else {
            Err(RobeError::message(format!(
                "Profile {}/{} not found.",
                &self.name, &profile
            )))
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TargetMetadata {
    pub real_path: String,
}

impl TargetMetadata {
    pub fn create(path: &Path) -> Result<Self, RobeError> {
        let tp = path.canonicalize()?;
        Ok(Self {
            real_path: tp.to_string_lossy().to_string(),
        })
    }
}

impl Registry {
    pub fn get_target_registry(&self, target: &str) -> Option<TargetRegistry> {
        self.targets.get(target).cloned()
    }

    pub fn target_registry(&self, target: &str) -> Result<TargetRegistry, RobeError> {
        match self.get_target_registry(target) {
            None => Err(RobeError::message(format!("Target {} not found.", target))),
            Some(tr) => Ok(tr),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_target_registry_new() {
        let dir = tempdir().unwrap();

        let p1 = dir.path().join("work");
        let p2 = dir.path().join("clean");

        fs::create_dir(&p1).unwrap();
        fs::create_dir(&p2).unwrap();

        let meta = TargetMetadata {
            real_path: "/real/path".to_string(),
        };

        let tr = TargetRegistry::new("tmux", &meta, &vec![p1.clone(), p2.clone()]);

        assert_eq!(tr.name, "tmux");
        assert_eq!(tr.real_path, PathBuf::from("/real/path"));
        assert_eq!(tr.profiles.len(), 2);
        assert!(tr.profiles.contains(&"work".to_string()));
        assert!(tr.profiles.contains(&"clean".to_string()));
    }

    #[test]
    fn test_assert_profile_exists_ok() {
        let tr = TargetRegistry {
            name: "tmux".to_string(),
            real_path: PathBuf::from("/tmp"),
            profiles: vec!["work".to_string(), "clean".to_string()],
        };

        assert!(tr.assert_profile_exists("work").is_ok());
    }

    #[test]
    fn test_assert_profile_exists_error() {
        let tr = TargetRegistry {
            name: "tmux".to_string(),
            real_path: PathBuf::from("/tmp"),
            profiles: vec!["work".to_string()],
        };

        let err = tr.assert_profile_exists("missing").unwrap_err();
        let msg = format!("{}", err);

        assert!(msg.contains("Profile tmux/missing not found"));
    }

    #[test]
    fn test_target_metadata_create() {
        let dir = tempdir().unwrap();
        let path = dir.path();

        let meta = TargetMetadata::create(path).unwrap();

        assert!(meta.real_path.contains(path.to_string_lossy().as_ref()));
    }

    #[test]
    fn test_registry_get_target() {
        let mut reg = Registry::default();

        let tr = TargetRegistry {
            name: "tmux".to_string(),
            real_path: PathBuf::from("/tmp"),
            profiles: vec!["work".to_string()],
        };

        reg.targets.insert("tmux".to_string(), tr.clone());

        let result = reg.get_target_registry("tmux");
        assert!(result.is_some());
    }

    #[test]
    fn test_registry_target_registry_error() {
        let reg = Registry::default();

        let err = reg.target_registry("missing").unwrap_err();
        let msg = format!("{}", err);

        assert!(msg.contains("Target missing not found"));
    }
}