1use core::fmt;
8use core::time::Duration;
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
20pub enum UnavailableReason {
21 CounterReset,
25 DeviceDisappeared,
27 InterfaceRenamed,
29 ProcessExited,
33 ReadFailed,
35 ParseFailed,
37 Timeout,
39 SkippedUnderLoad,
41 LinkSpeedUnknown,
46 NeedsSecondSample,
48}
49
50impl UnavailableReason {
51 #[must_use]
53 pub const fn message(self) -> &'static str {
54 match self {
55 Self::CounterReset => "counter reset",
56 Self::DeviceDisappeared => "device disappeared",
57 Self::InterfaceRenamed => "interface renamed",
58 Self::ProcessExited => "process exited",
59 Self::ReadFailed => "read failed",
60 Self::ParseFailed => "unparsable data",
61 Self::Timeout => "collection timed out",
62 Self::SkippedUnderLoad => "skipped under load",
63 Self::LinkSpeedUnknown => "link speed unknown",
64 Self::NeedsSecondSample => "needs a second sample",
65 }
66 }
67}
68
69impl fmt::Display for UnavailableReason {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(self.message())
72 }
73}
74
75#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
85pub enum MetricState<T> {
86 Available(T),
88 Stale {
92 value: T,
94 age: Duration,
96 },
97 WarmingUp,
101 PermissionDenied,
103 Unsupported,
105 TemporarilyUnavailable(UnavailableReason),
107}
108
109impl<T> MetricState<T> {
110 #[must_use]
115 pub const fn fresh(&self) -> Option<&T> {
116 match self {
117 Self::Available(value) => Some(value),
118 _ => None,
119 }
120 }
121
122 #[must_use]
127 pub const fn displayable(&self) -> Option<(&T, Duration)> {
128 match self {
129 Self::Available(value) => Some((value, Duration::ZERO)),
130 Self::Stale { value, age } => Some((value, *age)),
131 _ => None,
132 }
133 }
134
135 #[must_use]
137 pub const fn is_available(&self) -> bool {
138 matches!(self, Self::Available(_))
139 }
140
141 #[must_use]
143 pub const fn is_stale(&self) -> bool {
144 matches!(self, Self::Stale { .. })
145 }
146
147 #[must_use]
151 pub const fn is_unsupported(&self) -> bool {
152 matches!(self, Self::Unsupported)
153 }
154
155 #[must_use]
157 pub const fn is_warming_up(&self) -> bool {
158 matches!(self, Self::WarmingUp)
159 }
160
161 #[must_use]
163 pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> MetricState<U> {
164 match self {
165 Self::Available(value) => MetricState::Available(f(value)),
166 Self::Stale { value, age } => MetricState::Stale {
167 value: f(value),
168 age,
169 },
170 Self::WarmingUp => MetricState::WarmingUp,
171 Self::PermissionDenied => MetricState::PermissionDenied,
172 Self::Unsupported => MetricState::Unsupported,
173 Self::TemporarilyUnavailable(reason) => MetricState::TemporarilyUnavailable(reason),
174 }
175 }
176
177 #[must_use]
179 pub const fn as_ref(&self) -> MetricState<&T> {
180 match self {
181 Self::Available(value) => MetricState::Available(value),
182 Self::Stale { value, age } => MetricState::Stale { value, age: *age },
183 Self::WarmingUp => MetricState::WarmingUp,
184 Self::PermissionDenied => MetricState::PermissionDenied,
185 Self::Unsupported => MetricState::Unsupported,
186 Self::TemporarilyUnavailable(reason) => MetricState::TemporarilyUnavailable(*reason),
187 }
188 }
189
190 #[must_use]
196 pub const fn placeholder(&self) -> Option<&'static str> {
197 match self {
198 Self::Available(_) | Self::Stale { .. } => None,
199 Self::WarmingUp => Some("warming up"),
200 Self::PermissionDenied => Some("permission denied"),
201 Self::Unsupported => Some("n/a"),
202 Self::TemporarilyUnavailable(reason) => Some(reason.message()),
203 }
204 }
205
206 #[must_use]
208 pub const fn symbol(&self) -> char {
209 match self {
210 Self::Available(_) => ' ',
211 Self::Stale { .. } => '~',
212 Self::WarmingUp => '.',
213 Self::PermissionDenied => '!',
214 Self::Unsupported => '-',
215 Self::TemporarilyUnavailable(_) => '?',
216 }
217 }
218
219 #[must_use]
225 pub fn into_stale(self, age: Duration) -> Self {
226 match self {
227 Self::Available(value) => Self::Stale { value, age },
228 other => other,
229 }
230 }
231}
232
233impl<T> From<Option<T>> for MetricState<T> {
234 fn from(value: Option<T>) -> Self {
239 match value {
240 Some(value) => Self::Available(value),
241 None => Self::Unsupported,
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn unavailable_is_never_zero() {
252 let state: MetricState<u64> = MetricState::PermissionDenied;
255 assert!(state.fresh().is_none());
256 assert!(state.displayable().is_none());
257 assert_eq!(state.placeholder(), Some("permission denied"));
258 }
259
260 #[test]
261 fn warming_up_is_distinct_from_zero() {
262 let warming: MetricState<u64> = MetricState::WarmingUp;
263 let zero = MetricState::Available(0u64);
264 assert_ne!(warming, zero);
265 assert_eq!(warming.fresh(), None);
266 assert_eq!(zero.fresh(), Some(&0));
267 }
268
269 #[test]
270 fn stale_values_are_only_readable_together_with_their_age() {
271 let state = MetricState::Available(42u64).into_stale(Duration::from_secs(3));
272 assert!(state.is_stale());
273 assert_eq!(state.fresh(), None);
275 let (value, age) = state.displayable().expect("stale values are displayable");
277 assert_eq!(*value, 42);
278 assert_eq!(age, Duration::from_secs(3));
279 }
280
281 #[test]
282 fn staleness_cannot_be_applied_to_a_missing_value() {
283 let state: MetricState<u64> = MetricState::Unsupported.into_stale(Duration::from_secs(9));
284 assert_eq!(state, MetricState::Unsupported);
285 let state: MetricState<u64> = MetricState::WarmingUp.into_stale(Duration::from_secs(9));
286 assert_eq!(state, MetricState::WarmingUp);
287 }
288
289 #[test]
290 fn staleness_does_not_compound() {
291 let once = MetricState::Available(7u64).into_stale(Duration::from_secs(1));
292 let twice = once.into_stale(Duration::from_secs(30));
293 assert_eq!(
294 once, twice,
295 "re-staling must not overwrite the original age"
296 );
297 }
298
299 #[test]
300 fn map_preserves_availability_state() {
301 assert_eq!(
302 MetricState::Available(2u64).map(|v| v * 2),
303 MetricState::Available(4u64)
304 );
305 let stale = MetricState::Stale {
306 value: 2u64,
307 age: Duration::from_secs(5),
308 };
309 assert_eq!(
310 stale.map(|v| v * 2),
311 MetricState::Stale {
312 value: 4,
313 age: Duration::from_secs(5)
314 }
315 );
316 let denied: MetricState<u64> = MetricState::PermissionDenied;
317 assert_eq!(denied.map(|v| v * 2), MetricState::PermissionDenied);
318 }
319
320 #[test]
321 fn every_state_has_a_redundant_non_color_cue() {
322 let states: [MetricState<u64>; 6] = [
323 MetricState::Available(1),
324 MetricState::Stale {
325 value: 1,
326 age: Duration::ZERO,
327 },
328 MetricState::WarmingUp,
329 MetricState::PermissionDenied,
330 MetricState::Unsupported,
331 MetricState::TemporarilyUnavailable(UnavailableReason::ReadFailed),
332 ];
333 let mut symbols: Vec<char> = states.iter().map(MetricState::symbol).collect();
334 symbols.sort_unstable();
335 symbols.dedup();
336 assert_eq!(
337 symbols.len(),
338 states.len(),
339 "symbols must be distinguishable"
340 );
341 }
342
343 #[test]
344 fn placeholders_are_strict_ascii_so_they_are_legal_in_both_glyph_modes() {
345 let reasons = [
346 UnavailableReason::CounterReset,
347 UnavailableReason::DeviceDisappeared,
348 UnavailableReason::InterfaceRenamed,
349 UnavailableReason::ProcessExited,
350 UnavailableReason::ReadFailed,
351 UnavailableReason::ParseFailed,
352 UnavailableReason::Timeout,
353 UnavailableReason::SkippedUnderLoad,
354 UnavailableReason::LinkSpeedUnknown,
355 UnavailableReason::NeedsSecondSample,
356 ];
357 for reason in reasons {
358 assert!(
359 reason.message().is_ascii(),
360 "{reason:?} message is not strict ASCII"
361 );
362 }
363 for text in ["warming up", "permission denied", "n/a"] {
364 assert!(text.is_ascii());
365 }
366 }
367}