nedots 0.1.2

A tool to manage configuration files/(ne)dots.
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct Config {
    pub(crate) root: PathBuf,
    pub(crate) dots_dir: PathBuf,
    pub(crate) backup_dir: PathBuf,
    pub(crate) remote: String,
    pub(crate) symlink: bool,
    pub(crate) sources: Vec<PathBuf>,
    pub(crate) git_repos: Vec<GitRepo>,
    pub(crate) systemd_services: Vec<String>,
}

#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct GitRepo {
    pub(crate) id: String,
    pub(crate) remote: String,
    pub(crate) path: PathBuf,
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum ConfigError {
    #[error("`{0}`: Path does not exist")]
    PathResolutionError(String),

    #[error(transparent)]
    IoError(#[from] std::io::Error),

    #[error(transparent)]
    SerdeError(#[from] serde_yaml::Error),
}

pub(crate) fn read(path: &Path) -> Result<Config, ConfigError> {
    let path = match path.resolve_path() {
        Ok(pb) => pb,
        Err(err) => return Err(err),
    };

    let file = match std::fs::read_to_string(path) {
        Ok(file) => file,
        Err(err) => return Err(ConfigError::IoError(err)),
    };

    match serde_yaml::from_str(&file) {
        Ok(config) => Ok(config),
        Err(err) => Err(ConfigError::SerdeError(err)),
    }
}

pub(crate) trait ResolvePath {
    fn resolve_path(&self) -> Result<PathBuf, ConfigError>;
}

impl ResolvePath for Path {
    fn resolve_path(&self) -> Result<PathBuf, ConfigError> {
        self.to_path_buf().resolve_path()
    }
}

impl ResolvePath for PathBuf {
    fn resolve_path(&self) -> Result<PathBuf, ConfigError> {
        if !self.as_path().exists() {
            match Path::new(env!("HOME")).join(self.as_path()).canonicalize() {
                Ok(pb) => return Ok(pb),
                Err(_) => {
                    return Err(ConfigError::PathResolutionError(
                        self.to_string_lossy().to_string(),
                    ))
                }
            }
        }

        Ok(self.to_path_buf())
    }
}

impl Config {
    pub(crate) fn resolve_paths(mut self) -> Config {
        match self.root.resolve_path() {
            Ok(path) => {
                log::debug!("{} resolved", self.root.to_string_lossy());
                self.root = path;
            }
            Err(err) => log::error!("{}: {}", self.root.to_string_lossy(), err),
        }

        self.dots_dir = self.root.join(self.dots_dir);
        match self.dots_dir.resolve_path() {
            Ok(path) => {
                log::debug!("{} resolved", self.dots_dir.to_string_lossy());
                self.dots_dir = path;
            }
            Err(err) => log::error!("{}: {}", self.dots_dir.to_string_lossy(), err),
        }

        self.backup_dir = self.root.join(self.backup_dir);
        match self.backup_dir.resolve_path() {
            Ok(path) => {
                log::debug!("{} resolved", self.backup_dir.to_string_lossy());
                self.backup_dir = path;
            }
            Err(err) => log::error!("{}: {}", self.backup_dir.to_string_lossy(), err),
        }

        self.sources = self
            .sources
            .into_iter()
            .map(|s| match s.resolve_path() {
                Ok(path) => {
                    log::debug!("{} resolved", path.to_string_lossy());
                    path
                }
                Err(err) => {
                    log::error!("{}: {}", s.to_string_lossy(), err);
                    Path::new("").to_path_buf()
                }
            })
            .filter(|s| s.ne(Path::new("")))
            .collect();

        self.git_repos = self
            .git_repos
            .into_iter()
            .map(|mut gr| match gr.path.resolve_path() {
                Ok(path) => {
                    log::debug!("{} resolved", path.to_string_lossy());
                    gr.path = path;
                    gr
                }
                Err(err) => {
                    log::error!("{}: {}", gr.path.to_string_lossy(), err);
                    gr.path = Path::new("").to_path_buf();
                    gr
                }
            })
            .filter(|gr| gr.path.ne(Path::new("")))
            .collect();

        self
    }

    pub(crate) fn get_dst_from_src(
        &self,
        src: &Path,
    ) -> Result<PathBuf, std::path::StripPrefixError> {
        Ok(self.dots_dir.join(src.strip_prefix("/")?))
    }

    pub(crate) fn get_sources_as_hashmap(&self) -> HashMap<&str, PathBuf> {
        let mut all_parts: Vec<&str> = Vec::new();
        for pb in &self.sources {
            let _: Vec<&str> = pb
                .to_str()
                .unwrap_or("")
                .split('/')
                .map(|part| {
                    all_parts.push(part);
                    part
                })
                .collect();
        }

        all_parts.sort();
        all_parts.dedup_by(|a, b| a.eq(&b));

        let mut hash_map: HashMap<&str, PathBuf> = HashMap::new();
        for part in &all_parts {
            if *part != "" {
                hash_map.insert(
                    part,
                    self.sources
                        .iter()
                        .find(|s| s.to_str().unwrap_or("").contains(part))
                        .unwrap()
                        .to_path_buf(),
                );
            }
        }

        log::debug!("Sources as HashMap: {:#?}", &hash_map);
        hash_map
    }
}