fallow_output/
health_trends.rs1use crate::CoverageModel;
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9pub struct HealthTrend {
10 pub compared_to: TrendPoint,
12 pub metrics: Vec<TrendMetric>,
14 pub snapshots_loaded: usize,
16 pub overall_direction: TrendDirection,
18}
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23pub struct TrendPoint {
24 pub timestamp: String,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub git_sha: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub score: Option<f64>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub grade: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub score_formula_version: Option<u32>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub coverage_model: Option<CoverageModel>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub snapshot_schema_version: Option<u32>,
45}
46
47#[must_use]
50pub fn health_score_comparison_note(
51 previous_formula: Option<u32>,
52 current_formula: Option<u32>,
53) -> Option<&'static str> {
54 let (Some(previous), Some(current)) = (previous_formula, current_formula) else {
55 return Some("Score comparison omitted: score formula version is unknown.");
56 };
57 if previous != current {
58 return Some("Score comparison omitted: score formulas differ.");
59 }
60 None
61}
62
63#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66pub struct TrendMetric {
67 #[serde(deserialize_with = "crate::static_str::deserialize")]
69 pub name: crate::static_str::StaticStr,
70 #[serde(deserialize_with = "crate::static_str::deserialize")]
72 pub label: crate::static_str::StaticStr,
73 pub previous: f64,
75 pub current: f64,
77 pub delta: f64,
79 pub direction: TrendDirection,
81 #[serde(deserialize_with = "crate::static_str::deserialize")]
83 pub unit: crate::static_str::StaticStr,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub previous_count: Option<TrendCount>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub current_count: Option<TrendCount>,
90}
91
92#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
94#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
95pub struct TrendCount {
96 pub value: usize,
98 pub total: usize,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[serde(rename_all = "snake_case")]
106pub enum TrendDirection {
107 Improving,
109 Declining,
111 Stable,
113}
114
115impl TrendDirection {
116 #[must_use]
118 pub const fn arrow(self) -> &'static str {
119 match self {
120 Self::Improving => "\u{2191}",
121 Self::Declining => "\u{2193}",
122 Self::Stable => "\u{2192}",
123 }
124 }
125
126 #[must_use]
128 pub const fn label(self) -> &'static str {
129 match self {
130 Self::Improving => "improving",
131 Self::Declining => "declining",
132 Self::Stable => "stable",
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn trend_direction_labels_are_stable() {
143 assert_eq!(TrendDirection::Improving.label(), "improving");
144 assert_eq!(TrendDirection::Declining.label(), "declining");
145 assert_eq!(TrendDirection::Stable.label(), "stable");
146 }
147
148 #[test]
149 fn trend_direction_serializes_as_snake_case() {
150 let value = serde_json::to_value(TrendDirection::Improving).expect("serialize trend");
151 assert_eq!(value, serde_json::json!("improving"));
152 }
153}