use introspection_core::{ActionStatus, NodeStatus, StatusSnapshot};
use crate::helpers::{find_topic_status, first_positional, has_flag, utilization_percent};
use crate::status_api::{ACTION_INFO_API_VERSION, NODE_INFO_API_VERSION, TOPIC_INFO_API_VERSION};
use super::data_source::load_status_snapshot;
pub(super) fn node_info(args: &[String]) -> Result<(), String> {
let json = has_flag(args, "--json");
let node_name = first_positional(args).ok_or_else(|| String::from("missing node name"))?;
let (snapshot, source) = load_status_snapshot(args)?;
let node = snapshot
.nodes
.iter()
.find(|item| item.name == node_name)
.ok_or_else(|| format!("node not found: {node_name}"))?;
if json {
let payload = stable_node_info_json(&snapshot, &source.json, &node_name, node);
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize node info failed: {err}"))?
);
} else {
println!("RobotRT Node Info");
println!("source: {}", source.label);
println!("name: {}", node.name);
println!("namespace: {}", node.namespace);
println!("capabilities: {}", node.capabilities.join(", "));
}
Ok(())
}
pub(super) fn topic_info(args: &[String]) -> Result<(), String> {
let json = has_flag(args, "--json");
let topic_name = first_positional(args).ok_or_else(|| String::from("missing topic name"))?;
let (snapshot, source) = load_status_snapshot(args)?;
let topic = find_topic_status(&snapshot, &topic_name)
.ok_or_else(|| format!("topic not found: {topic_name}"))?;
if json {
let payload = stable_topic_info_json(&snapshot, &source.json, &topic_name, topic);
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize topic info failed: {err}"))?
);
} else {
println!("RobotRT Topic Info");
println!("source: {}", source.label);
println!("name: {}", topic.name);
println!("schema: {}", topic.schema);
println!("qos: reliable={} depth={}", topic.reliable, topic.depth);
println!(
"load: pending={} max_depth={} utilization={:.2}%",
topic.pending,
topic.max_depth,
utilization_percent(topic.pending, topic.max_depth),
);
println!(
"endpoints: publishers={} subscribers={}",
topic.publishers, topic.subscribers
);
}
Ok(())
}
pub(super) fn action_info(args: &[String]) -> Result<(), String> {
let json = has_flag(args, "--json");
let action_name = first_positional(args).ok_or_else(|| String::from("missing action name"))?;
let (snapshot, source) = load_status_snapshot(args)?;
let action = find_action_status(&snapshot, &action_name)
.ok_or_else(|| format!("action not found: {action_name}"))?;
if json {
let payload = stable_action_info_json(&snapshot, &source.json, &action_name, action);
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize action info failed: {err}"))?
);
} else {
println!("RobotRT Action Info");
println!("source: {}", source.label);
println!("name: {}", action.name);
println!(
"endpoints: clients={} servers={}",
action.clients, action.servers
);
println!(
"health: state={} current_state={} active_goals={} heartbeat_timeout_ms={}",
action.health_state.as_deref().unwrap_or("unknown"),
action.current_state.as_deref().unwrap_or("unknown"),
action
.active_goals
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string()),
action
.heartbeat_timeout_ms
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string()),
);
}
Ok(())
}
pub(super) fn plugin_list(args: &[String]) -> Result<(), String> {
let json = has_flag(args, "--json");
let loaded_only = has_flag(args, "--loaded");
let (snapshot, source) = load_status_snapshot(args)?;
let captured_at_unix_nanos = snapshot.captured_at_unix_nanos;
let plugins = snapshot
.plugins
.into_iter()
.filter(|plugin| !loaded_only || plugin.loaded)
.collect::<Vec<_>>();
if json {
let payload = serde_json::json!({
"api_version": "robotrt.plugin.list.v1",
"kind": "plugin_list",
"captured_at_unix_nanos": captured_at_unix_nanos,
"source": source.json,
"query": {
"loaded_only": loaded_only,
},
"result": {
"plugins": plugins,
},
});
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize plugin list failed: {err}"))?
);
} else {
println!("RobotRT Plugin List");
println!("source: {}", source.label);
println!("loaded_only: {}", loaded_only);
for plugin in &plugins {
println!(
"- {} kind={} loaded={}",
plugin.name, plugin.kind, plugin.loaded
);
}
}
Ok(())
}
pub(super) fn health_cmd(args: &[String]) -> Result<(), String> {
let json = has_flag(args, "--json");
let (snapshot, source) = load_status_snapshot(args)?;
if json {
let payload = serde_json::json!({
"api_version": "robotrt.health.v1",
"kind": "health",
"captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
"source": source.json,
"result": {
"health": snapshot.health,
},
});
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize health failed: {err}"))?
);
} else {
println!("RobotRT Health");
println!("source: {}", source.label);
for item in &snapshot.health {
println!(
"- component={} status={} reason={}",
item.component,
item.status,
item.reason.as_deref().unwrap_or("-")
);
}
}
Ok(())
}
pub(super) fn graph_cmd(args: &[String]) -> Result<(), String> {
let json = has_flag(args, "--json");
let (snapshot, source) = load_status_snapshot(args)?;
if json {
let payload = serde_json::json!({
"api_version": "robotrt.graph.v1",
"kind": "graph",
"captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
"source": source.json,
"result": {
"edges": snapshot.edges,
},
});
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize graph failed: {err}"))?
);
} else {
println!("RobotRT Graph");
println!("source: {}", source.label);
for edge in &snapshot.edges {
println!("{} -[{}]-> {}", edge.from, edge.relation, edge.to);
}
}
Ok(())
}
fn stable_node_info_json(
snapshot: &StatusSnapshot,
source: &serde_json::Value,
query_name: &str,
node: &NodeStatus,
) -> serde_json::Value {
serde_json::json!({
"api_version": NODE_INFO_API_VERSION,
"kind": "node_info",
"captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
"source": source,
"query": {
"node_name": query_name,
},
"result": {
"name": node.name,
"namespace": node.namespace,
"capabilities": node.capabilities,
},
})
}
fn stable_topic_info_json(
snapshot: &StatusSnapshot,
source: &serde_json::Value,
query_name: &str,
topic: &introspection_core::TopicStatus,
) -> serde_json::Value {
serde_json::json!({
"api_version": TOPIC_INFO_API_VERSION,
"kind": "topic_info",
"captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
"source": source,
"query": {
"topic_name": query_name,
},
"result": {
"name": topic.name,
"schema": topic.schema,
"qos": {
"reliable": topic.reliable,
"depth": topic.depth,
},
"load": {
"pending": topic.pending,
"max_depth": topic.max_depth,
"utilization_percent": utilization_percent(topic.pending, topic.max_depth),
},
"endpoints": {
"publishers": topic.publishers,
"subscribers": topic.subscribers,
},
},
})
}
fn stable_action_info_json(
snapshot: &StatusSnapshot,
source: &serde_json::Value,
query_name: &str,
action: &ActionStatus,
) -> serde_json::Value {
serde_json::json!({
"api_version": ACTION_INFO_API_VERSION,
"kind": "action_info",
"captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
"source": source,
"query": {
"action_name": query_name,
},
"result": {
"name": action.name,
"endpoints": {
"clients": action.clients,
"servers": action.servers,
},
"health": {
"current_state": action.current_state,
"active_goals": action.active_goals,
"health_state": action.health_state,
"heartbeat_timeout_ms": action.heartbeat_timeout_ms,
"last_heartbeat_at_unix_nanos": action.last_heartbeat_at_unix_nanos,
"last_feedback_at_unix_nanos": action.last_feedback_at_unix_nanos,
"last_result_at_unix_nanos": action.last_result_at_unix_nanos,
},
},
})
}
fn find_action_status<'a>(snapshot: &'a StatusSnapshot, name: &str) -> Option<&'a ActionStatus> {
snapshot
.actions
.iter()
.find(|action| action_matches(&action.name, name))
}
pub(super) fn action_matches(candidate: &str, input: &str) -> bool {
candidate == input || candidate.ends_with(&format!("/{input}"))
}