robotrt-cli 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
use super::*;

fn build_remote_ops_source(
    endpoint: String,
    timeout_ms: u64,
    gateway_observation: Option<serde_json::Value>,
) -> OpsSource {
    OpsSource {
        label: format!("remote:{endpoint}"),
        json: serde_json::json!({
            "mode": "remote_service",
            "service": STATUS_SERVICE_NAME,
            "endpoint": endpoint,
            "timeout_ms": timeout_ms,
            "gateway_observation": gateway_observation,
        }),
    }
}

fn build_report_ops_source(report: String) -> OpsSource {
    OpsSource {
        label: format!("report:{report}"),
        json: serde_json::json!({
            "mode": "report_file",
            "report": report,
        }),
    }
}

fn load_report_snapshot(report: String, refresh_demo: bool) -> Result<(StatusSnapshot, OpsSource), String> {
    let path = PathBuf::from(&report);
    if refresh_demo {
        let snapshot = crate::demo::demo_status_snapshot();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(|err| {
                format!(
                    "create status snapshot report dir {} failed: {err}",
                    parent.display()
                )
            })?;
        }
        introspection_core::write_status_snapshot(&path, &snapshot).map_err(|err| {
            format!(
                "write status snapshot to {} failed: {err}",
                path.display()
            )
        })?;
        return Ok((snapshot, build_report_ops_source(report)));
    }

    let snapshot = introspection_core::read_status_snapshot(&path)
        .map_err(|err| format!("read status snapshot from {} failed: {err}", path.display()))?;
    Ok((snapshot, build_report_ops_source(report)))
}

fn fetch_gateway_observation(endpoint: &str, timeout_ms: u64) -> Option<serde_json::Value> {
    let client = crate::gateway::make_udp_service_client(endpoint.to_string(), timeout_ms).ok()?;
    let request_id = crate::gateway::next_request_id();
    let request = crate::gateway::build_request(crate::gateway::STATUS_OP_GATEWAY_OBSERVE);
    let response: crate::gateway::StatusServiceResponse = client
        .call_json(crate::gateway::STATUS_SERVICE_NAME, request_id, &request)
        .ok()?;
    crate::gateway::validate_response(
        &response,
        request_id,
        crate::gateway::STATUS_OP_GATEWAY_OBSERVE,
    )
    .ok()?;
    response.op_result
}

pub(in crate::commands::ops) fn load_status_snapshot(
    args: &[String],
) -> Result<(StatusSnapshot, OpsSource), String> {
    if let Some(report) = option_value(args, "--report") {
        return load_report_snapshot(report, has_flag(args, "--refresh-demo"));
    }

    crate::commands::snapshot_shared::load_status_snapshot(
        args,
        option_value(args, "--endpoint"),
        "--timeout-ms",
        DEFAULT_STATUS_REPORT_PATH,
    )
}

pub(in crate::commands::ops) fn load_baseline_snapshot(
    args: &[String],
) -> Result<(StatusSnapshot, OpsSource), String> {
    if let Some(report) = option_value(args, "--baseline-report") {
        return load_report_snapshot(report, has_flag(args, "--refresh-demo"));
    }

    let raw_endpoint = option_value(args, "--baseline-endpoint").ok_or_else(|| {
        String::from("ops diff requires --baseline-endpoint <addr>")
    })?;
    let timeout_ms = parse_u64_option(args, "--baseline-timeout-ms", 1000)?;
    let (snapshot, endpoint) = crate::commands::snapshot_shared::fetch_remote_status_snapshot(
        &raw_endpoint,
        timeout_ms,
    )?;
    let gateway_observation = fetch_gateway_observation(&endpoint, timeout_ms);
    let source = build_remote_ops_source(endpoint, timeout_ms, gateway_observation);
    Ok((snapshot, source))
}

pub(in crate::commands::ops) fn load_fleet_snapshots(
    args: &[String],
    baseline: bool,
) -> Result<Vec<(StatusSnapshot, OpsSource)>, String> {
    let reports_option = if baseline {
        "--baseline-reports"
    } else {
        "--reports"
    };
    let endpoints_option = if baseline {
        "--baseline-endpoints"
    } else {
        "--endpoints"
    };
    let timeout_option = if baseline {
        "--baseline-timeout-ms"
    } else {
        "--timeout-ms"
    };

    let mut items = Vec::new();
    let timeout_ms = parse_u64_option(args, timeout_option, 1000)?;

    if let Some(raw_reports) = option_value(args, reports_option) {
        for report in parse_csv_list(&raw_reports) {
            let snapshot = introspection_core::read_status_snapshot(&report).map_err(|err| {
                format!("read status snapshot from {report} failed: {err}")
            })?;
            items.push((snapshot, build_report_ops_source(report)));
        }
    }

    if let Some(raw_endpoints) = option_value(args, endpoints_option) {
        for raw in parse_csv_list(&raw_endpoints) {
            let (snapshot, endpoint) =
                crate::commands::snapshot_shared::fetch_remote_status_snapshot(
                    &raw,
                    timeout_ms,
                )?;
            let gateway_observation = fetch_gateway_observation(&endpoint, timeout_ms);
            items.push((
                snapshot,
                build_remote_ops_source(endpoint, timeout_ms, gateway_observation),
            ));
        }
    } else if !baseline && items.is_empty() {
        for endpoint in crate::helpers::discover_cluster_endpoints(args) {
            let (snapshot, endpoint) =
                crate::commands::snapshot_shared::fetch_remote_status_snapshot(
                    &endpoint,
                    timeout_ms,
                )?;
            let gateway_observation = fetch_gateway_observation(&endpoint, timeout_ms);
            items.push((
                snapshot,
                build_remote_ops_source(endpoint, timeout_ms, gateway_observation),
            ));
        }
    }

    if baseline {
        if items.is_empty() && option_value(args, "--baseline-endpoint").is_some() {
            let (snapshot, source) = load_baseline_snapshot(args)?;
            items.push((snapshot, source));
        }
        return Ok(items);
    }

    if items.is_empty() {
        let (snapshot, source) = load_status_snapshot(args)?;
        items.push((snapshot, source));
    }

    Ok(items)
}

pub(in crate::commands::ops) fn parse_csv_list(raw: &str) -> Vec<String> {
    raw.split(',')
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToString::to_string)
        .collect()
}

pub(in crate::commands::ops) fn write_json_payload_file(
    path: &Path,
    payload: &serde_json::Value,
) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|err| {
            format!(
                "create ops diff output dir {} failed: {err}",
                parent.display()
            )
        })?;
    }

    let body = serde_json::to_string_pretty(payload)
        .map_err(|err| format!("serialize ops diff report failed: {err}"))?;
    fs::write(path, body)
        .map_err(|err| format!("write ops diff report to {} failed: {err}", path.display()))
}