moonlight-cli 0.1.2

Compare command behavior against one or two reference commands.
Documentation
use crate::{
    args::RunArgs,
    command_form::{
        parse_json_argv_flag, parse_optional_command_form, parse_required_command_form,
        CommandFormLabels,
    },
    config::{build_compare_config, CliDefaults},
    execute::execute_case,
    types::{Case, PreparedCase},
};
use moonlight_core::config::normalize_timeout;
use moonlight_core::config::CliTargetConfig;
use moonlight_core::storage::RunWriter;
use std::sync::Arc;

pub(crate) async fn run(args: RunArgs, defaults: &CliDefaults) -> anyhow::Result<()> {
    let targets = merge_targets(&args, &defaults.run.targets)?;
    let mut ignore_json_paths = defaults.ignore_json_paths.clone();
    ignore_json_paths.extend(args.ignore_json_paths);
    let mut ignore_json_path_patterns = defaults.ignore_json_path_patterns.clone();
    ignore_json_path_patterns.extend(args.ignore_json_path_patterns);
    let mut redact_json_paths = defaults.redact_json_paths.clone();
    redact_json_paths.extend(args.redact_json_paths);
    let mut redact_json_path_patterns = defaults.redact_json_path_patterns.clone();
    redact_json_path_patterns.extend(args.redact_json_path_patterns);
    let mut ignore_headers = defaults.ignore_headers.clone();
    ignore_headers.extend(args.ignore_headers);
    let ignore_stderr = defaults.ignore_stderr || args.ignore_stderr;
    let target_timeout_ms = args
        .target_timeout_ms
        .map(normalize_timeout)
        .unwrap_or(defaults.target_timeout_ms);
    let max_body_capture_bytes = args
        .max_body_capture_bytes
        .unwrap_or(defaults.max_body_capture_bytes);
    let serial_targets = defaults.run.serial_targets || args.serial_targets;
    let quiet = defaults.run.quiet || args.quiet;
    let compact = defaults.run.compact || args.compact;
    let storage_path = args
        .storage
        .storage_path
        .unwrap_or_else(|| defaults.storage_path.clone());

    let case = Case {
        primary: parse_required_command_form(
            run_labels("primary"),
            targets.primary,
            targets.primary_argv,
        )?,
        candidate: parse_required_command_form(
            run_labels("candidate"),
            targets.candidate,
            targets.candidate_argv,
        )?,
        secondary: parse_optional_command_form(
            run_labels("secondary"),
            targets.secondary,
            targets.secondary_argv,
        )?,
        max_body_capture_bytes,
        ignore_json_paths,
        ignore_json_path_patterns,
        redact_json_paths,
        redact_json_path_patterns,
        ignore_headers,
        ignore_stderr,
        target_timeout_ms,
    };
    let compare_config = Arc::new(build_compare_config(
        &case.ignore_json_paths,
        &case.ignore_json_path_patterns,
        &case.redact_json_paths,
        &case.redact_json_path_patterns,
        &case.ignore_headers,
        case.ignore_stderr,
    ));
    let run = execute_case(
        PreparedCase {
            case,
            compare_config,
        },
        serial_targets,
    )
    .await;

    let writer = RunWriter::open(storage_path).await?;
    writer.append(&run).await?;
    writer.flush().await?;

    if !quiet {
        if compact {
            println!("{}", serde_json::to_string(&run)?);
        } else {
            println!("{}", serde_json::to_string_pretty(&run)?);
        }
    }
    Ok(())
}

fn merge_targets(args: &RunArgs, defaults: &CliTargetConfig) -> anyhow::Result<CliTargetConfig> {
    Ok(CliTargetConfig {
        primary: merge_shell(&args.primary, &args.primary_argv, &defaults.primary),
        primary_argv: merge_argv(
            "--primary-argv",
            &args.primary,
            &args.primary_argv,
            &defaults.primary_argv,
        )?,
        candidate: merge_shell(&args.candidate, &args.candidate_argv, &defaults.candidate),
        candidate_argv: merge_argv(
            "--candidate-argv",
            &args.candidate,
            &args.candidate_argv,
            &defaults.candidate_argv,
        )?,
        secondary: merge_shell(&args.secondary, &args.secondary_argv, &defaults.secondary),
        secondary_argv: merge_argv(
            "--secondary-argv",
            &args.secondary,
            &args.secondary_argv,
            &defaults.secondary_argv,
        )?,
    })
}

fn merge_shell(
    arg_shell: &Option<String>,
    arg_argv: &Option<String>,
    default_shell: &Option<String>,
) -> Option<String> {
    if arg_shell.is_some() || arg_argv.is_some() {
        arg_shell.clone()
    } else {
        default_shell.clone()
    }
}

fn merge_argv(
    label: &'static str,
    arg_shell: &Option<String>,
    arg_argv: &Option<String>,
    default_argv: &Option<Vec<String>>,
) -> anyhow::Result<Option<Vec<String>>> {
    if arg_shell.is_some() || arg_argv.is_some() {
        parse_json_argv_flag(label, arg_argv.clone())
    } else {
        Ok(default_argv.clone())
    }
}

fn run_labels(role: &'static str) -> CommandFormLabels {
    let (shell, argv) = match role {
        "primary" => ("--primary", "--primary-argv"),
        "candidate" => ("--candidate", "--candidate-argv"),
        "secondary" => ("--secondary", "--secondary-argv"),
        _ => unreachable!("unknown target role"),
    };

    CommandFormLabels {
        role,
        shell,
        argv,
        reject_empty_shell: false,
    }
}