use core::fmt;
use core::time::Duration;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum UnavailableReason {
CounterReset,
DeviceDisappeared,
InterfaceRenamed,
ProcessExited,
ReadFailed,
ParseFailed,
Timeout,
SkippedUnderLoad,
LinkSpeedUnknown,
NeedsSecondSample,
}
impl UnavailableReason {
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::CounterReset => "counter reset",
Self::DeviceDisappeared => "device disappeared",
Self::InterfaceRenamed => "interface renamed",
Self::ProcessExited => "process exited",
Self::ReadFailed => "read failed",
Self::ParseFailed => "unparsable data",
Self::Timeout => "collection timed out",
Self::SkippedUnderLoad => "skipped under load",
Self::LinkSpeedUnknown => "link speed unknown",
Self::NeedsSecondSample => "needs a second sample",
}
}
}
impl fmt::Display for UnavailableReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.message())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MetricState<T> {
Available(T),
Stale {
value: T,
age: Duration,
},
WarmingUp,
PermissionDenied,
Unsupported,
TemporarilyUnavailable(UnavailableReason),
}
impl<T> MetricState<T> {
#[must_use]
pub const fn fresh(&self) -> Option<&T> {
match self {
Self::Available(value) => Some(value),
_ => None,
}
}
#[must_use]
pub const fn displayable(&self) -> Option<(&T, Duration)> {
match self {
Self::Available(value) => Some((value, Duration::ZERO)),
Self::Stale { value, age } => Some((value, *age)),
_ => None,
}
}
#[must_use]
pub const fn is_available(&self) -> bool {
matches!(self, Self::Available(_))
}
#[must_use]
pub const fn is_stale(&self) -> bool {
matches!(self, Self::Stale { .. })
}
#[must_use]
pub const fn is_unsupported(&self) -> bool {
matches!(self, Self::Unsupported)
}
#[must_use]
pub const fn is_warming_up(&self) -> bool {
matches!(self, Self::WarmingUp)
}
#[must_use]
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> MetricState<U> {
match self {
Self::Available(value) => MetricState::Available(f(value)),
Self::Stale { value, age } => MetricState::Stale {
value: f(value),
age,
},
Self::WarmingUp => MetricState::WarmingUp,
Self::PermissionDenied => MetricState::PermissionDenied,
Self::Unsupported => MetricState::Unsupported,
Self::TemporarilyUnavailable(reason) => MetricState::TemporarilyUnavailable(reason),
}
}
#[must_use]
pub const fn as_ref(&self) -> MetricState<&T> {
match self {
Self::Available(value) => MetricState::Available(value),
Self::Stale { value, age } => MetricState::Stale { value, age: *age },
Self::WarmingUp => MetricState::WarmingUp,
Self::PermissionDenied => MetricState::PermissionDenied,
Self::Unsupported => MetricState::Unsupported,
Self::TemporarilyUnavailable(reason) => MetricState::TemporarilyUnavailable(*reason),
}
}
#[must_use]
pub const fn placeholder(&self) -> Option<&'static str> {
match self {
Self::Available(_) | Self::Stale { .. } => None,
Self::WarmingUp => Some("warming up"),
Self::PermissionDenied => Some("permission denied"),
Self::Unsupported => Some("n/a"),
Self::TemporarilyUnavailable(reason) => Some(reason.message()),
}
}
#[must_use]
pub const fn symbol(&self) -> char {
match self {
Self::Available(_) => ' ',
Self::Stale { .. } => '~',
Self::WarmingUp => '.',
Self::PermissionDenied => '!',
Self::Unsupported => '-',
Self::TemporarilyUnavailable(_) => '?',
}
}
#[must_use]
pub fn into_stale(self, age: Duration) -> Self {
match self {
Self::Available(value) => Self::Stale { value, age },
other => other,
}
}
}
impl<T> From<Option<T>> for MetricState<T> {
fn from(value: Option<T>) -> Self {
match value {
Some(value) => Self::Available(value),
None => Self::Unsupported,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unavailable_is_never_zero() {
let state: MetricState<u64> = MetricState::PermissionDenied;
assert!(state.fresh().is_none());
assert!(state.displayable().is_none());
assert_eq!(state.placeholder(), Some("permission denied"));
}
#[test]
fn warming_up_is_distinct_from_zero() {
let warming: MetricState<u64> = MetricState::WarmingUp;
let zero = MetricState::Available(0u64);
assert_ne!(warming, zero);
assert_eq!(warming.fresh(), None);
assert_eq!(zero.fresh(), Some(&0));
}
#[test]
fn stale_values_are_only_readable_together_with_their_age() {
let state = MetricState::Available(42u64).into_stale(Duration::from_secs(3));
assert!(state.is_stale());
assert_eq!(state.fresh(), None);
let (value, age) = state.displayable().expect("stale values are displayable");
assert_eq!(*value, 42);
assert_eq!(age, Duration::from_secs(3));
}
#[test]
fn staleness_cannot_be_applied_to_a_missing_value() {
let state: MetricState<u64> = MetricState::Unsupported.into_stale(Duration::from_secs(9));
assert_eq!(state, MetricState::Unsupported);
let state: MetricState<u64> = MetricState::WarmingUp.into_stale(Duration::from_secs(9));
assert_eq!(state, MetricState::WarmingUp);
}
#[test]
fn staleness_does_not_compound() {
let once = MetricState::Available(7u64).into_stale(Duration::from_secs(1));
let twice = once.into_stale(Duration::from_secs(30));
assert_eq!(
once, twice,
"re-staling must not overwrite the original age"
);
}
#[test]
fn map_preserves_availability_state() {
assert_eq!(
MetricState::Available(2u64).map(|v| v * 2),
MetricState::Available(4u64)
);
let stale = MetricState::Stale {
value: 2u64,
age: Duration::from_secs(5),
};
assert_eq!(
stale.map(|v| v * 2),
MetricState::Stale {
value: 4,
age: Duration::from_secs(5)
}
);
let denied: MetricState<u64> = MetricState::PermissionDenied;
assert_eq!(denied.map(|v| v * 2), MetricState::PermissionDenied);
}
#[test]
fn every_state_has_a_redundant_non_color_cue() {
let states: [MetricState<u64>; 6] = [
MetricState::Available(1),
MetricState::Stale {
value: 1,
age: Duration::ZERO,
},
MetricState::WarmingUp,
MetricState::PermissionDenied,
MetricState::Unsupported,
MetricState::TemporarilyUnavailable(UnavailableReason::ReadFailed),
];
let mut symbols: Vec<char> = states.iter().map(MetricState::symbol).collect();
symbols.sort_unstable();
symbols.dedup();
assert_eq!(
symbols.len(),
states.len(),
"symbols must be distinguishable"
);
}
#[test]
fn placeholders_are_strict_ascii_so_they_are_legal_in_both_glyph_modes() {
let reasons = [
UnavailableReason::CounterReset,
UnavailableReason::DeviceDisappeared,
UnavailableReason::InterfaceRenamed,
UnavailableReason::ProcessExited,
UnavailableReason::ReadFailed,
UnavailableReason::ParseFailed,
UnavailableReason::Timeout,
UnavailableReason::SkippedUnderLoad,
UnavailableReason::LinkSpeedUnknown,
UnavailableReason::NeedsSecondSample,
];
for reason in reasons {
assert!(
reason.message().is_ascii(),
"{reason:?} message is not strict ASCII"
);
}
for text in ["warming up", "permission denied", "n/a"] {
assert!(text.is_ascii());
}
}
}