verifyos-cli 0.13.1

AI agent-friendly Rust CLI for scanning iOS app bundles for App Store rejection risks before submission.
Documentation
use crate::{AgentPackOutput, FailOnLevel, OutputFormat, TimingLevel};
use miette::Result;
use std::collections::HashSet;
use verifyos_cli::profiles::{available_rule_ids, normalize_rule_id, RuleSelection, ScanProfile};
use verifyos_cli::report::{AgentPackFormat, FailOn, TimingMode};

pub fn output_format_key(value: OutputFormat) -> String {
    match value {
        OutputFormat::Table => "table".to_string(),
        OutputFormat::Json => "json".to_string(),
        OutputFormat::Sarif => "sarif".to_string(),
    }
}

pub fn profile_key(value: ScanProfile) -> String {
    match value {
        ScanProfile::Basic => "basic".to_string(),
        ScanProfile::Full => "full".to_string(),
    }
}

pub fn fail_on_key(value: FailOnLevel) -> String {
    match value {
        FailOnLevel::Off => "off".to_string(),
        FailOnLevel::Error => "error".to_string(),
        FailOnLevel::Warning => "warning".to_string(),
    }
}

pub fn timing_key(value: TimingLevel) -> String {
    match value {
        TimingLevel::Summary => "summary".to_string(),
        TimingLevel::Full => "full".to_string(),
    }
}

pub fn agent_pack_format_key(value: AgentPackOutput) -> String {
    match value {
        AgentPackOutput::Json => "json".to_string(),
        AgentPackOutput::Markdown => "markdown".to_string(),
        AgentPackOutput::Bundle => "bundle".to_string(),
    }
}

pub fn parse_output_format(value: &str) -> Result<OutputFormat> {
    match value.to_ascii_lowercase().as_str() {
        "table" => Ok(OutputFormat::Table),
        "json" => Ok(OutputFormat::Json),
        "sarif" => Ok(OutputFormat::Sarif),
        _ => Err(miette::miette!(
            "Unknown output format `{}`. Expected one of: table, json, sarif",
            value
        )),
    }
}

pub fn parse_profile(value: &str) -> Result<ScanProfile> {
    match value.to_ascii_lowercase().as_str() {
        "basic" => Ok(ScanProfile::Basic),
        "full" => Ok(ScanProfile::Full),
        _ => Err(miette::miette!(
            "Unknown profile `{}`. Expected one of: basic, full",
            value
        )),
    }
}

pub fn parse_optional_cli_profile(value: Option<&str>) -> Result<Option<ScanProfile>> {
    value.map(parse_profile).transpose()
}

pub fn parse_fail_on(value: &str) -> Result<FailOn> {
    match value.to_ascii_lowercase().as_str() {
        "off" => Ok(FailOn::Off),
        "error" => Ok(FailOn::Error),
        "warning" => Ok(FailOn::Warning),
        _ => Err(miette::miette!(
            "Unknown fail-on threshold `{}`. Expected one of: off, error, warning",
            value
        )),
    }
}

pub fn parse_timing_mode(value: &str) -> Result<TimingMode> {
    match value.to_ascii_lowercase().as_str() {
        "off" => Ok(TimingMode::Off),
        "summary" => Ok(TimingMode::Summary),
        "full" => Ok(TimingMode::Full),
        _ => Err(miette::miette!(
            "Unknown timings mode `{}`. Expected one of: off, summary, full",
            value
        )),
    }
}

pub fn parse_agent_pack_format(value: &str) -> Result<AgentPackFormat> {
    match value.to_ascii_lowercase().as_str() {
        "json" => Ok(AgentPackFormat::Json),
        "markdown" => Ok(AgentPackFormat::Markdown),
        "bundle" => Ok(AgentPackFormat::Bundle),
        _ => Err(miette::miette!(
            "Unknown agent pack format `{}`. Expected one of: json, markdown, bundle",
            value
        )),
    }
}

pub fn parse_optional_output_format(value: Option<&str>) -> Result<Option<OutputFormat>> {
    value.map(parse_output_format).transpose()
}

pub fn build_rule_selection(
    profile: ScanProfile,
    include: &[String],
    exclude: &[String],
) -> Result<RuleSelection> {
    let available: HashSet<String> = available_rule_ids(profile).into_iter().collect();
    let include = normalize_requested_rules(include, &available, "--include")?;
    let exclude = normalize_requested_rules(exclude, &available, "--exclude")?;

    Ok(RuleSelection { include, exclude })
}

fn normalize_requested_rules(
    values: &[String],
    available: &HashSet<String>,
    flag_name: &str,
) -> Result<HashSet<String>> {
    let mut normalized = HashSet::new();
    for value in values {
        let normalized_id = normalize_rule_id(value);
        if !available.contains(&normalized_id) {
            let mut valid = available.iter().cloned().collect::<Vec<_>>();
            valid.sort();
            return Err(miette::miette!(
                "Rule `{}` is not available for this profile via {}. Valid values: {}",
                normalized_id,
                flag_name,
                valid.join(", ")
            ));
        }
        normalized.insert(normalized_id);
    }
    Ok(normalized)
}