simple_slack_gen 0.1.0

Rust API Client
Documentation
use clap::{CommandFactory, Parser};
use std::str::FromStr;
#[tokio::main]
async fn main() {
    let cli = SidekoCli::parse();
    if let Err(e) = handle_cli(cli).await {
        display_error(e);
        std::process::exit(1);
    }
}
async fn handle_cli(cli: SidekoCli) -> simple_slack_gen::SdkResult<()> {
    init_logger(cli.verbose);
    load_dotsideko();
    let base_url_env_var = "SIMPLE_SLACK_GEN_BASE_URL";
    match cli.command {
        SidekoCommand::SidekoConfigSubcommand(
            SidekoConfigSubcommand::Docs { output },
        ) => {
            let markdown = clap_markdown::help_markdown::<SidekoCli>();
            std::fs::write(&output, markdown)?;
            log::info!("CLI documentation saved to {output}")
        }
        SidekoCommand::SidekoConfigSubcommand(
            SidekoConfigSubcommand::Completions { shell },
        ) => {
            let mut cmd = SidekoCli::command();
            let cmd_name = cmd.get_name().to_string();
            clap_complete::generate(shell, &mut cmd, cmd_name, &mut std::io::stdout());
        }
        SidekoCommand::SidekoConfigSubcommand(
            SidekoConfigSubcommand::BaseUrl { unset, url },
        ) => {
            if unset {
                write_dotsideko(base_url_env_var, "", true);
            } else if let Some(url) = url {
                write_dotsideko(base_url_env_var, &url, false);
            } else {
                log::error!("No base url provided");
                std::process::exit(1);
            }
            log::info!("Base URL updated")
        }
        SidekoCommand::ConversationsSubcommand(ConversationsSubcommand::List(req)) => {
            let mut client = simple_slack_gen::Client::default();
            if let Ok(base_url) = std::env::var(base_url_env_var) {
                client = client.with_base_url(&base_url);
                log::debug!("Using custom base url: {base_url}");
            }
            if let Ok(val) = std::env::var("SIMPLE_SLACK_GEN_AUTH") {
                log::debug!("Adding oauth auth 'auth' (key=\"****\")");
                client = client.with_auth(&val);
            }
            let res = client.conversations().list(req).await?;
            println!(
                "{}", serde_json::to_string_pretty(& res).unwrap_or_else(| _ |
                serde_json::json!(& res) .to_string())
            );
        }
        SidekoCommand::SidekoConfigSubcommand(
            SidekoConfigSubcommand::SidekoAuthSubcommand(
                SidekoAuthSubcommand::Auth { token },
            ),
        ) => {
            write_dotsideko("SIMPLE_SLACK_GEN_AUTH", &token, false);
            log::info!("Authentication added to CLI");
        }
    }
    Ok(())
}
fn display_error(e: simple_slack_gen::Error) {
    match &e {
        simple_slack_gen::Error::Io(error) => log::debug!("IO Error: {:?}", error),
        simple_slack_gen::Error::Request(error) => {
            log::debug!("Request Error: {:?}", error)
        }
        simple_slack_gen::Error::Api(api_error)
        | simple_slack_gen::Error::ContentType(api_error) => {
            log::debug!("Response headers: {:?}", & api_error.headers);
            if let Ok(val) = api_error.json::<serde_json::Value>() {
                log::debug!(
                    "Body: {}", serde_json::to_string_pretty(& val).unwrap_or_else(| _ |
                    val.to_string())
                );
            } else if let Ok(text) = std::str::from_utf8(&api_error.content) {
                log::debug!("Body: {text}",);
            } else {
                log::debug!("Unable to process body ({} bytes)", api_error.content.len())
            }
        }
        simple_slack_gen::Error::DeserializeJson(error, _json_str) => {
            log::debug!("JSON Error: {error}")
        }
    }
    log::error!("{e}");
}
#[allow(unused)]
fn save_binary_response(
    res: simple_slack_gen::BinaryResponse,
) -> Result<(), std::io::Error> {
    log::debug!("Binary response headers: {:?}", & res.headers);
    let content_type = res
        .headers
        .get("content-type")
        .map(|val| val.to_str().unwrap_or_default());
    let mut extension = "out".to_string();
    if let Some(ct) = content_type {
        if let Some(Some(suffix)) = mime_guess::get_mime_extensions_str(ct)
            .map(|s| s.first())
        {
            extension = suffix.to_string()
        } else {
            log::warn!("Unsable to determine file extension from content type '{ct}'")
        }
    } else {
        log::warn!(
            "Unable to determine file extension from empty content type header in response"
        )
    }
    let outpath = camino::Utf8PathBuf::from(format!("./output.{extension}"));
    std::fs::write(&outpath, &res.content)?;
    log::info!("Wrote {} bytes to {outpath}", res.content.len());
    Ok(())
}
fn get_dotsideko_path() -> camino::Utf8PathBuf {
    if let Ok(custom_dotsideko) = std::env::var("SIDEKO_CLI_CONFIG") {
        camino::Utf8PathBuf::from_str(&custom_dotsideko)
            .unwrap_or_else(|_| {
                log::debug!("$SIDEKO_CLI_CONFIG set to: '{custom_dotsideko}'");
                log::error!(
                    "$SIDEKO_CLI_CONFIG environment variable must be a valid path if set"
                );
                std::process::exit(1)
            })
    } else {
        let home = std::env::var("HOME")
            .unwrap_or_else(|_| {
                log::error!(
                    "$HOME environment variable must be set for the CLI to function"
                );
                std::process::exit(1)
            });
        let buf = camino::Utf8PathBuf::from_str(&home)
            .unwrap_or_else(|_| {
                log::debug!("$HOME set to: '{home}'");
                log::error!("$HOME environment variable must be a valid path");
                std::process::exit(1)
            });
        buf.join(".sideko-cli")
    }
}
fn load_dotsideko() {
    let path = get_dotsideko_path();
    if path.exists() {
        log::debug!("Loading CLI config from {path}");
        if let Err(e) = dotenv::from_path(path.clone()) {
            log::debug!("Dotenv error: {:?}", e);
            log::error!("Failed loading config from '{path}'");
            std::process::exit(1);
        }
        log::debug!("Loaded config from {path}")
    }
}
fn write_dotsideko(var: &str, val: &str, unset: bool) {
    let sh_safe_val = shlex::try_quote(val)
        .map(String::from)
        .unwrap_or_else(|_| val.to_string());
    let dotenv_entry = format!("{var}={sh_safe_val}");
    let path = get_dotsideko_path();
    let current_dotenv: Vec<String> = if path.exists() {
        let dotenv_string = std::fs::read_to_string(path.clone())
            .unwrap_or_else(|e| {
                log::debug!("FS error: {:?}", e);
                log::error!("Failed loading config from '{path}'");
                std::process::exit(1);
            });
        dotenv_string.split("\n").map(String::from).collect()
    } else {
        vec![]
    };
    let mut new_dotenv: Vec<String> = vec![];
    let mut replaced = false;
    for line in current_dotenv {
        if line.starts_with(&format!("{var}=")) {
            if !unset {
                new_dotenv.push(dotenv_entry.clone());
                replaced = true;
            }
        } else {
            new_dotenv.push(line);
        }
    }
    if !unset && !replaced {
        new_dotenv.push(dotenv_entry)
    }
    std::fs::write(&path, new_dotenv.join("\n"))
        .unwrap_or_else(|e| {
            log::debug!("FS error: {:?}", e);
            log::error!("Failed updating config at '{path}'");
            std::process::exit(1);
        });
    if unset {
        log::debug!("{var} unset in {path}")
    } else {
        log::debug!("{var} updated in {path}")
    }
}
fn init_logger(verbosity: u8) {
    let self_module = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
    let mut builder = env_logger::builder();
    if verbosity == 0 {
        builder
            .filter_module(&self_module, log::LevelFilter::Info)
            .format_target(false)
            .format_timestamp(None)
    } else if verbosity == 1 {
        builder.filter_module(&self_module, log::LevelFilter::Debug).format_target(false)
    } else {
        builder.filter_level(log::LevelFilter::Trace)
    };
    let _ = builder.try_init();
}
fn get_styles() -> clap::builder::Styles {
    clap::builder::Styles::styled()
        .usage(
            anstyle::Style::new()
                .bold()
                .underline()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
        )
        .header(
            anstyle::Style::new()
                .bold()
                .underline()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
        )
        .literal(
            anstyle::Style::new()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))),
        )
        .invalid(
            anstyle::Style::new()
                .bold()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
        )
        .error(
            anstyle::Style::new()
                .bold()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
        )
        .valid(
            anstyle::Style::new()
                .bold()
                .underline()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))),
        )
        .placeholder(
            anstyle::Style::new()
                .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::White))),
        )
}
#[derive(clap::Subcommand)]
enum SidekoConfigSubcommand {
    /// Generate markdown documentation for this CLI
    #[command(name = "docs")]
    Docs {
        /// Sets custom output path
        #[arg(long, default_value = "./CLI.md")]
        output: camino::Utf8PathBuf,
    },
    /// Output shell-autocompletion for this CLI
    /// (output to be piped into relevant rc file for sourcing)
    #[command(name = "completions")]
    Completions { #[arg(long)] shell: clap_complete::Shell },
    /// Configure a custom base url for the CLI to use
    #[command(name = "base-url")]
    BaseUrl {
        /// Base URL to use in future API requests
        #[arg(long)]
        url: Option<String>,
        /// Clear previously set custom base url in favour of default
        #[arg(long)]
        unset: bool,
    },
    /// Add authentication credentials to the CLI
    #[command(subcommand, name = "auth")]
    SidekoAuthSubcommand(SidekoAuthSubcommand),
}
#[derive(clap::Subcommand)]
enum SidekoAuthSubcommand {
    /// Add oAuth bearer token to the CLI for the 'auth' authentication method
    #[command(name = "auth")]
    Auth { #[arg(long)] token: String },
}
#[derive(clap::Parser)]
#[command(version, propagate_version = true, name = "simple-slack-gen")]
struct SidekoCli {
    #[command(subcommand)]
    command: SidekoCommand,
    #[arg(
        long,
        short = 'v',
        action = clap::ArgAction::Count,
        global = true,
        help = "Increase logging verbosity"
    )]
    verbose: u8,
}
#[derive(clap::Parser)]
#[command(styles = get_styles())]
#[allow(clippy::enum_variant_names)]
enum SidekoCommand {
    /// command group: authentication/documenention/configurations/etc.
    #[command(subcommand, name = "config")]
    SidekoConfigSubcommand(SidekoConfigSubcommand),
    /// command group (1 commands, 0 sub groups)
    #[command(subcommand, name = "conversations")]
    ConversationsSubcommand(ConversationsSubcommand),
}
#[derive(clap::Subcommand)]
#[allow(clippy::enum_variant_names)]
enum ConversationsSubcommand {
    /// Lists channels in the workspace.
    ///
    /// GET /conversations.list
    ///
    /// **Required Auth:** auth
    ///
    /// **Example:** `simple-slack-gen conversations list --cursor 'dXNlcjpVMDYxTkZUVDI=' --exclude-archived true --limit 10 --team-id T1234567890 --types 'public_channel,private_channel'`
    #[command(name = "list")]
    List(simple_slack_gen::resources::conversations::ListRequest),
}
#[cfg(test)]
mod cli_tests {
    use clap::Parser;
    #[serial_test::serial]
    #[tokio::test]
    async fn test_cli_conversations_list_200_generated_success() {
        let cli = super::SidekoCli::try_parse_from(
                shlex::Shlex::new(
                    &["simple-slack-gen", "config", "auth", "auth"].join(" "),
                ),
            )
            .expect("failed parsing auth cli input");
        super::handle_cli(cli).await.expect("failed running auth command");
        let cli = super::SidekoCli::try_parse_from(
                shlex::Shlex::new(
                    &[
                        "simple-slack-gen",
                        "conversations",
                        "list",
                        "--cursor",
                        "'dXNlcjpVMDYxTkZUVDI='",
                        "--exclude-archived",
                        "true",
                        "--limit",
                        "10",
                        "--team-id",
                        "T1234567890",
                        "--types",
                        "'public_channel,private_channel'",
                    ]
                        .join(" "),
                ),
            )
            .expect("failed parsing cli input");
        let result = super::handle_cli(cli).await;
        println!("{:?}", & result);
        assert!(result.is_ok())
    }
}