pkgs-cli 0.2.0

A simple cli tool to manage packages.
Documentation
use std::path::Path;

use super::{Config, PkgsParseError, VarMap};
use crate::config::{Package, PackageType};

impl Config {
    pub fn get(&self, name: &str) -> Result<NamedPackage, PkgsParseError> {
        NamedPackage::try_new(
            name,
            self.packages[name].clone(),
            VarMap::try_new(&self.vars)?, // PERF: varmap will be built multiple times here
        )
    }
}

#[derive(Debug)]
pub struct NamedPackage {
    name: String,
    kind: PackageType,
    maps: Vec<(String, String)>,
}

impl NamedPackage {
    pub fn try_new(name: &str, package: Package, mut vars: VarMap) -> Result<Self, PkgsParseError> {
        vars.extends(&package.vars)?;

        let maps = package
            .maps
            .into_iter()
            .map(|(k, v)| {
                let mut v = vars.parse(&v)?;

                let k_path = Path::new(&k);
                if v.ends_with('/') {
                    v.push_str(
                        k_path
                            .file_name()
                            .ok_or_else(|| PkgsParseError::NoneFilename(k.clone()))?
                            .to_string_lossy()
                            .as_ref(),
                    );
                }

                Ok((k, v))
            })
            .collect::<Result<Vec<_>, PkgsParseError>>()?;

        Ok(Self {
            name: name.to_string(),
            kind: package.kind,
            maps,
        })
    }

    pub fn get_directory(&self) -> String {
        match self.kind() {
            PackageType::Local => self.name.to_string(),
        }
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn kind(&self) -> PackageType {
        self.kind
    }

    pub fn maps(&self) -> &[(String, String)] {
        &self.maps
    }

    #[cfg(test)]
    pub fn insert_map(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) {
        self.maps
            .push((key.as_ref().to_string(), value.as_ref().to_string()));
    }

    #[cfg(test)]
    pub fn remove_map(&mut self, key: impl AsRef<str>) {
        self.maps.retain(|(k, _)| k.as_str() != key.as_ref());
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::*;
    use crate::{fs::home_dir, test_utils::prelude::*};

    fn setup() -> Config {
        let vars = vec![
            ("APP_DIR".to_string(), "${HOME}/myapp".to_string()),
            ("MY_VAR1".to_string(), "hello".to_string()),
            ("MY_VAR2".to_string(), "${MY_VAR1}_world".to_string()),
        ];

        let packages = BTreeMap::from_iter([(
            "test_pkg".to_string(),
            Package {
                kind: PackageType::Local,
                vars: vec![],
                maps: vec![
                    ("app_dir".to_string(), "${APP_DIR}".to_string()),
                    ("path".to_string(), "/usr/local/${MY_VAR2}".to_string()),
                    ("config".to_string(), "${MY_VAR1}_config".to_string()),
                ],
            },
        )]);

        Config { vars, packages }
    }

    #[gtest]
    fn it_works() -> Result<()> {
        let config = setup();

        let pkg = config.get("test_pkg")?;
        expect_eq!(pkg.name(), "test_pkg");
        expect_eq!(pkg.kind(), PackageType::Local);
        expect_eq!(pkg.get_directory(), "test_pkg");

        expect_eq!(
            *pkg.maps(),
            [
                (
                    "app_dir".into(),
                    home_dir().join("myapp").to_str().unwrap().into()
                ),
                ("path".into(), "/usr/local/hello_world".into()),
                ("config".into(), "hello_config".into())
            ]
        );

        Ok(())
    }

    #[gtest]
    fn local_vars() -> Result<()> {
        let mut config = setup();
        config
            .packages
            .get_mut("test_pkg")
            .unwrap()
            .vars
            .push(("MY_VAR1".to_string(), "hi".to_string()));

        let pkg = config.get("test_pkg")?;
        expect_eq!(
            *pkg.maps(),
            [
                (
                    "app_dir".into(),
                    home_dir().join("myapp").to_str().unwrap().into()
                ),
                ("path".into(), "/usr/local/hello_world".into()),
                ("config".into(), "hi_config".into())
            ]
        );

        Ok(())
    }

    #[gtest]
    fn unknown_var_when_build() -> Result<()> {
        let mut config = setup();
        config
            .vars
            .push(("MY_VAR3".to_string(), "${UNKNOWN}".to_string()));

        let err = config.get("test_pkg").unwrap_err();
        expect_that!(err, pat!(PkgsParseError::VarsBuild(_)));

        Ok(())
    }

    #[gtest]
    fn unknown_var_when_parse() -> Result<()> {
        let mut config = setup();
        config
            .packages
            .get_mut("test_pkg")
            .unwrap()
            .maps
            .push(("bad".to_string(), "${UNKNOWN}".to_string()));

        let err = config.get("test_pkg").unwrap_err();
        expect_that!(err, pat!(PkgsParseError::VarsParse(_)));

        Ok(())
    }

    mod trailing_slash {
        use super::*;

        fn setup(src: &str, dst: &str) -> Config {
            let vars = vec![
                ("APP_DIR".to_string(), "${HOME}/myapp".to_string()),
                ("MY_VAR1".to_string(), "hello/".to_string()),
            ];

            let packages = BTreeMap::from_iter([(
                "test_pkg".to_string(),
                Package {
                    kind: PackageType::Local,
                    vars: vec![],
                    maps: vec![(src.to_string(), dst.to_string())],
                },
            )]);

            Config { vars, packages }
        }

        #[gtest]
        fn common_trailing_slash() -> Result<()> {
            let src = "path/to/trailing_slash";
            let config = setup(src, "/usr/bin/");
            let pkg = config.get("test_pkg")?;

            expect_eq!(
                *pkg.maps(),
                [(src.into(), "/usr/bin/trailing_slash".into())]
            );

            Ok(())
        }

        #[gtest]
        fn no_trailing_slash() -> Result<()> {
            let src = "path/to/no_trailing_slash";
            let config = setup(src, "/usr/local/bin");
            let pkg = config.get("test_pkg")?;

            expect_eq!(*pkg.maps(), [(src.into(), "/usr/local/bin".into())]);

            Ok(())
        }

        #[gtest]
        fn trailing_slash_in_var() -> Result<()> {
            let src = "path/to/trailing_var";
            let config = setup(src, "${MY_VAR1}");
            let pkg = config.get("test_pkg")?;

            expect_eq!(*pkg.maps(), [(src.into(), "hello/trailing_var".into())]);

            Ok(())
        }

        #[gtest]
        fn no_filename() -> Result<()> {
            let src = "no_filename/..";
            let config = setup(src, "/usr/bin/");
            let err = config.get("test_pkg").unwrap_err();

            expect_that!(err, pat!(PkgsParseError::NoneFilename(src)));

            Ok(())
        }
    }

    mod local_vars {}
}