cargo-temp 0.4.0

Create temporary Rust project with specified dependencies
use crate::subprocess::SubProcess;
use anyhow::{Context, Result};
use serde::Deserialize;
use std::{fs, path::PathBuf};

#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Config {
    #[serde(default)]
    pub temporary_project_dir: Option<PathBuf>,
    #[serde(default)]
    pub welcome_message: bool,
    #[serde(default)]
    pub prompt: bool,
    #[serde(default)]
    pub preserved_project_dir: Option<PathBuf>,
    #[serde(default)]
    pub vcs: Option<String>,
    #[serde(default)]
    pub git_repo_depth: Option<Depth>,
    #[serde(default)]
    pub cargo_target_dir: Option<PathBuf>,
    #[serde(default)]
    pub editor: Option<String>,
    #[serde(default)]
    pub editor_args: Option<Vec<String>>,
    #[serde(default, rename = "subprocess", skip_serializing_if = "Vec::is_empty")]
    pub subprocesses: Vec<SubProcess>,
}

impl Config {
    fn template() -> Result<String> {
        let temporary_project_dir = Config::default_temporary_project_dir()?
            .to_str()
            .expect("path shouldn't contains invalid unicode")
            .replace('\\', "\\\\");

        Ok(format!(
            include_str!("../config_template.toml"),
            welcome_message = true,
            temporary_project_dir = temporary_project_dir,
        ))
    }

    #[cfg(unix)]
    pub(crate) fn default_temporary_project_dir() -> Result<PathBuf> {
        let base = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"));

        base.get_cache_home()
            .context("could not find HOME directory")
    }

    #[cfg(windows)]
    pub(crate) fn default_temporary_project_dir() -> Result<PathBuf> {
        Ok(dirs::cache_dir()
            .context("could not get cache directory")?
            .join(env!("CARGO_PKG_NAME")))
    }

    pub fn get_or_create() -> Result<Self> {
        #[cfg(unix)]
        let config_file_path = {
            let config_dir = xdg::BaseDirectories::with_prefix("cargo-temp");
            config_dir.place_config_file("config.toml")?
        };
        #[cfg(windows)]
        let config_file_path = {
            let config_dir = dirs::config_dir()
                .context("could not get config directory")?
                .join(env!("CARGO_PKG_NAME"));
            let _ = fs::create_dir_all(&config_dir);

            config_dir.join("config.toml")
        };

        let config: Self = match fs::read_to_string(&config_file_path) {
            Ok(file) => toml::de::from_str(&file)?,
            Err(_) => {
                let config_str = Config::template()?;

                fs::write(&config_file_path, &config_str)?;
                log::info!("Config file created at: {}", config_file_path.display());

                toml::de::from_str(&config_str)?
            }
        };

        Ok(config)
    }
}

#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum Depth {
    Active(bool),
    Level(u8),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn read_empty_config() {
        let _: Config = toml::from_str("").expect("can deserialize empty configuration");
    }

    #[test]
    fn from_template() {
        let template_str = Config::template().expect("can generate template");
        let template: Config = toml::de::from_str(&template_str).expect("can deserialize template");

        let default = Config {
            welcome_message: true,
            temporary_project_dir: Some(
                Config::default_temporary_project_dir()
                    .expect("can determine default temporary project directory"),
            ),
            ..Default::default()
        };

        assert_eq!(template, default);
    }
}