use std::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteStatus {
Ok,
Disabled,
Unavailable,
}
impl WriteStatus {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Disabled => "disabled",
Self::Unavailable => "unavailable",
}
}
#[must_use]
pub fn is_degraded(self) -> bool {
matches!(self, Self::Unavailable)
}
}
#[derive(Debug)]
pub struct RegistryHealth {
writes: RwLock<WriteState>,
}
#[derive(Debug, Clone)]
struct WriteState {
status: WriteStatus,
detail: Option<String>,
}
impl RegistryHealth {
#[must_use]
pub fn new(didcomm_enabled: bool) -> Self {
Self {
writes: RwLock::new(WriteState {
status: if didcomm_enabled {
WriteStatus::Ok
} else {
WriteStatus::Disabled
},
detail: None,
}),
}
}
pub fn mark_writes_unavailable(&self, detail: impl Into<String>) {
let mut guard = self.write_lock();
guard.status = WriteStatus::Unavailable;
guard.detail = Some(detail.into());
}
#[must_use]
pub fn write_status(&self) -> WriteStatus {
self.read_lock().status
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
let state = self.read_lock().clone();
let mut body = serde_json::json!({
"status": if state.status.is_degraded() { "degraded" } else { "OK" },
"writes": state.status.as_str(),
});
if let Some(detail) = state.detail {
body["detail"] = serde_json::Value::String(detail);
}
body
}
fn read_lock(&self) -> std::sync::RwLockReadGuard<'_, WriteState> {
self.writes
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write_lock(&self) -> std::sync::RwLockWriteGuard<'_, WriteState> {
self.writes
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn healthy_reports_ok() {
let health = RegistryHealth::new(true);
assert_eq!(health.to_json(), json!({ "status": "OK", "writes": "ok" }));
assert!(!health.write_status().is_degraded());
}
#[test]
fn disabled_didcomm_is_not_degraded() {
let health = RegistryHealth::new(false);
assert_eq!(
health.to_json(),
json!({ "status": "OK", "writes": "disabled" })
);
assert!(!health.write_status().is_degraded());
}
#[test]
fn failed_listener_degrades_and_explains_why() {
let health = RegistryHealth::new(true);
health.mark_writes_unavailable("mediator unreachable: NXDOMAIN");
assert_eq!(
health.to_json(),
json!({
"status": "degraded",
"writes": "unavailable",
"detail": "mediator unreachable: NXDOMAIN",
})
);
assert!(health.write_status().is_degraded());
}
#[test]
fn disabled_can_still_transition_to_unavailable() {
let health = RegistryHealth::new(false);
health.mark_writes_unavailable("stopped");
assert_eq!(health.write_status(), WriteStatus::Unavailable);
}
#[test]
fn detail_is_absent_until_something_fails() {
let health = RegistryHealth::new(true);
assert!(health.to_json().get("detail").is_none());
}
#[test]
fn wire_values_are_stable() {
assert_eq!(WriteStatus::Ok.as_str(), "ok");
assert_eq!(WriteStatus::Disabled.as_str(), "disabled");
assert_eq!(WriteStatus::Unavailable.as_str(), "unavailable");
}
}