pukbot 0.3.5

Post disclosed GitHub comments through the Pukbot GitHub App
use std::path::PathBuf;
use std::{env, fs};

use anyhow::{Context, Result, bail};
use clap::CommandFactory;
use clap_complete::{Shell, generate};
use serde::Serialize;

use crate::Cli;

#[derive(Debug, Serialize)]
pub struct CompletionInstall {
    pub shell: String,
    pub path: PathBuf,
}

pub fn script(shell: Shell) -> Result<String> {
    let mut command = Cli::command();
    let mut contents = Vec::new();
    generate(shell, &mut command, "pukbot", &mut contents);
    String::from_utf8(contents).context("completion script was not valid UTF-8")
}

pub fn install(shell: Option<Shell>) -> Result<CompletionInstall> {
    let shell = shell.map_or_else(detect_shell, Ok)?;
    let path = completion_path(shell)?;
    let parent = path
        .parent()
        .context("completion path has no parent directory")?;
    fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
    fs::write(&path, script(shell)?)
        .with_context(|| format!("failed to write {}", path.display()))?;
    Ok(CompletionInstall {
        shell: shell.to_string(),
        path,
    })
}

fn detect_shell() -> Result<Shell> {
    if cfg!(windows) && env::var_os("PSModulePath").is_some() {
        return Ok(Shell::PowerShell);
    }
    let shell = env::var_os("SHELL")
        .and_then(|value| PathBuf::from(value).file_name().map(ToOwned::to_owned))
        .and_then(|value| value.to_str().map(str::to_ascii_lowercase))
        .context("could not detect the shell; provide bash, zsh, fish, elvish, or powershell")?;
    match shell.as_str() {
        "bash" => Ok(Shell::Bash),
        "zsh" => Ok(Shell::Zsh),
        "fish" => Ok(Shell::Fish),
        "elvish" => Ok(Shell::Elvish),
        "pwsh" | "powershell" => Ok(Shell::PowerShell),
        _ => bail!("unsupported shell: {shell}"),
    }
}

fn completion_path(shell: Shell) -> Result<PathBuf> {
    let home = env::var_os("HOME").map(PathBuf::from);
    let config = env::var_os("XDG_CONFIG_HOME")
        .map(PathBuf::from)
        .or_else(|| home.as_ref().map(|path| path.join(".config")));
    let data = env::var_os("XDG_DATA_HOME")
        .map(PathBuf::from)
        .or_else(|| home.as_ref().map(|path| path.join(".local/share")));
    match shell {
        Shell::Bash => Ok(data
            .context("HOME or XDG_DATA_HOME is required")?
            .join("bash-completion/completions/pukbot")),
        Shell::Fish => Ok(config
            .context("HOME or XDG_CONFIG_HOME is required")?
            .join("fish/completions/pukbot.fish")),
        Shell::Zsh => Ok(home.context("HOME is required")?.join(".zfunc/_pukbot")),
        Shell::Elvish => Ok(config
            .context("HOME or XDG_CONFIG_HOME is required")?
            .join("elvish/lib/pukbot.elv")),
        Shell::PowerShell => Ok(env::var_os("LOCALAPPDATA")
            .map(PathBuf::from)
            .or(home)
            .context("LOCALAPPDATA or HOME is required")?
            .join("Pukbot/pukbot-completions.ps1")),
        _ => bail!("unsupported completion shell"),
    }
}

#[cfg(test)]
mod tests {
    use clap_complete::Shell;

    use super::script;

    #[test]
    fn generates_every_completion() {
        for shell in [
            Shell::Bash,
            Shell::Zsh,
            Shell::Fish,
            Shell::Elvish,
            Shell::PowerShell,
        ] {
            assert_ne!(script(shell).expect("completion should render"), "");
        }
    }
}