kcfg 0.1.0

KUBECONFIG manipulation CLI
use crate::common::check_directory_path;
use crate::error::KcfgError;
use clap::{ArgEnum, Clap, ValueHint};
use indoc::formatdoc;
use std::str::FromStr;

/// Options for the `init` command
#[derive(Clap, Debug)]
pub struct InitOptions {
    #[clap(arg_enum, default_value = "common")]
    /// target shell type
    pub shell_type: InitShellType,
    #[clap(short = 'p', long = "path", value_hint = ValueHint::DirPath)]
    /// Define a custom path. Otherwise the current path will be used
    pub custom_path: Option<String>,
}

/// Shell type you can run with the Init command
///
/// # Example
///
/// * kcfg init <SHELL-TYPE>
///
#[derive(Clap, Debug, ArgEnum)]
pub enum InitShellType {
    Zsh,
    Bash,
    Common,
}

impl FromStr for InitShellType {
    type Err = KcfgError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "zsh" => Ok(InitShellType::Zsh),
            "bash" => Ok(InitShellType::Bash),
            "common" => Ok(InitShellType::Common),
            _ => Err(KcfgError::NotSupportedShellType(s.to_string())),
        }
    }
}

/// Match with the shell type the User has
/// entered for `kcfg init`
///
/// # Arguments
///
/// * `params` - options for the `init` command
///
/// # Returns
///
/// A `String` containing the produced code is returned on success.
/// An error is returned if the current path is unavailable (FileSystem error) or invalid.
///
pub fn init(params: InitOptions) -> Result<String, KcfgError> {
    let path = match params.custom_path {
        None => {
            let current_path = std::env::current_exe()?;
            current_path
                .to_str()
                .ok_or_else(|| KcfgError::InvalidPath(current_path.clone()))?
                .to_string()
        }
        Some(custom_path) => {
            check_directory_path(&custom_path)?;
            custom_path
        }
    };
    Ok(match params.shell_type {
        InitShellType::Common => init_common_full(&path),
        InitShellType::Zsh => init_zsh(&path),
        InitShellType::Bash => init_bash(&path),
    })
}

/// # Arguments
///
/// `current_path` - string slice that contains the current path
///
/// # Returns
///
/// Return a string that contains the shell function to use in the `Common` case
///
fn init_common_full(current_path: &str) -> String {
    return formatdoc! {"
        function kcfg() {{
          result=$({cmd} $@)
          if [[ $result = 'export '* ]]
          then
            eval $result
          else
            echo $result
          fi
        }}
        ",
        cmd = current_path
    };
}

/// # Arguments
///
/// `current_path` - string slice that contains the current path
///
/// # Returns
///
/// Return a string that contains the shell command to use in the `zsh` case
///
fn init_zsh(current_path: &str) -> String {
    formatdoc!("source <({} init zsh --print-full-init)", current_path)
}

/// # Arguments
///
/// `current_path` - string slice that contains the current path
///
/// # Returns
///
/// Return a string that contains the shell code to use in the `bash` case
///
fn init_bash(current_path: &str) -> String {
    formatdoc! {"
        __main() {{
            local major=\"${{BASH_VERSINFO[0]}}\"
            local minor=\"${{BASH_VERSINFO[1]}}\"

            if ((major > 4)) || {{ ((major == 4)) && ((minor >= 1)); }}; then
                source <(\"{cmd}\" init bash --print-full-init)
            else
                source /dev/stdin <<<\"$(\"{cmd}\" init bash --print-full-init)\"
            fi
        }}
        __main
        unset -f __main
        ",
        cmd = current_path
    }
}

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

    mod router {
        use super::*;

        #[test]
        fn can_fail_with_wrong_path() {
            let params = InitOptions {
                shell_type: InitShellType::Bash,
                custom_path: Some("toto".to_string()),
            };
            match init(params) {
                Ok(_) => panic!("Test should have failed"),
                Err(e) => {
                    if !matches!(e, KcfgError::PathDoesNotExist(_)) {
                        panic!("Test failed with wrong error");
                    }
                }
            }
        }

        #[test]
        fn can_fail_with_wrong_directory() {
            let params = InitOptions {
                shell_type: InitShellType::Bash,
                custom_path: Some("./Cargo.toml".to_string()),
            };
            match init(params) {
                Ok(_) => panic!("Test should have failed"),
                Err(e) => {
                    if !matches!(e, KcfgError::WrongDirectory(_)) {
                        panic!("Test failed with wrong error");
                    }
                }
            }
        }

        #[test]
        fn can_succeed_with_bash() {
            let params = InitOptions {
                shell_type: InitShellType::Bash,
                custom_path: Some("/tmp".to_string()),
            };
            let res = init(params).unwrap();
            assert_eq!(
                formatdoc! {"
                    __main() {{
                        local major=\"${{BASH_VERSINFO[0]}}\"
                        local minor=\"${{BASH_VERSINFO[1]}}\"

                        if ((major > 4)) || {{ ((major == 4)) && ((minor >= 1)); }}; then
                            source <(\"/tmp\" init bash --print-full-init)
                        else
                            source /dev/stdin <<<\"$(\"/tmp\" init bash --print-full-init)\"
                        fi
                    }}
                    __main
                    unset -f __main
                "},
                res,
            )
        }

        #[test]
        fn can_succeed_with_zsh() {
            let params = InitOptions {
                shell_type: InitShellType::Zsh,
                custom_path: Some("/tmp".to_string()),
            };
            let res = init(params).unwrap();
            assert_eq!("source <(/tmp init zsh --print-full-init)".to_string(), res,)
        }

        #[test]
        fn can_succeed_with_common() {
            let params = InitOptions {
                shell_type: InitShellType::Common,
                custom_path: Some("/tmp".to_string()),
            };
            let res = init(params).unwrap();
            assert_eq!(
                formatdoc! {"
                    function kcfg() {{
                      result=$(/tmp $@)
                      if [[ $result = 'export '* ]]
                      then
                        eval $result
                      else
                        echo $result
                      fi
                    }}
                "},
                res,
            )
        }
    }

    mod init {
        use super::*;

        #[test]
        fn test_init_common_full() {
            assert_eq!(
                formatdoc! {"
                    function kcfg() {{
                      result=$(test $@)
                      if [[ $result = 'export '* ]]
                      then
                        eval $result
                      else
                        echo $result
                      fi
                    }}
                "},
                init_common_full("test")
            );
        }

        #[test]
        fn test_init_zsh() {
            assert_eq!(
                "source <(test init zsh --print-full-init)".to_string(),
                init_zsh("test")
            );
        }

        #[test]
        fn test_init_bash() {
            assert_eq!(
                formatdoc! {"
                    __main() {{
                        local major=\"${{BASH_VERSINFO[0]}}\"
                        local minor=\"${{BASH_VERSINFO[1]}}\"

                        if ((major > 4)) || {{ ((major == 4)) && ((minor >= 1)); }}; then
                            source <(\"test\" init bash --print-full-init)
                        else
                            source /dev/stdin <<<\"$(\"test\" init bash --print-full-init)\"
                        fi
                    }}
                    __main
                    unset -f __main
                "},
                init_bash("test")
            );
        }
    }
}