kcfg 0.2.0

KUBECONFIG manipulation CLI
use crate::common::check_file_path;
use crate::error::KcfgError;
use clap::{Parser, ValueEnum, ValueHint};
use indoc::formatdoc;

/// Options for the `init` command
#[derive(Clone, Parser, Debug)]
pub struct InitOptions {
    #[clap(value_enum)]
    /// target shell type
    pub shell_type: Option<InitShellType>,
    #[clap(short = 'p', long = "path", value_hint = ValueHint::DirPath)]
    /// Defines a custom path to kcfg program, otherwise the current filesystem path to kcfg will be used
    pub custom_path: Option<String>,
    /// Print for shell double evaluation
    #[clap(short, long)]
    pub full: bool,
}

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

/// 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_file_path(&custom_path)?;
            custom_path
        }
    };
    let res = match params.shell_type {
        Some(shell_type) => match shell_type {
            InitShellType::Bash => init_bash(&path, params.full),
            InitShellType::Zsh => init_zsh(&path, params.full),
        },
        None => return Err(KcfgError::MissingShellType),
    };
    Ok(res)
}

/// # Arguments
///
/// `current_path` - string slice that contains the current path
///
/// # Returns
///
/// Return a string that contains the shell command to use in the `full` `zsh` or `bash` case
///
fn init_full_zsh_or_bash(current_path: &str) -> String {
    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_simple_zsh(current_path: &str) -> String {
    format!("source <({} init zsh --full)", 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_simple_bash(current_path: &str) -> String {
    formatdoc! {"
        __kcfg() {{
            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 --full)\"
            fi
        }}
        __kcfg
        unset -f __kcfg
        ",
        cmd = current_path
    }
}

/// # Arguments
///
/// `current_path` - string slice that contains the current path
/// `is_full` - bool for full command. Double eval.
///
/// # Returns
///
/// Return a string that contains the shell command to use in the `zsh` case, with double eval if `is_full`is true
///
fn init_zsh(current_path: &str, is_full: bool) -> String {
    if is_full {
        init_full_zsh_or_bash(current_path)
    } else {
        init_simple_zsh(current_path)
    }
}

/// # Arguments
///
/// `current_path` - string slice that contains the current path
/// `is_full` - bool for full command. Double eval.
///
/// # Returns
///
/// Return a string that contains the shell code to use in the `bash` case. Double eval if `is_full`is true
///
fn init_bash(current_path: &str, is_full: bool) -> String {
    if is_full {
        init_full_zsh_or_bash(current_path)
    } else {
        init_simple_bash(current_path)
    }
}

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

    mod router {
        use super::*;

        #[test]
        fn can_fail_with_wrong_path() {
            let params = InitOptions {
                shell_type: Some(InitShellType::Bash),
                custom_path: Some("toto".to_string()),
                full: false,
            };
            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_filr() {
            let params = InitOptions {
                shell_type: Some(InitShellType::Bash),
                custom_path: Some("src".to_string()),
                full: false,
            };
            match init(params) {
                Ok(_) => panic!("Test should have failed"),
                Err(e) => {
                    if !matches!(e, KcfgError::WrongFile(_)) {
                        panic!("Test failed with wrong error");
                    }
                }
            }
        }

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

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

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

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

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

        #[test]
        fn can_fail_without_shell_type() {
            let params = InitOptions {
                shell_type: None,
                custom_path: Some("src/main.rs".to_string()),
                full: false,
            };
            match init(params) {
                Ok(_) => panic!("Test should have failed"),
                Err(e) => {
                    if !matches!(e, KcfgError::MissingShellType) {
                        panic!("Test failed with wrong error");
                    }
                }
            }
        }
    }

    mod init {
        use super::*;

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

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

        #[test]
        fn test_init_bash() {
            assert_eq!(
                formatdoc! {"
                    __kcfg() {{
                        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 --full)\"
                        fi
                    }}
                    __kcfg
                    unset -f __kcfg
                "},
                init_bash("test", false)
            );
        }

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