use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dashboard {
pub generated_at_unix_ms: u64,
pub cpu_count: usize,
pub containers: Vec<ContainerMetrics>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerMetrics {
pub id: String,
pub name: String,
pub image: String,
pub state: ContainerState,
pub status: String,
pub health: HealthState,
pub stack: Option<String>,
pub cpu_percent: Option<f64>,
pub mem_used: Option<u64>,
pub mem_limit: Option<u64>,
pub net_rx_bps: Option<f64>,
pub net_tx_bps: Option<f64>,
pub disk_read_bps: Option<f64>,
pub disk_write_bps: Option<f64>,
pub ports: Vec<Port>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Port {
pub private: u16,
pub public: Option<u16>,
pub proto: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContainerState {
Created,
Running,
Paused,
Restarting,
Exited,
Removing,
Dead,
Stopping,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HealthState {
Healthy,
Unhealthy,
Starting,
#[serde(other)]
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostEntry {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
pub version: String,
#[serde(default)]
pub read_only: bool,
pub dashboard: Dashboard,
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_snapshot() -> Snapshot {
Snapshot {
version: "0.10.0".into(),
read_only: true,
dashboard: Dashboard {
generated_at_unix_ms: 1_720_000_000_000,
cpu_count: 8,
containers: vec![ContainerMetrics {
id: "deadbeef".into(),
name: "web".into(),
image: "nginx:latest".into(),
state: ContainerState::Running,
status: "Up 2 hours (healthy)".into(),
health: HealthState::Healthy,
stack: Some("blog".into()),
cpu_percent: Some(12.5),
mem_used: Some(64 << 20),
mem_limit: None,
net_rx_bps: Some(1024.0),
net_tx_bps: None,
disk_read_bps: None,
disk_write_bps: Some(0.0),
ports: vec![Port {
private: 80,
public: Some(8080),
proto: "tcp".into(),
}],
}],
},
}
}
#[test]
fn snapshot_roundtrips_through_json() {
let snap = sample_snapshot();
let json = serde_json::to_string(&snap).expect("serialize");
let back: Snapshot = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.version, snap.version);
assert!(back.read_only);
let (a, b) = (&back.dashboard.containers[0], &snap.dashboard.containers[0]);
assert_eq!(a.id, b.id);
assert_eq!(a.state, b.state);
assert_eq!(a.health, b.health);
assert_eq!(a.cpu_percent, b.cpu_percent);
assert_eq!(a.ports, b.ports);
}
#[test]
fn snapshot_tolerates_additive_changes_from_newer_nodes() {
let json = r#"{
"version": "0.99.0",
"read_only": false,
"brand_new_field": {"nested": true},
"dashboard": {
"generated_at_unix_ms": 1,
"cpu_count": 2,
"future_field": 42,
"containers": [{
"id": "x", "name": "n", "image": "i",
"state": "hibernating",
"status": "s",
"health": "quantum",
"stack": null,
"cpu_percent": null, "mem_used": null, "mem_limit": null,
"net_rx_bps": null, "net_tx_bps": null,
"disk_read_bps": null, "disk_write_bps": null,
"ports": []
}]
}
}"#;
let snap: Snapshot = serde_json::from_str(json).expect("tolerant deserialize");
let c = &snap.dashboard.containers[0];
assert_eq!(c.state, ContainerState::Unknown);
assert_eq!(c.health, HealthState::None);
}
#[test]
fn snapshot_defaults_missing_read_only() {
let json = r#"{"version":"0.10.0","dashboard":{"generated_at_unix_ms":1,"cpu_count":1,"containers":[]}}"#;
let snap: Snapshot = serde_json::from_str(json).expect("deserialize");
assert!(!snap.read_only);
}
}