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