ghpending 0.1.1

CLI to watch GitHub repos for open issues and pull requests at a glance
use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
    pub user: Option<String>,
    #[serde(default)]
    pub repos: Vec<String>,
}

fn config_path() -> Result<PathBuf> {
    let proj =
        ProjectDirs::from("", "", "ghpending").context("could not determine config directory")?;
    Ok(proj.config_dir().join("config.toml"))
}

pub fn load() -> Result<Config> {
    let path = config_path()?;
    if !path.exists() {
        return Ok(Config::default());
    }
    let text =
        std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
    let cfg: Config =
        toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
    Ok(cfg)
}

pub fn save(cfg: &Config) -> Result<()> {
    let path = config_path()?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("creating dir {}", parent.display()))?;
    }
    let text = toml::to_string(cfg).context("serializing config")?;
    std::fs::write(&path, &text).with_context(|| format!("writing {}", path.display()))?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(&path)?.permissions();
        perms.set_mode(0o600);
        std::fs::set_permissions(&path, perms)?;
    }

    Ok(())
}

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

    #[test]
    fn round_trip_with_user() {
        let cfg = Config {
            user: Some("octocat".into()),
            repos: vec!["owner/repo".into(), "foo/bar".into()],
        };
        let s = toml::to_string(&cfg).unwrap();
        let back: Config = toml::from_str(&s).unwrap();
        assert_eq!(back.user.as_deref(), Some("octocat"));
        assert_eq!(back.repos, vec!["owner/repo", "foo/bar"]);
    }

    #[test]
    fn round_trip_user_none() {
        let cfg = Config {
            user: None,
            repos: vec!["owner/repo".into()],
        };
        let s = toml::to_string(&cfg).unwrap();
        let back: Config = toml::from_str(&s).unwrap();
        assert!(back.user.is_none());
        assert_eq!(back.repos, vec!["owner/repo"]);
    }

    #[test]
    fn default_on_missing_file() {
        let cfg: Config = toml::from_str("").unwrap();
        assert!(cfg.user.is_none());
        assert!(cfg.repos.is_empty());
    }
}