use crate::CoverageModel;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct HealthTrend {
pub compared_to: TrendPoint,
pub metrics: Vec<TrendMetric>,
pub snapshots_loaded: usize,
pub overall_direction: TrendDirection,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TrendPoint {
pub timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git_sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grade: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score_formula_version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage_model: Option<CoverageModel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot_schema_version: Option<u32>,
}
#[must_use]
pub fn health_score_comparison_note(
previous_formula: Option<u32>,
current_formula: Option<u32>,
) -> Option<&'static str> {
let (Some(previous), Some(current)) = (previous_formula, current_formula) else {
return Some("Score comparison omitted: score formula version is unknown.");
};
if previous != current {
return Some("Score comparison omitted: score formulas differ.");
}
None
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TrendMetric {
#[serde(deserialize_with = "crate::static_str::deserialize")]
pub name: crate::static_str::StaticStr,
#[serde(deserialize_with = "crate::static_str::deserialize")]
pub label: crate::static_str::StaticStr,
pub previous: f64,
pub current: f64,
pub delta: f64,
pub direction: TrendDirection,
#[serde(deserialize_with = "crate::static_str::deserialize")]
pub unit: crate::static_str::StaticStr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous_count: Option<TrendCount>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_count: Option<TrendCount>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TrendCount {
pub value: usize,
pub total: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum TrendDirection {
Improving,
Declining,
Stable,
}
impl TrendDirection {
#[must_use]
pub const fn arrow(self) -> &'static str {
match self {
Self::Improving => "\u{2191}",
Self::Declining => "\u{2193}",
Self::Stable => "\u{2192}",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Improving => "improving",
Self::Declining => "declining",
Self::Stable => "stable",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trend_direction_labels_are_stable() {
assert_eq!(TrendDirection::Improving.label(), "improving");
assert_eq!(TrendDirection::Declining.label(), "declining");
assert_eq!(TrendDirection::Stable.label(), "stable");
}
#[test]
fn trend_direction_serializes_as_snake_case() {
let value = serde_json::to_value(TrendDirection::Improving).expect("serialize trend");
assert_eq!(value, serde_json::json!("improving"));
}
}