robotrt-cli 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
use crate::helpers::{has_flag, option_value, parse_u64_option, resolve_runtime_endpoint_from_hint};
use crate::gateway::{
    STATUS_OP_SNAPSHOT, STATUS_SERVICE_NAME, StatusServiceResponse, build_snapshot_request,
    make_udp_service_client, next_request_id, normalize_udp_endpoint, validate_response,
};

const DEFAULT_DAEMON_ENDPOINT: &str = "127.0.0.1:7588";

fn fetch_remote_status_snapshot(
    endpoint: &str,
    timeout_ms: u64,
) -> Result<(introspection_core::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 = client
        .call_json(STATUS_SERVICE_NAME, request_id, &request)
        .map_err(|err| format!("status query to {endpoint} failed: {err}"))?;

    validate_response(&response, request_id, STATUS_OP_SNAPSHOT)?;
    let snapshot = response.snapshot.ok_or_else(|| {
        format!(
            "status response from {endpoint} missing snapshot payload for op={STATUS_OP_SNAPSHOT}"
        )
    })?;
    Ok((snapshot, endpoint))
}

fn list_api_version(kind: &str) -> Option<&'static str> {
    match kind {
        "node" => Some("robotrt.node.list.v1"),
        "topic" => Some("robotrt.topic.list.v1"),
        "service" => Some("robotrt.service.list.v1"),
        "action" => Some("robotrt.action.list.v1"),
        "mission" => Some("robotrt.mission.list.v1"),
        _ => None,
    }
}

pub fn resource_list(kind: &str, args: &[String]) -> Result<(), String> {
    let json = has_flag(args, "--json");
    let endpoint = resolve_runtime_endpoint_from_hint(
        args,
        option_value(args, "--endpoint"),
        DEFAULT_DAEMON_ENDPOINT,
    )?;
    let timeout_ms = parse_u64_option(args, "--timeout-ms", 1000)?;
    let (snapshot, endpoint) = fetch_remote_status_snapshot(&endpoint, timeout_ms)?;
    let items = match kind {
        "node" => snapshot
            .nodes
            .iter()
            .map(|item| item.name.clone())
            .collect::<Vec<_>>(),
        "topic" => snapshot
            .topics
            .iter()
            .map(|item| item.name.clone())
            .collect::<Vec<_>>(),
        "service" => snapshot
            .services
            .iter()
            .map(|item| item.name.clone())
            .collect::<Vec<_>>(),
        "action" => snapshot
            .actions
            .iter()
            .map(|item| item.name.clone())
            .collect::<Vec<_>>(),
        "mission" => snapshot
            .missions
            .iter()
            .map(|item| item.name.clone())
            .collect::<Vec<_>>(),
        _ => return Err(format!("unsupported list kind: {kind}")),
    };

    if json {
        let api_version =
            list_api_version(kind).ok_or_else(|| format!("unsupported list kind: {kind}"))?;
        let payload = serde_json::json!({
            "api_version": api_version,
            "kind": kind,
            "captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
            "source": {
                "mode": "remote_service",
                "service": STATUS_SERVICE_NAME,
                "endpoint": endpoint,
                "timeout_ms": timeout_ms,
            },
            "result": {
                "count": items.len(),
                "items": items,
            }
        });
        let out = serde_json::to_string_pretty(&payload)
            .map_err(|err| format!("serialize resource list failed: {err}"))?;
        println!("{out}");
    } else {
        println!("RobotRT {} List", kind.to_uppercase());
        println!("source: remote:{}", endpoint);
        println!("count: {}", items.len());
        for item in &items {
            println!("- {}", item);
        }
    }

    Ok(())
}