sim_lib_music_serial/
invariant.rs1use std::fmt::{Display, Formatter};
4
5#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct WaiverId(String);
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct EvidenceId(String);
12
13fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
14 let value = value.into();
15 if value.trim().is_empty() {
16 return Err(format!("{kind} cannot be empty"));
17 }
18 if value
19 .chars()
20 .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
21 {
22 return Err(format!(
23 "{kind} must use ASCII letters, digits, /, -, _, or ."
24 ));
25 }
26 Ok(value)
27}
28
29macro_rules! stable_id {
30 ($name:ident, $kind:literal, $doc:literal) => {
31 #[doc = $doc]
32 impl $name {
33 pub fn new(value: impl Into<String>) -> Result<Self, String> {
35 Ok(Self(validate_id($kind, value)?))
36 }
37
38 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42 }
43
44 impl Display for $name {
45 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
46 formatter.write_str(&self.0)
47 }
48 }
49 };
50}
51
52stable_id!(
53 WaiverId,
54 "waiver-id",
55 "Stable identity for one declared practice waiver."
56);
57stable_id!(
58 EvidenceId,
59 "evidence-id",
60 "Stable identity for one invariant-evidence record."
61);
62
63#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum InvariantStatus {
66 Preserved,
68 Relaxed {
70 waiver: WaiverId,
72 },
73 Violated,
75 NotApplicable,
77 Unknown,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct InvariantLedgerEntry<R> {
84 pub rule_id: R,
86 pub invariant_id: Option<String>,
88 pub expected_fact: String,
90 pub observed_fact: String,
92 pub status: InvariantStatus,
94 pub evidence_ids: Vec<EvidenceId>,
96 pub declared_waiver: Option<WaiverId>,
98}
99
100impl<R> InvariantLedgerEntry<R> {
101 pub(crate) fn new(
102 rule_id: R,
103 expected_fact: impl Into<String>,
104 observed_fact: impl Into<String>,
105 status: InvariantStatus,
106 evidence_ids: Vec<EvidenceId>,
107 declared_waiver: Option<WaiverId>,
108 ) -> Self {
109 Self {
110 rule_id,
111 invariant_id: None,
112 expected_fact: expected_fact.into(),
113 observed_fact: observed_fact.into(),
114 status,
115 evidence_ids,
116 declared_waiver,
117 }
118 }
119
120 pub(crate) fn with_invariant_id(mut self, invariant_id: impl Into<String>) -> Self {
121 self.invariant_id = Some(invariant_id.into());
122 self
123 }
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct InvariantLedger<R> {
129 entries: Vec<InvariantLedgerEntry<R>>,
130}
131
132impl<R> InvariantLedger<R> {
133 pub fn new(entries: Vec<InvariantLedgerEntry<R>>) -> Self {
135 Self { entries }
136 }
137
138 pub fn entries(&self) -> &[InvariantLedgerEntry<R>] {
140 &self.entries
141 }
142
143 pub fn is_preserved(&self, invariant_id: &str) -> bool {
145 self.entries.iter().any(|entry| {
146 entry.invariant_id.as_deref() == Some(invariant_id)
147 && matches!(entry.status, InvariantStatus::Preserved)
148 })
149 }
150
151 pub fn is_relaxed(&self, invariant_id: &str) -> bool {
153 self.entries.iter().any(|entry| {
154 entry.invariant_id.as_deref() == Some(invariant_id)
155 && matches!(entry.status, InvariantStatus::Relaxed { .. })
156 })
157 }
158}