crates/monetize-guardian/src/case.rs
//! **A case**: one thing wrong with one estate, from detection to done, with who pressed and
//! what happened — the receipt.
use monetize_product::guardian::{Finding, Remedy, Step};
/// detected → investigating → approved → running → done | failed; `resolved` when the
/// estate recovered on its own before anyone pressed.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CaseState {
#[default]
Detected,
Investigating,
Approved,
Running,
Done,
Failed,
Resolved,
}
impl CaseState {
/// Still being worked on: at most one per estate.
pub fn open(self) -> bool {
matches!(self, CaseState::Detected | CaseState::Investigating | CaseState::Approved | CaseState::Running)
}
pub fn as_str(self) -> &'static str {
match self {
CaseState::Detected => "detected",
CaseState::Investigating => "investigating",
CaseState::Approved => "approved",
CaseState::Running => "running",
CaseState::Done => "done",
CaseState::Failed => "failed",
CaseState::Resolved => "resolved",
}
}
}
/// Where a step of the plan stands.
pub mod step_state {
pub const PENDING: &str = "pending";
pub const RUNNING: &str = "running";
pub const MET: &str = "met";
pub const FAILED: &str = "failed";
}
/// One step of the case's plan, and how it went.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct CaseStep {
pub step: Step,
/// [`step_state`].
pub state: String,
#[serde(default)]
pub started_unix_ms: u64,
#[serde(default)]
pub done_unix_ms: u64,
#[serde(default)]
pub words: String,
/// The order's seq, for an order step that was queued; 0 otherwise.
#[serde(default)]
pub seq: u64,
/// What a fence step asked the provider to give back (its ids), once; empty before.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub asked: Vec<String>,
/// **A fence step's proof** — the old box the provider's listing read back gone, and
/// when; empty while unconfirmed, and for a fence that had no laid record to give back
/// ("nothing to fence" is no fence). A promotion needs it ([`crate::runner`]).
#[serde(default, skip_serializing_if = "String::is_empty")]
pub fenced: String,
}
/// One line of the case's log.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Line {
pub at_unix_ms: u64,
pub words: String,
}
/// **The case** — stored whole, and its JSON is the receipt.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Case {
/// `<instance>-<n>`.
pub id: String,
pub instance: String,
pub product: String,
pub state: CaseState,
pub opened_unix_ms: u64,
pub updated_unix_ms: u64,
pub finding: Finding,
/// The remedy pressed (or auto-fix took).
pub remedy: Option<Remedy>,
/// WHO approved: the operator's principal, or [`crate::AUTO_PRINCIPAL`].
pub approved_by: String,
pub approved_unix_ms: u64,
/// Approved by the estate's auto-fix opt-in, not a human.
pub auto: bool,
/// The fence epoch this case's orders carry.
pub epoch: u64,
pub steps: Vec<CaseStep>,
/// Evidence and progress, in order.
pub log: Vec<Line>,
pub outcome: String,
}
impl Case {
pub fn say(&mut self, now_unix_ms: u64, words: impl Into<String>) {
self.updated_unix_ms = now_unix_ms;
self.log.push(Line { at_unix_ms: now_unix_ms, words: words.into() });
}
/// The buttons this case offers now: its finding's remedies while nobody pressed.
pub fn buttons(&self) -> Vec<Remedy> {
if matches!(self.state, CaseState::Detected | CaseState::Investigating) {
self.finding.remedies.clone()
} else {
Vec::new()
}
}
/// The receipt: the case, whole, as JSON.
pub fn receipt_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
}