tovuk 0.1.107

Use Tovuk scraper APIs from a native CLI.
use super::{flags, model::CliOptions};
use crate::cli::errors::{Result, agent_error};
use std::env;

const END_OF_OPTIONS: &str = "\x2d\x2d";
const OUTPUT_ENV: &str = "TOVUK_OUTPUT";

pub(crate) fn parse_args(argv: &[String]) -> Result<CliOptions> {
    let mut cli = CliOptions::default();
    apply_output_env(&mut cli)?;
    let mut positional = Vec::new();
    let mut index = 0usize;

    while index < argv.len() {
        let arg = argv[index].clone();
        if arg == END_OF_OPTIONS {
            positional.extend(argv.iter().skip(index + 1).cloned());
            break;
        }
        if let Some((name, inline)) = flags::parse_flag(&arg) {
            let consumed = flags::apply_flag(&mut cli, name, inline, argv, index)?;
            index += consumed;
        } else if arg.starts_with('-') {
            return Err(agent_error(
                "unknown_argument",
                format!("Unknown Tovuk option: {arg}."),
                "Run `tovuk --help`, remove or correct the unsupported option, then retry.",
                cli.output.json,
            ));
        } else {
            positional.push(arg);
            index += 1;
        }
    }

    if let Some(command) = positional.first() {
        cli.command.clone_from(command);
        cli.args = positional.into_iter().skip(1).collect();
    }
    cli.api_url
        .truncate(cli.api_url.trim_end_matches('/').len());
    validate_command_flags(&cli)?;
    Ok(cli)
}

fn apply_output_env(cli: &mut CliOptions) -> Result<()> {
    let Ok(value) = env::var(OUTPUT_ENV) else {
        return Ok(());
    };
    let value = value.trim();
    if value.is_empty() {
        return Ok(());
    }
    flags::set_output_format(cli, value, OUTPUT_ENV, false)
}

fn validate_command_flags(cli: &CliOptions) -> Result<()> {
    for policy in FLAG_POLICIES {
        if (policy.is_used)(cli) && !policy.usage.allows(cli) {
            return Err(agent_error(
                "unknown_argument",
                format!("{} is not supported for this Tovuk command.", policy.name),
                "Run `tovuk --help`, remove flags that do not belong to this command, then retry.",
                cli.output.json,
            ));
        }
    }
    Ok(())
}

struct FlagPolicy {
    name: &'static str,
    is_used: fn(&CliOptions) -> bool,
    usage: FlagUsage,
}

#[derive(Copy, Clone)]
enum FlagUsage {
    Global,
    BillingCheckout,
    RequestListCreateResults,
    RequestListResults,
    SupportCreate,
    SupportList,
}

const FLAG_POLICIES: &[FlagPolicy] = &[
    FlagPolicy {
        name: "--limit",
        is_used: |cli| !cli.limit.is_empty(),
        usage: FlagUsage::RequestListCreateResults,
    },
    FlagPolicy {
        name: "--cursor",
        is_used: |cli| !cli.cursor.is_empty(),
        usage: FlagUsage::RequestListResults,
    },
    FlagPolicy {
        name: "--top-up-usd-cents",
        is_used: |cli| !cli.top_up_usd_cents.is_empty(),
        usage: FlagUsage::BillingCheckout,
    },
    FlagPolicy {
        name: "--token",
        is_used: |cli| !cli.token.is_empty(),
        usage: FlagUsage::Global,
    },
    FlagPolicy {
        name: "--failing-command",
        is_used: |cli| !cli.failing_command.is_empty(),
        usage: FlagUsage::SupportCreate,
    },
    FlagPolicy {
        name: "--first-log-line",
        is_used: |cli| !cli.first_log_line.is_empty(),
        usage: FlagUsage::SupportCreate,
    },
    FlagPolicy {
        name: "--request-id",
        is_used: |cli| !cli.request_id.is_empty(),
        usage: FlagUsage::SupportCreate,
    },
    FlagPolicy {
        name: "--scraper-id",
        is_used: |cli| !cli.scraper_id.is_empty(),
        usage: FlagUsage::SupportCreate,
    },
    FlagPolicy {
        name: "--severity",
        is_used: |cli| !cli.severity.is_empty(),
        usage: FlagUsage::SupportCreate,
    },
];

impl FlagUsage {
    fn allows(self, cli: &CliOptions) -> bool {
        match self {
            Self::Global => true,
            Self::BillingCheckout => {
                command_is(cli, "billing", &["checkout"])
                    || command_default_is(cli, "billing", "checkout")
            }
            Self::RequestListCreateResults => {
                command_is(cli, "request", &["list", "create", "results"])
                    || command_default_is(cli, "request", "list")
                    || Self::SupportList.allows(cli)
            }
            Self::RequestListResults => {
                command_is(cli, "request", &["list", "results"])
                    || command_default_is(cli, "request", "list")
                    || Self::SupportList.allows(cli)
            }
            Self::SupportCreate => command_is(cli, "support", &["create"]),
            Self::SupportList => {
                command_is(cli, "support", &["list"]) || command_default_is(cli, "support", "list")
            }
        }
    }
}

fn command_is(cli: &CliOptions, command: &str, subcommands: &[&str]) -> bool {
    cli.command == command
        && subcommands
            .iter()
            .any(|subcommand| cli.args.first().map_or("", String::as_str) == *subcommand)
}

fn command_default_is(cli: &CliOptions, command: &str, default_subcommand: &str) -> bool {
    cli.command == command
        && cli.args.first().map_or(default_subcommand, String::as_str) == default_subcommand
}

#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;