use serde::Serialize;
use crate::digest::Digest;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum EvidenceKind {
Record,
Configuration,
Destination,
Repository,
Bundle,
Host,
Pin,
Forge,
}
#[derive(Debug, Clone, Serialize)]
pub struct EvidenceItem {
pub id: String,
pub kind: EvidenceKind,
pub producer: String,
pub observed_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sha256: Option<Digest>,
pub method: String,
}
#[derive(Debug, Default)]
pub struct Ledger {
items: Vec<EvidenceItem>,
}
impl Ledger {
#[must_use]
pub const fn new() -> Self {
Self { items: Vec::new() }
}
pub fn observe(
&mut self,
id: impl Into<String>,
kind: EvidenceKind,
producer: impl Into<String>,
observed_at: &str,
sha256: Option<Digest>,
method: impl Into<String>,
) -> String {
let id = id.into();
debug_assert!(
!self.items.iter().any(|item| item.id == id),
"evidence id {id} is already in the ledger"
);
self.items.push(EvidenceItem {
id: id.clone(),
kind,
producer: producer.into(),
observed_at: observed_at.to_owned(),
sha256,
method: method.into(),
});
id
}
#[must_use]
pub fn into_items(self) -> Vec<EvidenceItem> {
self.items
}
#[must_use]
pub fn has(&self, id: &str) -> bool {
self.items.iter().any(|item| item.id == id)
}
}
#[cfg(test)]
mod tests {
use super::{EvidenceKind, Ledger};
use crate::digest::Digest;
#[test]
fn an_observation_answers_the_id_a_field_cites() {
let mut ledger = Ledger::new();
let id = ledger.observe(
"record",
EvidenceKind::Record,
"rk",
"2026-01-01T00:00:00Z",
Some(Digest::of(b"{}")),
"read .release-kit/manifest.json",
);
assert_eq!(id, "record");
assert!(ledger.has("record"));
let items = ledger.into_items();
assert_eq!(items.len(), 1);
assert_eq!(
serde_json::to_string(&items[0]).expect("serializes"),
format!(
r#"{{"id":"record","kind":"record","producer":"rk","observed_at":"2026-01-01T00:00:00Z","sha256":"{}","method":"read .release-kit/manifest.json"}}"#,
Digest::of(b"{}")
)
);
}
}