use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FrameId {
pub provider_id: String,
pub frame_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
}
impl FrameId {
pub fn new(
provider_id: impl Into<String>,
frame_id: impl Into<String>,
content_digest: Option<String>,
) -> Self {
Self {
provider_id: provider_id.into(),
frame_id: frame_id.into(),
content_digest,
}
}
pub fn is_verifiable(&self) -> bool {
self.content_digest.is_some()
}
}
pub fn canonical_order(ids: &mut [FrameId]) {
ids.sort();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_id_roundtrips_through_json() {
let id = FrameId::new("repo-graph", "retry-doc", Some("sha256:9f2c".into()));
let json = serde_json::to_string(&id).unwrap();
let back: FrameId = serde_json::from_str(&json).unwrap();
assert_eq!(back, id);
}
#[test]
fn an_absent_digest_is_omitted_and_marks_the_frame_unverifiable() {
let id = FrameId::new("repo-graph", "retry-doc", None);
assert!(!id.is_verifiable());
let json = serde_json::to_string(&id).unwrap();
assert!(
!json.contains("content_digest"),
"an absent digest must be omitted, not serialized as null: {json}"
);
let back: FrameId = serde_json::from_str(&json).unwrap();
assert_eq!(back, id);
}
#[test]
fn canonical_order_is_by_provider_then_frame_then_digest() {
let mut ids = vec![
FrameId::new("b-provider", "frame-1", None),
FrameId::new("a-provider", "frame-2", None),
FrameId::new("a-provider", "frame-1", Some("sha256:zz".into())),
FrameId::new("a-provider", "frame-1", Some("sha256:aa".into())),
];
canonical_order(&mut ids);
assert_eq!(
ids,
vec![
FrameId::new("a-provider", "frame-1", Some("sha256:aa".into())),
FrameId::new("a-provider", "frame-1", Some("sha256:zz".into())),
FrameId::new("a-provider", "frame-2", None),
FrameId::new("b-provider", "frame-1", None),
]
);
}
#[test]
fn ordering_is_total_and_stable_regardless_of_input_order() {
let canonical = {
let mut ids = vec![
FrameId::new("p", "c", None),
FrameId::new("p", "a", None),
FrameId::new("p", "b", None),
];
canonical_order(&mut ids);
ids
};
let mut shuffled = vec![
FrameId::new("p", "b", None),
FrameId::new("p", "c", None),
FrameId::new("p", "a", None),
];
canonical_order(&mut shuffled);
assert_eq!(shuffled, canonical);
}
}