bitbucket_server_cli 0.4.5

A CLI for cloning and updating entire projects from bitbucket server
Documentation
use std::path::Path;

use clap::Shell;
use dialoguer::{Confirm, Input, Select};
use generic_error::Result;
use structopt::StructOpt;

use crate::types::Opts;
use crate::util::bail;

#[cfg(target_os = "windows")]
const HOME_VAR: &str = "USERPROFILE";
#[cfg(not(target_os = "windows"))]
const HOME_VAR: &str = "HOME";

pub fn gen_completions() -> Result<()> {
    let mut app: clap::App = Opts::clap();
    if confirm("Do you want to generate completions for bitbucket_server_cli?")? {
        let shell = get_shell_selection()?;
        let output_dir = get_output_dir(shell)?;
        if confirm(&format!(
            "Do you want to generate {} completions into {}",
            shell, output_dir
        ))? {
            app.gen_completions("bitbucket_server_cli", shell, output_dir);
        }
    }
    Ok(())
}

fn confirm(question: &str) -> Result<bool> {
    match Confirm::new()
        .default(false)
        .with_prompt(question)
        .interact()
    {
        Ok(ans) => Ok(ans),
        Err(e) => bail(&format!(
            "Failed getting answer for question due to {:?}",
            e
        )),
    }
}

fn create_dir_if_necessary(output_dir: &str) -> Result<()> {
    if !Path::new(output_dir).exists() {
        match Confirm::new()
            .with_prompt(format!(
                "Output directory does not exist. Do you want to create '{}' ?",
                output_dir
            ))
            .default(false)
            .interact()
        {
            Ok(ans) if ans => match std::fs::create_dir_all(output_dir) {
                Ok(_) => {
                    println!("Directory created.");
                    Ok(())
                }
                Err(e) => bail(&format!(
                    "Failed creating dir {} due to {:?}",
                    output_dir, e
                )),
            },
            Ok(_ans) => {
                bail("Cannot proceed writing to output_directory without creating it first.")
            }
            Err(e) => bail(&format!("Failed reading input due to {:?}", e)),
        }
    } else {
        Ok(())
    }
}

fn get_output_dir(shell: Shell) -> Result<String> {
    let mut input = Input::new();
    if let Some(output_dir_default) = get_default_completion_location(shell) {
        input.default(output_dir_default);
    }
    match input.with_prompt("Output directory").interact() {
        Ok(output_dir) => {
            create_dir_if_necessary(&output_dir)?;
            Ok(output_dir)
        }
        Err(e) => bail(&format!("Failed interpreting prompt due to {:?}", e)),
    }
}

fn get_shell_selection() -> Result<Shell> {
    let variants1 = clap::Shell::variants();
    match Select::new()
        .with_prompt("Shell")
        .default(0)
        .items(&variants1)
        .interact()
    {
        Ok(shell_idx) => {
            let shell_str = variants1[shell_idx];
            match shell_str.parse() {
                Ok(shell) => Ok(shell),
                Err(e) => bail(&format!("Failed parsing shell selection due to {}", e)),
            }
        }
        Err(e) => bail(&format!("Failed determining selection due to {:?}", e)),
    }
}

fn get_default_completion_location(shell: Shell) -> Option<String> {
    let home = std::env::var(HOME_VAR);
    match (shell, home) {
        (Shell::Zsh, Ok(home)) => join_path(&[&home, ".oh-my-zsh", "completions"]),
        (Shell::Fish, Ok(home)) => join_path(&[&home, ".config", "fish", "completions"]),
        (Shell::Bash, _) => join_path(&["/usr", "local", "share", "bash-completion"]),
        (Shell::PowerShell, Ok(home)) => join_path(&[&home, "Documents", "WindowsPowerShell"]),
        (_, Err(e)) => {
            eprintln!("Failed reading {} variable due to {:?}", HOME_VAR, e);
            None
        }
        (_, Ok(_)) => None,
    }
}

fn join_path(parts: &[&str]) -> Option<String> {
    let mut path = Path::new(parts[0]).to_path_buf();
    for part in parts[1..].to_vec() {
        path.push(part);
    }
    path.to_str().map(|s| s.to_owned())
}

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

    #[test]
    #[ignore]
    fn test_home() {
        let home = std::env::var(HOME_VAR).unwrap();
        #[cfg(target_os = "windows")]
        assert!(home.to_lowercase().starts_with(r"c:\"));
        #[cfg(not(target_os = "windows"))]
        assert!(home.starts_with('/'));
    }

    #[test]
    fn test_join_path() {
        let home = std::env::var(HOME_VAR).unwrap();
        #[cfg(not(target_os = "windows"))]
        {
            assert_eq!(join_path(&[&home, "foo"]).unwrap(), format!("{}/foo", home))
        }
        #[cfg(target_os = "windows")]
        {
            assert_eq!(
                join_path(&[&home, "foo"]).unwrap(),
                format!(r"{}\foo", home)
            )
        }
    }

    #[test]
    fn test_shell_default_location() {
        let home = std::env::var(HOME_VAR).unwrap();
        #[cfg(not(target_os = "windows"))]
        {
            assert_eq!(
                get_default_completion_location(Shell::Zsh).unwrap(),
                format!("{}/.oh-my-zsh/completions", home)
            );
        }
        #[cfg(target_os = "windows")]
        {
            assert_eq!(
                get_default_completion_location(Shell::PowerShell).unwrap(),
                format!(r"{}\Documents\WindowsPowerShell", home)
            );
        }
    }
}