use std::fmt;
use thiserror::Error;
use crate::{
snapshot::{CpuMetrics, LoadAverage, MemoryMetrics, StatusSnapshot, SwapMetrics},
SCHEMA_VERSION_V1,
};
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{kind}")]
pub struct ValidationViolation {
pub kind: ViolationKind,
pub field: String,
}
impl ValidationViolation {
fn new(kind: ViolationKind, field: impl Into<String>) -> Self {
Self {
kind,
field: field.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViolationKind {
UnsupportedSchemaVersion { found: u16 },
ZeroNotAllowed,
PercentageNotFinite,
PercentageOutOfRange,
UsedExceedsTotal,
IowaitCapabilityMismatch,
}
impl fmt::Display for ViolationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedSchemaVersion { found } => write!(
f,
"unsupported schema_version {found} (expected {SCHEMA_VERSION_V1})"
),
Self::ZeroNotAllowed => f.write_str("value must be positive"),
Self::PercentageNotFinite => f.write_str("percentage must be finite"),
Self::PercentageOutOfRange => f.write_str("percentage must be in 0.0..=100.0"),
Self::UsedExceedsTotal => f.write_str("used_bytes exceeds total_bytes"),
Self::IowaitCapabilityMismatch => {
f.write_str("iowait_pct must be Some(_) iff cpu_iowait capability is true")
}
}
}
}
pub(crate) fn validate(snap: &StatusSnapshot) -> Result<(), Vec<ValidationViolation>> {
let mut violations = Vec::new();
if snap.schema_version != SCHEMA_VERSION_V1 {
violations.push(ValidationViolation::new(
ViolationKind::UnsupportedSchemaVersion {
found: snap.schema_version,
},
"schema_version",
));
}
if snap.observed_at_unix_ms == 0 {
violations.push(ValidationViolation::new(
ViolationKind::ZeroNotAllowed,
"observed_at_unix_ms",
));
}
if snap.sample_interval_ms == 0 {
violations.push(ValidationViolation::new(
ViolationKind::ZeroNotAllowed,
"sample_interval_ms",
));
}
validate_cpu(&snap.cpu, snap.capabilities.cpu_iowait, &mut violations);
validate_load(&snap.load, &mut violations);
validate_memory(&snap.memory, &mut violations);
validate_swap(&snap.swap, &mut violations);
if violations.is_empty() {
Ok(())
} else {
Err(violations)
}
}
fn validate_cpu(cpu: &CpuMetrics, cpu_iowait: bool, out: &mut Vec<ValidationViolation>) {
if cpu.logical_cores == 0 {
out.push(ValidationViolation::new(
ViolationKind::ZeroNotAllowed,
"cpu.logical_cores",
));
}
check_percentage(cpu.usage_pct, "cpu.usage_pct", out);
match cpu.iowait_pct {
None => {
if cpu_iowait {
out.push(ValidationViolation::new(
ViolationKind::IowaitCapabilityMismatch,
"cpu.iowait_pct",
));
}
}
Some(value) => {
if cpu_iowait {
check_percentage(value, "cpu.iowait_pct", out);
} else {
out.push(ValidationViolation::new(
ViolationKind::IowaitCapabilityMismatch,
"cpu.iowait_pct",
));
}
}
}
}
fn validate_load(load: &LoadAverage, out: &mut Vec<ValidationViolation>) {
check_load(load.one, "load.one", out);
check_load(load.five, "load.five", out);
check_load(load.fifteen, "load.fifteen", out);
}
fn check_load(value: f32, field: &str, out: &mut Vec<ValidationViolation>) {
if !value.is_finite() || value < 0.0 {
out.push(ValidationViolation::new(
ViolationKind::PercentageOutOfRange,
field,
));
}
}
fn validate_memory(memory: &MemoryMetrics, out: &mut Vec<ValidationViolation>) {
check_percentage(memory.usage_pct, "memory.usage_pct", out);
if memory.used_bytes > memory.total_bytes {
out.push(ValidationViolation::new(
ViolationKind::UsedExceedsTotal,
"memory.used_bytes",
));
}
}
fn validate_swap(swap: &SwapMetrics, out: &mut Vec<ValidationViolation>) {
check_percentage(swap.usage_pct, "swap.usage_pct", out);
if swap.used_bytes > swap.total_bytes {
out.push(ValidationViolation::new(
ViolationKind::UsedExceedsTotal,
"swap.used_bytes",
));
}
if swap.total_bytes == 0 && swap.usage_pct != 0.0 {
out.push(ValidationViolation::new(
ViolationKind::PercentageOutOfRange,
"swap.usage_pct",
));
}
}
fn check_percentage(value: f32, field: &str, out: &mut Vec<ValidationViolation>) {
if !value.is_finite() {
out.push(ValidationViolation::new(
ViolationKind::PercentageNotFinite,
field,
));
return;
}
if !(0.0..=100.0).contains(&value) {
out.push(ValidationViolation::new(
ViolationKind::PercentageOutOfRange,
field,
));
}
}