robotrt-cli 0.1.0-beta.1

RobotRT modular robotics runtime and middleware components.
use introspection_core::read_resource_catalog_report;

use crate::constants::DEFAULT_RESOURCE_REPORT_PATH;
use crate::demo::capture_demo_resource_report;
use crate::helpers::{has_flag, option_value, parse_report_path, parse_u64_option};
use crate::status_api::{
    STATUS_SERVICE_NAME, StatusServiceResponse, build_snapshot_request, make_udp_service_client,
    next_request_id, validate_response,
};

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

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CliRuntimeMode {
    Embedded,
    Daemon,
}

fn parse_runtime_mode(args: &[String]) -> Result<CliRuntimeMode, String> {
    match option_value(args, "--mode").as_deref() {
        None => Ok(CliRuntimeMode::Embedded),
        Some("embedded") => Ok(CliRuntimeMode::Embedded),
        Some("daemon") => Ok(CliRuntimeMode::Daemon),
        Some(other) => Err(format!(
            "unsupported --mode value: {other} (expected embedded|daemon)"
        )),
    }
}

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())
}

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)?;
    Ok((response.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 mode = parse_runtime_mode(args)?;
    let explicit_endpoint = option_value(args, "--endpoint");
    let remote_endpoint = match (mode, explicit_endpoint) {
        (CliRuntimeMode::Daemon, Some(ep)) => Some(ep),
        (CliRuntimeMode::Daemon, None) => Some(DEFAULT_DAEMON_ENDPOINT.to_string()),
        (CliRuntimeMode::Embedded, Some(ep)) => Some(ep),
        (CliRuntimeMode::Embedded, None) => None,
    };

    if let Some(raw_endpoint) = remote_endpoint {
        let timeout_ms = parse_u64_option(args, "--timeout-ms", 1000)?;
        let (snapshot, endpoint) = fetch_remote_status_snapshot(&raw_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);
            }
        }

        return Ok(());
    }

    let report_path = parse_report_path(args, DEFAULT_RESOURCE_REPORT_PATH)?;
    let refresh_demo = has_flag(args, "--refresh-demo");

    if refresh_demo {
        capture_demo_resource_report(&report_path).map_err(|err| {
            format!(
                "refresh demo resource report to {} failed: {err}",
                report_path.display()
            )
        })?;
    }

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

    let items = match kind {
        "node" => &report.nodes,
        "topic" => &report.topics,
        "service" => &report.services,
        "action" => &report.actions,
        "mission" => &report.missions,
        _ => 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": report.captured_at_unix_nanos,
            "source": {
                "mode": "file",
                "report": report_path,
            },
            "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: {}", report_path.display());
        println!("count: {}", items.len());
        for item in items {
            println!("- {}", item);
        }
    }

    Ok(())
}