robotrt-cli 0.1.0-beta.1

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

pub(in crate::commands::ops) fn load_status_snapshot(
    args: &[String],
) -> Result<(StatusSnapshot, OpsSource), String> {
    if let Some(raw_endpoint) = option_value(args, "--endpoint") {
        let timeout_ms = parse_u64_option(args, "--timeout-ms", 1000)?;
        let (snapshot, endpoint) = fetch_remote_status_snapshot(&raw_endpoint, timeout_ms)?;
        let source = OpsSource {
            label: format!("remote:{endpoint}"),
            json: serde_json::json!({
                "mode": "remote_service",
                "service": STATUS_SERVICE_NAME,
                "endpoint": endpoint,
                "timeout_ms": timeout_ms,
            }),
        };
        return Ok((snapshot, source));
    }

    let report_path = parse_report_path(args, DEFAULT_STATUS_REPORT_PATH)?;
    if has_flag(args, "--refresh-demo") {
        capture_demo_status_report(&report_path).map_err(|err| {
            format!(
                "refresh demo status report to {} failed: {err}",
                report_path.display()
            )
        })?;
    }

    let snapshot = read_status_snapshot(&report_path).map_err(|err| {
        format!(
            "read status snapshot from {} failed: {err}. Hint: run `cargo run -p local-loop` first, or pass --refresh-demo.",
            report_path.display()
        )
    })?;

    let source = OpsSource {
        label: report_path.display().to_string(),
        json: serde_json::json!({
            "mode": "file",
            "report": report_path,
        }),
    };
    Ok((snapshot, source))
}

pub(in crate::commands::ops) fn load_baseline_snapshot(
    args: &[String],
) -> Result<(StatusSnapshot, OpsSource), String> {
    if let Some(raw_endpoint) = option_value(args, "--baseline-endpoint") {
        let timeout_ms = parse_u64_option(args, "--baseline-timeout-ms", 1000)?;
        let (snapshot, endpoint) = fetch_remote_status_snapshot(&raw_endpoint, timeout_ms)?;
        let source = OpsSource {
            label: format!("remote:{endpoint}"),
            json: serde_json::json!({
                "mode": "remote_service",
                "service": STATUS_SERVICE_NAME,
                "endpoint": endpoint,
                "timeout_ms": timeout_ms,
            }),
        };
        return Ok((snapshot, source));
    }

    let Some(raw_path) = option_value(args, "--baseline-report") else {
        return Err(String::from(
            "ops diff requires --baseline-report <path> or --baseline-endpoint <addr>",
        ));
    };

    let report_path = PathBuf::from(raw_path);
    let snapshot = read_status_snapshot(&report_path).map_err(|err| {
        format!(
            "read baseline status snapshot from {} failed: {err}",
            report_path.display()
        )
    })?;

    let source = OpsSource {
        label: report_path.display().to_string(),
        json: serde_json::json!({
            "mode": "file",
            "report": report_path,
        }),
    };
    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 raw in parse_csv_list(&raw_reports) {
            let report_path = PathBuf::from(raw);
            let snapshot = read_status_snapshot(&report_path).map_err(|err| {
                format!(
                    "read fleet status snapshot from {} failed: {err}",
                    report_path.display()
                )
            })?;
            items.push((
                snapshot,
                OpsSource {
                    label: report_path.display().to_string(),
                    json: serde_json::json!({
                        "mode": "file",
                        "report": report_path,
                    }),
                },
            ));
        }
    }

    if let Some(raw_endpoints) = option_value(args, endpoints_option) {
        for raw in parse_csv_list(&raw_endpoints) {
            let (snapshot, endpoint) = fetch_remote_status_snapshot(&raw, timeout_ms)?;
            items.push((
                snapshot,
                OpsSource {
                    label: format!("remote:{endpoint}"),
                    json: serde_json::json!({
                        "mode": "remote_service",
                        "service": STATUS_SERVICE_NAME,
                        "endpoint": endpoint,
                        "timeout_ms": timeout_ms,
                    }),
                },
            ));
        }
    }

    if baseline {
        if items.is_empty()
            && (option_value(args, "--baseline-report").is_some()
                || 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()))
}

pub(in crate::commands::ops) fn fetch_remote_status_snapshot(
    endpoint: &str,
    timeout_ms: u64,
) -> Result<(StatusSnapshot, String), String> {
    let endpoint = normalize_udp_endpoint(endpoint)?;
    let client = make_udp_service_client(endpoint.clone(), timeout_ms)?;

    let request_id = next_request_id();
    let request = build_snapshot_request(request_id);
    let response: StatusServiceResponse =
        call_status_service(&client, request_id, &request, &endpoint)?;

    validate_response(&response, request_id)?;
    Ok((response.snapshot, endpoint))
}

pub(in crate::commands::ops) fn call_status_service(
    client: &core_api::UdpServiceClient,
    request_id: ServiceRequestId,
    request: &crate::status_api::StatusServiceRequest,
    endpoint: &str,
) -> Result<StatusServiceResponse, String> {
    client
        .call_json(STATUS_SERVICE_NAME, request_id, request)
        .map_err(|err| format!("status query to {endpoint} failed: {err}"))
}

pub(in crate::commands::ops) fn normalize_udp_endpoint(raw: &str) -> Result<String, String> {
    if let Some(rest) = raw.strip_prefix("udp://") {
        if rest.is_empty() {
            return Err(String::from("invalid --endpoint value: udp://"));
        }
        return Ok(rest.to_string());
    }
    if raw.contains("://") {
        return Err(format!("unsupported endpoint scheme: {raw}"));
    }
    Ok(raw.to_string())
}