rslack 0.2.7

cli for posting slack message
Documentation
use anyhow::{anyhow, Result};
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

const RSLACK_TOKEN: &str = "RSLACK_TOKEN";

#[derive(Debug, PartialEq)]
pub struct Config {
    pub token: String,
}

impl Config {
    pub fn new(filename: &str) -> Result<Self> {
        let mut config = Self {
            token: String::new(),
        };

        config.read_from_env()?;

        if Path::new(filename).exists() {
            config.read_from_file(filename)?;
        }

        config.validate()?;

        Ok(config)
    }

    #[allow(clippy::single_match)]
    fn read_from_env(&mut self) -> Result<&Self> {
        match env::var(RSLACK_TOKEN) {
            Ok(token) => self.token = token,
            Err(_) => {}
        }

        Ok(self)
    }

    #[allow(clippy::single_match)]
    fn read_from_file(&mut self, filename: &str) -> Result<&Self> {
        let file = match File::open(filename) {
            Ok(file) => file,
            Err(err) => return Err(anyhow!(err)),
        };

        for line in BufReader::new(file).lines() {
            if let Ok(line) = line {
                let entries: Vec<_> = line.split('=').map(str::trim).collect();

                if entries.len() == 2 {
                    let (key, val) = (entries[0].trim(), entries[1].trim().to_string());

                    match key {
                        RSLACK_TOKEN => self.token = val,
                        _ => {}
                    }
                }
            }
        }

        Ok(self)
    }

    fn validate(&mut self) -> Result<()> {
        if self.token.is_empty() {
            return Err(anyhow!("{} not found.", RSLACK_TOKEN));
        }

        Ok(())
    }
}

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

    fn setup() {
        env::remove_var(RSLACK_TOKEN);
    }

    #[test]
    #[serial]
    fn initialize_with_valid_file() {
        setup();
        let actual = Config::new("tests/token.test.valid").unwrap();
        let expected = Config {
            token: String::from("token-from-file-123"),
        };
        assert_eq!(actual, expected);
    }

    #[test]
    #[serial]
    #[should_panic]
    fn initialize_with_invalid_file() {
        setup();
        Config::new("tests/token.test.invalid").unwrap();
    }

    #[test]
    #[serial]
    fn initialize_with_env() {
        setup();
        env::set_var(RSLACK_TOKEN, "token-from-env-123");
        let actual = Config::new("no_file").unwrap();
        let expected = Config {
            token: String::from("token-from-env-123"),
        };
        assert_eq!(actual, expected);
    }

    #[test]
    #[serial]
    #[should_panic]
    fn initialize_with_empty_env() {
        setup();
        env::set_var(RSLACK_TOKEN, "");
        Config::new("no_file").unwrap();
    }

    #[test]
    #[serial]
    fn initialize_with_env_and_file() {
        setup();
        env::set_var(RSLACK_TOKEN, "token-from-env-123");
        let expected = Config {
            token: String::from("token-from-file-123"),
        };
        let actual = Config::new("tests/token.test.valid").unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn valid_token() {
        let mut config = Config {
            token: String::from("token"),
        };
        let actual = config.validate();
        assert!(actual.is_ok())
    }

    #[test]
    fn invalid_token() {
        let mut config = Config {
            token: String::new(),
        };
        let actual = config.validate();
        assert!(actual.is_err())
    }
}