cargo-blinc 0.1.2

Notifies about commands exit code using LED light blink(1)
use crate::colors::Colors;
use crate::task::Task;
use anyhow::Result;
use log::debug;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::Path;
use transition::Led;

#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct Config {
    #[serde(rename = "task")]
    tasks: Vec<Task>,
    colors: Colors,
    env: Option<HashMap<String, String>>,
}

impl Config {
    pub(crate) fn get<A: AsRef<Path>>(path: A) -> Result<Self> {
        let path = path.as_ref();
        if Path::new(path).exists() {
            debug!("config file exists, loading from path {:?}", path);
            Ok(Self::load(path)?)
        } else {
            debug!("no config file, using default configuration");
            Ok(Self::default())
        }
    }

    fn load<A: AsRef<Path>>(path: A) -> Result<Self> {
        Self::read(&mut File::open(path.as_ref())?)
    }

    pub(crate) fn read<R: Read>(read: &mut R) -> Result<Self> {
        let mut config_content = String::new();
        read.read_to_string(&mut config_content)?;
        debug!("read config {}", config_content);
        let config = toml::from_str(&config_content)?;
        debug!("created config struct: {:?}", config);
        Ok(config)
    }

    pub(crate) fn store<A: AsRef<Path>>(&self, path: A) -> Result<()> {
        debug!(
            "storing config: {:?} under path {:?} with name {:?}",
            self,
            env::current_dir()?,
            path.as_ref()
        );
        let mut config_file = OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(path)?;
        self.write(&mut config_file)?;
        Ok(())
    }

    fn write<W: Write>(&self, write: &mut W) -> Result<()> {
        write.write_all(toml::to_string(&self)?.as_bytes())?;
        debug!("config written");
        Ok(())
    }

    pub(crate) fn tasks(&self) -> &Vec<Task> {
        &self.tasks
    }

    pub(crate) fn env(&self) -> &Option<HashMap<String, String>> {
        &self.env
    }

    pub(crate) fn pending(&self) -> &[Led] {
        self.colors.pending()
    }

    pub(crate) fn failure(&self) -> &Led {
        self.colors.failure()
    }

    pub(crate) fn success(&self) -> &Led {
        self.colors.success()
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            tasks: vec![
                Task::new("cargo", &["check"]),
                Task::new("cargo", &["test"]),
            ],
            colors: Colors::new(vec![Led::Blue, Led::Blank], Led::Red, Led::Green),
            env: Some(HashMap::new()),
        }
    }
}

#[cfg(test)]
mod test {
    use super::Config;
    use crate::testutils::{init_logger, ReaderStub, WriterMock};
    use crate::Result;
    use transition::Led;

    #[test]
    fn test_load_config_with_valid_config() -> Result<()> {
        init_logger();
        let config_content = r#"
            [[task]]
            cmd = "cargo"
            args = ["check"]

            [[task]]
            cmd = "cargo"
            args = ["test"]

            [colors]
            pending = ["blue", "blank"]
            failure = "red"
            success = "green"
        "#
        .to_string();

        let c = Config::read(&mut ReaderStub::new(config_content))?;

        assert_eq!(c.pending()[0], Led::Blue, "Testing transition");
        assert_eq!(c.pending()[1], Led::Blank, "Testing transition");
        assert_eq!(
            c.tasks().first().unwrap().command(),
            "cargo",
            "Testing first task command"
        );
        assert_eq!(
            c.tasks().first().unwrap().args(),
            vec!["check"],
            "Testing first task arguments"
        );
        assert_eq!(c.failure(), &Led::Red, "Testing failure color");
        assert_eq!(c.success(), &Led::Green, "Testing success color");

        Ok(())
    }

    #[test]
    #[should_panic]
    fn test_tasks_config_with_lack_of_cmd_key() {
        init_logger();
        let config_content = r#"
            [[task]]
            args = ["check"]

            [[task]]
            cmd = "cargo"
            args = ["test"]

            [colors]
            pending = ["blue", "blank"]
            failure = "red"
            success = "green"
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_tasks_config_with_empty_tasks_key() {
        init_logger();
        let config_content = r#"
            [[task]]

            [colors]
            pending = ["blue", "blank"]
            failure = "red"
            success = "green"
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_tasks_config_with_lack_of_tasks() {
        init_logger();
        let config_content = r#"
            [colors]
            pending = ["blue", "blank"]
            failure = "red"
            success = "green"
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    fn test_tasks_config_with_lack_of_optional_args_key() -> Result<()> {
        init_logger();
        let config_content = r#"
            [[task]]
            cmd = "cargo"

            [[task]]
            cmd = "cargo"

            [colors]
            pending = ["blue", "blank"]
            failure = "red"
            success = "green"
        "#
        .to_string();
        let c = Config::read(&mut ReaderStub::new(config_content))?;

        assert_eq!(c.pending()[0], Led::Blue, "Testing transition");
        assert_eq!(c.pending()[1], Led::Blank, "Testing transition");
        assert_eq!(
            c.tasks().first().unwrap().command(),
            "cargo",
            "Testing first task command"
        );
        assert_eq!(
            c.tasks().first().unwrap().args(),
            Vec::<String>::new(),
            "Testing first task arguments"
        );
        assert_eq!(c.failure(), &Led::Red, "Testing failure color");
        assert_eq!(c.success(), &Led::Green, "Testing success color");

        Ok(())
    }

    #[test]
    #[should_panic]
    fn test_colors_config_with_lack_of_pending_key() {
        init_logger();
        let config_content = r#"
            [[task]]
            cmd = "cargo"
            args = ["check"]

            [[task]]
            cmd = "cargo"
            args = ["test"]

            [colors]
            failure = "red"
            success = "green"
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_colors_config_with_lack_of_failure_key() {
        init_logger();
        let config_content = r#"
            [[task]]
            cmd = "cargo"
            args = ["check"]

            [[task]]
            cmd = "cargo"
            args = ["test"]

            [colors]
            pending = ["blue", "blank"]
            success = "green"
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_colors_config_with_lack_of_success_key() {
        init_logger();
        let config_content = r#"
            [[task]]
            cmd = "cargo"
            args = ["check"]

            [[task]]
            cmd = "cargo"
            args = ["test"]

            [colors]
            pending = ["blue", "blank"]
            failure = "red"
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_colors_config_with_lack_of_colors_key() {
        init_logger();
        let config_content = r#"
            [[task]]
            cmd = "cargo"
            args = ["check"]

            [[task]]
            cmd = "cargo"
            args = ["test"]
        "#
        .to_string();
        Config::read(&mut ReaderStub::new(config_content)).unwrap();
    }

    #[test]
    fn test_store_config() -> Result<()> {
        init_logger();
        let config_content = r#"[[task]]
cmd = "cargo"
args = ["check"]

[[task]]
cmd = "cargo"
args = ["test"]

[colors]
pending = ["blue", "blank"]
failure = "red"
success = "green"

[env]
"#
        .to_string();

        let mut writer = WriterMock::new(config_content);
        Config::default().write(&mut writer)?;

        assert!(writer.all_config_written(), "Testing writing config");

        Ok(())
    }
}