use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WaiverId(String);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EvidenceId(String);
fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
let value = value.into();
if value.trim().is_empty() {
return Err(format!("{kind} cannot be empty"));
}
if value
.chars()
.any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
{
return Err(format!(
"{kind} must use ASCII letters, digits, /, -, _, or ."
));
}
Ok(value)
}
macro_rules! stable_id {
($name:ident, $kind:literal, $doc:literal) => {
#[doc = $doc]
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
Ok(Self(validate_id($kind, value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for $name {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
};
}
stable_id!(
WaiverId,
"waiver-id",
"Stable identity for one declared practice waiver."
);
stable_id!(
EvidenceId,
"evidence-id",
"Stable identity for one invariant-evidence record."
);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InvariantStatus {
Preserved,
Relaxed {
waiver: WaiverId,
},
Violated,
NotApplicable,
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvariantLedgerEntry<R> {
pub rule_id: R,
pub invariant_id: Option<String>,
pub expected_fact: String,
pub observed_fact: String,
pub status: InvariantStatus,
pub evidence_ids: Vec<EvidenceId>,
pub declared_waiver: Option<WaiverId>,
}
impl<R> InvariantLedgerEntry<R> {
pub(crate) fn new(
rule_id: R,
expected_fact: impl Into<String>,
observed_fact: impl Into<String>,
status: InvariantStatus,
evidence_ids: Vec<EvidenceId>,
declared_waiver: Option<WaiverId>,
) -> Self {
Self {
rule_id,
invariant_id: None,
expected_fact: expected_fact.into(),
observed_fact: observed_fact.into(),
status,
evidence_ids,
declared_waiver,
}
}
pub(crate) fn with_invariant_id(mut self, invariant_id: impl Into<String>) -> Self {
self.invariant_id = Some(invariant_id.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvariantLedger<R> {
entries: Vec<InvariantLedgerEntry<R>>,
}
impl<R> InvariantLedger<R> {
pub fn new(entries: Vec<InvariantLedgerEntry<R>>) -> Self {
Self { entries }
}
pub fn entries(&self) -> &[InvariantLedgerEntry<R>] {
&self.entries
}
pub fn is_preserved(&self, invariant_id: &str) -> bool {
self.entries.iter().any(|entry| {
entry.invariant_id.as_deref() == Some(invariant_id)
&& matches!(entry.status, InvariantStatus::Preserved)
})
}
pub fn is_relaxed(&self, invariant_id: &str) -> bool {
self.entries.iter().any(|entry| {
entry.invariant_id.as_deref() == Some(invariant_id)
&& matches!(entry.status, InvariantStatus::Relaxed { .. })
})
}
}