forklaunch 1.22.0

Launch faster with forklaunch
use std::io::Write;

use anyhow::{Context, Result};
use clap::{Arg, ArgAction, ArgMatches, Command};
use reqwest::Method;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};

use super::shared::{iam_get, iam_send};
use crate::{CliCommand, core::command::command};

#[derive(Debug)]
pub(super) struct InvitationsCommand {
    list: ListCommand,
    resend: ResendCommand,
    cancel: CancelCommand,
}

impl InvitationsCommand {
    pub(super) fn new() -> Self {
        Self {
            list: ListCommand,
            resend: ResendCommand,
            cancel: CancelCommand,
        }
    }
}

impl CliCommand for InvitationsCommand {
    fn command(&self) -> Command {
        command("invitations", "List, resend or cancel pending invitations")
            .subcommand(self.list.command())
            .subcommand(self.resend.command())
            .subcommand(self.cancel.command())
    }

    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        match matches.subcommand() {
            Some(("list", m)) => self.list.handler(m),
            Some(("resend", m)) => self.resend.handler(m),
            Some(("cancel", m)) => self.cancel.handler(m),
            _ => self.list.handler(matches),
        }
    }
}

#[derive(Debug)]
struct ListCommand;
impl CliCommand for ListCommand {
    fn command(&self) -> Command {
        command("list", "List pending invitations").arg(
            Arg::new("json")
                .long("json")
                .action(ArgAction::SetTrue)
                .help("Output raw JSON"),
        )
    }
    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        let body = iam_get("/invitations")?;
        let mut stdout = StandardStream::stdout(ColorChoice::Always);
        if matches.get_flag("json") {
            writeln!(stdout, "{}", serde_json::to_string_pretty(&body)?)?;
            return Ok(());
        }
        let rows = body
            .get("invitations")
            .and_then(|v| v.as_array())
            .or_else(|| body.as_array())
            .cloned()
            .unwrap_or_default();
        writeln!(stdout)?;
        for r in &rows {
            let s = |k: &str| r.get(k).and_then(|v| v.as_str()).unwrap_or("-");
            stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)).set_bold(true))?;
            write!(stdout, "  {:<34}", s("email"))?;
            stdout.reset()?;
            writeln!(stdout, "{:<10}{:<12}{}", s("role"), s("status"), s("id"))?;
        }
        if rows.is_empty() {
            writeln!(stdout, "  No pending invitations.")?;
        }
        writeln!(stdout)?;
        Ok(())
    }
}

#[derive(Debug)]
struct ResendCommand;
impl CliCommand for ResendCommand {
    fn command(&self) -> Command {
        command("resend", "Resend a pending invitation").arg(
            Arg::new("id")
                .required(true)
                .help("Invitation id (from `org invitations list`)"),
        )
    }
    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        let id = matches.get_one::<String>("id").context("id is required")?;
        iam_send(
            Method::POST,
            &format!("/invitations/{}/resend", urlencoding::encode(id)),
            Some(serde_json::json!({})),
            "resend this invitation",
        )?;
        let mut stdout = StandardStream::stdout(ColorChoice::Always);
        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
        write!(stdout, "  Resent")?;
        stdout.reset()?;
        writeln!(stdout, "  invitation {}", id)?;
        Ok(())
    }
}

#[derive(Debug)]
struct CancelCommand;
impl CliCommand for CancelCommand {
    fn command(&self) -> Command {
        command("cancel", "Cancel a pending invitation").arg(
            Arg::new("id")
                .required(true)
                .help("Invitation id (from `org invitations list`)"),
        )
    }
    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        let id = matches.get_one::<String>("id").context("id is required")?;
        iam_send(
            Method::DELETE,
            &format!("/invitations/{}", urlencoding::encode(id)),
            None,
            "cancel this invitation",
        )?;
        let mut stdout = StandardStream::stdout(ColorChoice::Always);
        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
        write!(stdout, "  Cancelled")?;
        stdout.reset()?;
        writeln!(stdout, "  invitation {}", id)?;
        Ok(())
    }
}

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

    fn cmd() -> Command {
        InvitationsCommand::new().command().version("0.0.0-test")
    }

    #[test]
    fn command_definition_is_valid() {
        cmd().debug_assert();
    }

    #[test]
    fn resend_and_cancel_both_need_an_id() {
        assert!(
            cmd()
                .clone()
                .try_get_matches_from(["invitations", "resend"])
                .is_err()
        );
        assert!(
            cmd()
                .clone()
                .try_get_matches_from(["invitations", "cancel"])
                .is_err()
        );
        assert!(
            cmd()
                .try_get_matches_from(["invitations", "cancel", "inv-1"])
                .is_ok()
        );
    }
}