cloudcover-cli 0.2.1

Command-line interface for analyzing cloud SDK usage and generating permission policies
use std::{env, ffi::OsString, process::ExitCode};

use cloudcover_core::Language;

use crate::{
    error::CliError,
    policy::{build_policy, build_terraform_policy},
};

#[derive(Clone, Copy)]
enum OutputFormat {
    Json,
    Terraform,
}

pub(crate) const USAGE: &str = "Usage: cloudcover policy [--language go|terraform|javascript|typescript] [--format json|terraform] <PATH>";

pub(crate) fn run_main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(CliError::Help) => {
            println!("{USAGE}");
            ExitCode::SUCCESS
        }
        Err(CliError::Usage(message)) => {
            eprintln!("{message}");
            eprintln!("{USAGE}");
            ExitCode::from(2)
        }
        Err(CliError::Runtime(message)) => {
            eprintln!("error: {message}");
            ExitCode::from(1)
        }
    }
}

fn run() -> Result<(), CliError> {
    let mut args = env::args_os();
    let _program = args.next();

    match args.next() {
        None => Err(CliError::Usage("missing command".to_owned())),
        Some(command) if is_help_flag(&command) => Err(CliError::Help),
        Some(command) if command == "policy" => run_policy(&args.collect::<Vec<_>>()),
        Some(command) => Err(CliError::Usage(format!(
            "unsupported command: {}",
            command.to_string_lossy()
        ))),
    }
}

fn run_policy(args: &[OsString]) -> Result<(), CliError> {
    if args.len() == 1 && is_help_flag(&args[0]) {
        return Err(CliError::Help);
    }
    let mut language = Language::Go;
    let mut format = OutputFormat::Json;
    let mut path = None;
    let mut index = 0;
    while index < args.len() {
        let argument = &args[index];
        if is_help_flag(argument) {
            return Err(CliError::Help);
        }
        if argument == "--language" {
            let value = args
                .get(index + 1)
                .ok_or_else(|| CliError::Usage("missing value for --language".to_owned()))?;
            language = match value.to_string_lossy().as_ref() {
                "go" => Language::Go,
                "terraform" => Language::Terraform,
                "javascript" => Language::JavaScript,
                "typescript" => Language::TypeScript,
                _ => {
                    return Err(CliError::Usage(format!(
                        "unsupported language: {}",
                        value.to_string_lossy()
                    )));
                }
            };
            index += 2;
            continue;
        }
        if argument == "--format" {
            let value = args
                .get(index + 1)
                .ok_or_else(|| CliError::Usage("missing value for --format".to_owned()))?;
            format = match value.to_string_lossy().as_ref() {
                "json" => OutputFormat::Json,
                "terraform" => OutputFormat::Terraform,
                _ => {
                    return Err(CliError::Usage(format!(
                        "unsupported format: {}",
                        value.to_string_lossy()
                    )));
                }
            };
            index += 2;
            continue;
        }
        if argument.to_string_lossy().starts_with('-') {
            return Err(CliError::Usage(format!(
                "unsupported option: {}",
                argument.to_string_lossy()
            )));
        }
        if path.replace(argument.clone()).is_some() {
            return Err(CliError::Usage("too many positional arguments".to_owned()));
        }
        index += 1;
    }

    let path = path.ok_or_else(|| CliError::Usage("missing path".to_owned()))?;
    match format {
        OutputFormat::Json => {
            let policy = build_policy(path, language)?;
            let stdout = serde_json::to_string_pretty(&policy).map_err(|error| {
                CliError::Runtime(format!("failed to encode policy JSON: {error}"))
            })?;
            println!("{stdout}");
        }
        OutputFormat::Terraform => {
            println!("{}", build_terraform_policy(path, language)?);
        }
    }
    Ok(())
}

fn is_help_flag(argument: &OsString) -> bool {
    argument == "-h" || argument == "--help"
}