krankerl 0.13.2

A CLI helper to manage Nextcloud apps
Documentation
use std::convert::Into;
use std::default::Default;
use std::fs;
use std::path::Path;

use color_eyre::{eyre::WrapErr, Report, Result};
use toml;

use super::{ConfigFileReader, ConfigReader};

#[derive(Debug, Deserialize)]
struct ParsedAppConfig {
    package: Option<ParsedPackageConfig>,
}

#[derive(Debug, Deserialize)]
struct ParsedPackageConfig {
    before_cmds: Option<Vec<String>>,
    exclude: Option<Vec<String>>,
}

#[derive(Debug)]
pub struct AppConfig {
    package: PackageConfig,
}

impl AppConfig {
    pub fn package(&self) -> &PackageConfig {
        &self.package
    }
}

impl Default for AppConfig {
    fn default() -> Self {
        AppConfig {
            package: PackageConfig::default(),
        }
    }
}

impl Into<AppConfig> for ParsedAppConfig {
    fn into(self) -> AppConfig {
        AppConfig {
            package: self
                .package
                .map(|pc| pc.into())
                .unwrap_or(PackageConfig::default()),
        }
    }
}

#[derive(Debug)]
pub struct PackageConfig {
    before_cmds: Vec<String>,
}

impl PackageConfig {
    pub fn before_cmds(&self) -> &Vec<String> {
        &self.before_cmds
    }
}

impl Into<PackageConfig> for ParsedPackageConfig {
    fn into(self) -> PackageConfig {
        if self.exclude.is_some() {
            panic!("The exclude array in krankerl.toml was removed in 0.12. Use a .nextcloudignore instead.")
        }
        PackageConfig {
            before_cmds: self.before_cmds.unwrap_or(vec![]),
        }
    }
}

impl Default for PackageConfig {
    fn default() -> Self {
        PackageConfig {
            before_cmds: vec![],
        }
    }
}

pub fn init_config(app_path: &Path) -> Result<()> {
    let config_path = app_path.join("krankerl.toml");

    if config_path.exists() {
        return Err(Report::msg("krankerl.toml already exists"));
    }

    fs::write(
        &config_path,
        r#"[package]
before_cmds = [

]
"#,
    )
    .wrap_err("Failed to write krankerl.toml")?;

    let ignore_path = app_path.join(".nextcloudignore");
    if !ignore_path.exists() {
        fs::write(
            &ignore_path,
            r#".drone
.git
.github
.gitignore
.scrutinizer.yml
.travis.yml
.tx
krankerl.toml
screenshots
.nextcloudignore
"#,
        )
        .wrap_err("Failed to write .nextcloudignore")?;
    }

    Ok(())
}

fn load_config<R>(reader: &R) -> Result<String>
where
    R: ConfigReader,
{
    reader.read()
}

fn parse_config(config: String) -> Result<ParsedAppConfig> {
    toml::from_str(&config).wrap_err("Failed to parse config as toml")
}

pub fn get_config(path: &Path) -> Result<Option<AppConfig>> {
    let mut path_buf = path.to_path_buf();
    path_buf.push("krankerl.toml");
    let reader = ConfigFileReader::new(path_buf);

    if !reader.has_config() {
        Ok(None)
    } else {
        let config_str = load_config(&reader)?;
        parse_config(config_str).map(|config| Some(config.into()))
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use fs_extra::dir::{copy, CopyOptions};
    use tempdir::TempDir;

    use super::*;

    struct StaticReader {}

    impl ConfigReader for StaticReader {
        fn has_config(&self) -> bool {
            true
        }

        fn read(&self) -> Result<String> {
            Ok("some config".to_owned())
        }
    }

    #[test]
    fn test_load_config() {
        let reader = StaticReader {};

        let conf = load_config(&reader).unwrap();

        assert_eq!("some config".to_owned(), conf);
    }

    fn prepare_fs_test(id: &'static str) -> (PathBuf, TempDir) {
        let mut src = PathBuf::from("./tests/apps");
        src.push(id);

        let tmp = TempDir::new("krankerl").unwrap();
        let options = CopyOptions::new();
        copy(&src, tmp.path(), &options).expect("copy app files");

        let mut app_path = tmp.path().to_path_buf();
        app_path.push(id);
        (app_path, tmp)
    }

    #[test]
    fn test_init_creates_config() {
        let (app_path, tmp) = prepare_fs_test("app1");

        let krankerl_path = app_path.join("krankerl.toml");
        assert!(!krankerl_path.exists());

        init_config(&app_path).unwrap();

        assert!(krankerl_path.exists());
        tmp.close().unwrap();
    }

    #[test]
    fn test_init_creates_ignore() {
        let (app_path, tmp) = prepare_fs_test("app1");

        let ignore_path = app_path.join(".nextcloudignore");
        assert!(!ignore_path.exists());

        init_config(&app_path).unwrap();

        assert!(ignore_path.exists());
        tmp.close().unwrap();
    }

    #[test]
    fn test_init_stops_if_config_exists() {
        let (app_path, tmp) = prepare_fs_test("app2");

        let krankerl_path = app_path.join("krankerl.toml");
        assert!(krankerl_path.exists());

        assert!(init_config(&app_path).is_err());

        assert!(krankerl_path.exists());
        tmp.close().unwrap();
    }

    #[test]
    fn test_init_doesnt_change_ignore() {
        let (app_path, tmp) = prepare_fs_test("app1");

        let ignore_path = app_path.join(".nextcloudignore");
        fs::write(&ignore_path, "dummy").unwrap();

        init_config(&app_path).unwrap();

        assert_eq!("dummy", fs::read_to_string(&ignore_path).unwrap());
        tmp.close().unwrap();
    }

    #[test]
    fn test_load_real_config() {
        let (app_path, tmp) = prepare_fs_test("app3");
        let config_path = app_path.join("krankerl.toml");
        let reader = ConfigFileReader::new(config_path);

        load_config(&reader).unwrap();

        tmp.close().unwrap();
    }

    #[test]
    fn test_parse_empty_config() {
        let toml = r#""#;

        let config = parse_config(toml.to_owned());

        assert!(config.is_ok());
    }

    #[test]
    fn test_parse_simple_config() {
        let toml = r#"
            [package]
            exclude = []
        "#;

        let config = parse_config(toml.to_owned());

        assert!(config.is_ok());
    }

    #[test]
    fn test_parse_config_without_commands() {
        let toml = r#"
            [package]
            exclude = [
                ".git",
                "composer.json",
                "composer.lock",
            ]
        "#;

        let config = parse_config(toml.to_owned());

        assert!(config.is_ok());
    }

    #[test]
    fn test_parse_config_with_commands() {
        let toml = r#"
        [package]
        before_cmds = [
            "composer install",
            "npm install",
            "npm run build",
        ]

        exclude = []"#;

        let config = parse_config(toml.to_owned());

        assert!(config.is_ok());
        let config = config.unwrap();
        assert!(config.package.is_some());
        let package_config = config.package.unwrap();
        assert!(package_config.before_cmds.is_some());
        let cmds = package_config.before_cmds.unwrap();
        assert_eq!(3, cmds.len());
    }
}