use serde::{Deserialize, Serialize};
use crate::domain::ownership::Sha256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Producer {
Disk,
Record,
Declaration,
Bundle,
Registry,
Host,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Evidence {
pub id: String,
pub about: String,
pub producer: Producer,
pub observed_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sha256: Option<Sha256>,
pub method: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ledger {
pub items: Vec<Evidence>,
}
impl Ledger {
#[must_use]
pub const fn new() -> Self {
Self { items: Vec::new() }
}
pub fn record(
&mut self,
id: &str,
about: &str,
producer: Producer,
observed_at: &str,
sha256: Option<Sha256>,
method: &str,
) -> String {
self.items.push(Evidence {
id: id.to_string(),
about: about.to_string(),
producer,
observed_at: observed_at.to_string(),
sha256,
method: method.to_string(),
});
id.to_string()
}
#[must_use]
pub fn holds(&self, id: &str) -> bool {
self.items.iter().any(|item| item.id == id)
}
#[must_use]
pub fn ids(&self) -> Vec<&str> {
self.items.iter().map(|item| item.id.as_str()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_recorded_observation_is_citable_by_the_reference_it_returns() {
let mut ledger = Ledger::new();
let reference = ledger.record(
"record",
"the instance manifest",
Producer::Record,
"2026-09-12T00:00:00Z",
Some(Sha256::of(b"x")),
"read from disk",
);
assert_eq!(reference, "record");
assert!(ledger.holds("record"));
assert!(!ledger.holds("absent"));
assert_eq!(ledger.ids(), ["record"]);
}
#[test]
fn an_observation_without_bytes_carries_no_digest() {
let mut ledger = Ledger::new();
ledger.record(
"host",
"the resolved paths",
Producer::Host,
"2026-09-12T00:00:00Z",
None,
"read from the environment",
);
assert_eq!(ledger.items[0].sha256, None);
assert_eq!(ledger.items[0].producer, Producer::Host);
}
}