robotrt-cli 0.1.0-beta.1

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

#[allow(clippy::too_many_arguments)]
pub(in crate::commands::ops) fn build_ops_diff_payload(
    current: &StatusSnapshot,
    current_source: &OpsSource,
    baseline: &StatusSnapshot,
    baseline_source: &OpsSource,
    current_alerts: &[OpsAlert],
    baseline_alerts: &[OpsAlert],
    current_health: &str,
    baseline_health: &str,
) -> serde_json::Value {
    let (added_edges, removed_edges) = diff_topology_edges(current, baseline);
    let mission_changes = diff_mission_states(current, baseline);
    let plugin_changes = diff_plugin_states(current, baseline);
    let health_changes = diff_health_states(current, baseline);

    serde_json::json!({
        "api_version": OPS_DIFF_API_VERSION,
        "kind": "ops_diff",
        "captured_at_unix_nanos": current.captured_at_unix_nanos,
        "source": {
            "current": current_source.json,
            "baseline": baseline_source.json,
        },
        "result": {
            "summary": {
                "overall_health": {
                    "current": current_health,
                    "baseline": baseline_health,
                },
                "alerts": {
                    "current": current_alerts.len(),
                    "baseline": baseline_alerts.len(),
                },
            },
            "delta": {
                "nodes": count_delta(current.nodes.len(), baseline.nodes.len()),
                "topics": count_delta(current.topics.len(), baseline.topics.len()),
                "services": count_delta(current.services.len(), baseline.services.len()),
                "actions": count_delta(current.actions.len(), baseline.actions.len()),
                "missions": count_delta(current.missions.len(), baseline.missions.len()),
                "plugins_loaded": count_delta(
                    current.plugins.iter().filter(|plugin| plugin.loaded).count(),
                    baseline.plugins.iter().filter(|plugin| plugin.loaded).count(),
                ),
            },
            "topology": {
                "added_edges": added_edges,
                "removed_edges": removed_edges,
            },
            "missions": {
                "changed": mission_changes,
            },
            "plugins": {
                "changed": plugin_changes,
            },
            "health": {
                "changed": health_changes,
            }
        }
    })
}

pub(in crate::commands::ops) fn count_delta(current: usize, baseline: usize) -> serde_json::Value {
    let current_i64 = current as i64;
    let baseline_i64 = baseline as i64;
    serde_json::json!({
        "current": current,
        "baseline": baseline,
        "delta": current_i64 - baseline_i64,
    })
}

pub(in crate::commands::ops) fn diff_topology_edges(
    current: &StatusSnapshot,
    baseline: &StatusSnapshot,
) -> (Vec<String>, Vec<String>) {
    let current_edges = current
        .edges
        .iter()
        .map(|edge| format!("{} --{}--> {}", edge.from, edge.relation, edge.to))
        .collect::<HashSet<_>>();
    let baseline_edges = baseline
        .edges
        .iter()
        .map(|edge| format!("{} --{}--> {}", edge.from, edge.relation, edge.to))
        .collect::<HashSet<_>>();

    let mut added = current_edges
        .difference(&baseline_edges)
        .cloned()
        .collect::<Vec<_>>();
    let mut removed = baseline_edges
        .difference(&current_edges)
        .cloned()
        .collect::<Vec<_>>();
    added.sort();
    removed.sort();
    (added, removed)
}

pub(in crate::commands::ops) fn diff_mission_states(
    current: &StatusSnapshot,
    baseline: &StatusSnapshot,
) -> Vec<serde_json::Value> {
    let mut current_map = BTreeMap::new();
    for mission in &current.missions {
        current_map.insert(mission.name.clone(), mission.state.clone());
    }

    let mut baseline_map = BTreeMap::new();
    for mission in &baseline.missions {
        baseline_map.insert(mission.name.clone(), mission.state.clone());
    }

    let mut names = current_map.keys().cloned().collect::<HashSet<_>>();
    names.extend(baseline_map.keys().cloned());

    let mut keys = names.into_iter().collect::<Vec<_>>();
    keys.sort();

    keys.into_iter()
        .filter_map(|name| {
            let current_state = current_map
                .get(&name)
                .cloned()
                .unwrap_or_else(|| "<absent>".to_string());
            let baseline_state = baseline_map
                .get(&name)
                .cloned()
                .unwrap_or_else(|| "<absent>".to_string());
            if current_state == baseline_state {
                return None;
            }
            Some(serde_json::json!({
                "name": name,
                "current": current_state,
                "baseline": baseline_state,
            }))
        })
        .collect()
}

pub(in crate::commands::ops) fn diff_plugin_states(
    current: &StatusSnapshot,
    baseline: &StatusSnapshot,
) -> Vec<serde_json::Value> {
    let mut current_map = BTreeMap::new();
    for plugin in &current.plugins {
        current_map.insert(plugin.name.clone(), plugin.loaded);
    }

    let mut baseline_map = BTreeMap::new();
    for plugin in &baseline.plugins {
        baseline_map.insert(plugin.name.clone(), plugin.loaded);
    }

    let mut names = current_map.keys().cloned().collect::<HashSet<_>>();
    names.extend(baseline_map.keys().cloned());

    let mut keys = names.into_iter().collect::<Vec<_>>();
    keys.sort();

    keys.into_iter()
        .filter_map(|name| {
            let current_loaded = current_map.get(&name).copied().unwrap_or(false);
            let baseline_loaded = baseline_map.get(&name).copied().unwrap_or(false);
            if current_loaded == baseline_loaded {
                return None;
            }
            Some(serde_json::json!({
                "name": name,
                "current": current_loaded,
                "baseline": baseline_loaded,
            }))
        })
        .collect()
}

pub(in crate::commands::ops) fn diff_health_states(
    current: &StatusSnapshot,
    baseline: &StatusSnapshot,
) -> Vec<serde_json::Value> {
    let mut current_map = BTreeMap::new();
    for item in &current.health {
        let reason = item.reason.clone().unwrap_or_default();
        current_map.insert(
            item.component.clone(),
            format!("{}|{}", item.status, reason),
        );
    }

    let mut baseline_map = BTreeMap::new();
    for item in &baseline.health {
        let reason = item.reason.clone().unwrap_or_default();
        baseline_map.insert(
            item.component.clone(),
            format!("{}|{}", item.status, reason),
        );
    }

    let mut names = current_map.keys().cloned().collect::<HashSet<_>>();
    names.extend(baseline_map.keys().cloned());

    let mut keys = names.into_iter().collect::<Vec<_>>();
    keys.sort();

    keys.into_iter()
        .filter_map(|name| {
            let current_state = current_map
                .get(&name)
                .cloned()
                .unwrap_or_else(|| "<absent>|".to_string());
            let baseline_state = baseline_map
                .get(&name)
                .cloned()
                .unwrap_or_else(|| "<absent>|".to_string());
            if current_state == baseline_state {
                return None;
            }
            Some(serde_json::json!({
                "component": name,
                "current": current_state,
                "baseline": baseline_state,
            }))
        })
        .collect()
}