use crate::{EncodingSnapshot, Fingerprint, Manifest, MechanismSnapshot, Tier};
pub const GENERATED: &str = "# generated by `cargo conformance --bless`; do not edit by hand";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub key: String,
pub tier: Tier,
pub fingerprint: Fingerprint,
}
#[derive(Debug, Clone, Default)]
pub struct Snapshot {
pub encodings: Vec<EncodingSnapshot>,
pub mechanisms: Vec<MechanismSnapshot>,
}
impl Snapshot {
pub fn manifest(&self) -> Manifest {
Manifest { encodings: self.encodings.clone(), mechanisms: self.mechanisms.clone() }
}
pub fn encoding_entries(&self) -> Vec<Entry> {
let mut v: Vec<Entry> = self
.encodings
.iter()
.map(|e| Entry { key: e.name.clone(), tier: e.tier, fingerprint: e.fingerprint })
.collect();
v.sort_by(|a, b| a.key.cmp(&b.key));
v
}
pub fn mechanism_entries(&self) -> Vec<Entry> {
let mut v: Vec<Entry> = self
.mechanisms
.iter()
.map(|m| Entry {
key: format!("{} :: \"{}\"", m.mechanism, m.scenario),
tier: m.tier,
fingerprint: m.fingerprint,
})
.collect();
v.sort_by(|a, b| a.key.cmp(&b.key));
v
}
pub fn duplicate_keys(&self) -> Vec<String> {
let mut out = Vec::new();
for entries in [self.encoding_entries(), self.mechanism_entries()] {
for w in entries.windows(2) {
if w[0].key == w[1].key {
out.push(w[0].key.clone());
}
}
}
out
}
pub fn render(&self) -> Vec<(&'static str, String)> {
vec![
("encoding.snap", render(&self.encoding_entries(), "encoding.snap โ canonical encodings")),
("mechanism.snap", render(&self.mechanism_entries(), "mechanism.snap โ mechanism outputs on fixed scenarios")),
("manifest.snap", render_manifest(self.manifest().root(), self.encodings.len(), self.mechanisms.len())),
]
}
}
pub fn render(entries: &[Entry], title: &str) -> String {
let width = entries.iter().map(|e| e.key.len()).max().unwrap_or(0);
let mut s = String::new();
s.push_str(GENERATED);
s.push('\n');
s.push_str("# ");
s.push_str(title);
s.push('\n');
for e in entries {
s.push_str(&format!(
"{:<width$} tier={:<7} {}\n",
e.key,
e.tier.name(),
e.fingerprint.to_snap_string()
));
}
s
}
pub fn render_manifest(root: Fingerprint, encodings: usize, mechanisms: usize) -> String {
format!(
"{GENERATED}\n# manifest.snap โ hemera root over every encoding and mechanism entry\n\
encodings {encodings}\nmechanisms {mechanisms}\nroot {}\n",
root.to_snap_string()
)
}
pub fn parse(src: &str) -> Result<Vec<Entry>, String> {
let mut out = Vec::new();
for (n, line) in src.lines().enumerate() {
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
let bad = || format!("line {}: not a snapshot entry: {line}", n + 1);
let fp_at = line.rfind(" h").ok_or_else(bad)?;
let fingerprint = Fingerprint::from_snap_string(line[fp_at + 1..].trim()).ok_or_else(bad)?;
let head = line[..fp_at].trim_end();
let tier_at = head.rfind("tier=").ok_or_else(bad)?;
let tier = Tier::parse(head[tier_at + 5..].trim()).ok_or_else(bad)?;
let key = head[..tier_at].trim_end().to_string();
if key.is_empty() {
return Err(bad());
}
out.push(Entry { key, tier, fingerprint });
}
Ok(out)
}
pub fn parse_manifest_root(src: &str) -> Option<Fingerprint> {
src.lines()
.find_map(|l| l.strip_prefix("root"))
.and_then(|r| Fingerprint::from_snap_string(r.trim()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
Added(Entry),
Removed(Entry),
Drift {
committed: Entry,
current: Fingerprint,
},
Retier {
committed: Entry,
current: Tier,
},
}
impl Change {
pub fn fails(&self) -> bool {
match self {
Change::Added(_) | Change::Removed(_) | Change::Retier { .. } => true,
Change::Drift { committed, .. } => committed.tier.enforces(),
}
}
pub fn ceremony(&self) -> Tier {
match self {
Change::Added(e) => e.tier,
Change::Removed(e) => e.tier,
Change::Drift { committed, .. } => committed.tier,
Change::Retier { committed, current } => committed.tier.max(*current),
}
}
pub fn describe(&self) -> String {
match self {
Change::Added(e) => format!("added {} tier={} {}", e.key, e.tier.name(), e.fingerprint.to_snap_string()),
Change::Removed(e) => format!("removed {} tier={} {}", e.key, e.tier.name(), e.fingerprint.to_snap_string()),
Change::Drift { committed, current } => format!(
"drift {} tier={} {} -> {}",
committed.key,
committed.tier.name(),
committed.fingerprint.to_snap_string(),
current.to_snap_string()
),
Change::Retier { committed, current } => format!(
"retier {} tier={} -> {}",
committed.key,
committed.tier.name(),
current.name()
),
}
}
}
pub fn diff(committed: &[Entry], current: &[Entry]) -> Vec<Change> {
use std::collections::BTreeMap;
let old: BTreeMap<&str, &Entry> = committed.iter().map(|e| (e.key.as_str(), e)).collect();
let new: BTreeMap<&str, &Entry> = current.iter().map(|e| (e.key.as_str(), e)).collect();
let mut out = Vec::new();
for (k, o) in &old {
match new.get(k) {
None => out.push(Change::Removed((*o).clone())),
Some(n) if n.fingerprint != o.fingerprint => {
out.push(Change::Drift { committed: (*o).clone(), current: n.fingerprint })
}
Some(n) if n.tier != o.tier => out.push(Change::Retier { committed: (*o).clone(), current: n.tier }),
Some(_) => {}
}
}
for (k, n) in &new {
if !old.contains_key(k) {
out.push(Change::Added((*n).clone()));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn snap() -> Snapshot {
Snapshot {
encodings: vec![EncodingSnapshot { name: "a::T@v1".into(), tier: Tier::Gamma, fingerprint: Fingerprint::of(b"a") }],
mechanisms: vec![
MechanismSnapshot { mechanism: "m::f@v1".into(), scenario: "s 1".into(), tier: Tier::Beta, fingerprint: Fingerprint::of(b"m") },
MechanismSnapshot { mechanism: "m::f@v1".into(), scenario: "s 0".into(), tier: Tier::Gamma, fingerprint: Fingerprint::of(b"n") },
],
}
}
#[test]
fn render_parse_roundtrip() {
let s = snap();
let files = s.render();
assert_eq!(parse(&files[0].1).unwrap(), s.encoding_entries());
assert_eq!(parse(&files[1].1).unwrap(), s.mechanism_entries());
assert_eq!(parse_manifest_root(&files[2].1), Some(s.manifest().root()));
let mut r = s.clone();
r.mechanisms.reverse();
assert_eq!(r.render(), files);
}
#[test]
fn diff_classifies() {
let s = snap();
let committed = s.mechanism_entries();
let mut current = committed.clone();
current[0].fingerprint = Fingerprint::of(b"other"); current[1].fingerprint = Fingerprint::of(b"other"); let d = diff(&committed, ¤t);
assert_eq!(d.len(), 2);
assert!(d.iter().any(|c| c.fails()));
assert!(d.iter().any(|c| !c.fails()), "beta drift reports, does not fail");
current.pop();
let d = diff(&committed, ¤t);
assert!(d.iter().any(|c| matches!(c, Change::Removed(_)) && c.fails()));
}
#[test]
fn parse_rejects_garbage() {
assert!(parse("not an entry\n").is_err());
assert!(parse("x tier=omega h00\n").is_err());
assert!(parse("# only comments\n\n").unwrap().is_empty());
}
}