use serde::{Deserialize, Serialize};
use crate::digest::Digest;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SchemaId {
pub name: String,
pub version: u32,
}
impl SchemaId {
pub fn new(name: impl Into<String>, version: u32) -> Self {
Self {
name: name.into(),
version,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SlicerId {
pub name: String,
pub version: u32,
}
impl SlicerId {
pub fn new(name: impl Into<String>, version: u32) -> Self {
Self {
name: name.into(),
version,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CondensatePayload {
Inline(ciborium::Value),
Stored(Digest),
}
impl CondensatePayload {
pub fn inline<T: Serialize>(value: &T) -> Result<Self, crate::CanonicalError> {
Ok(Self::Inline(ciborium::Value::serialized(value).map_err(
|e| crate::CanonicalError::Encode(e.to_string()),
)?))
}
pub fn is_inline(&self) -> bool {
matches!(self, Self::Inline(_))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Condensate {
pub schema: SchemaId,
pub payload: CondensatePayload,
pub raw: Digest,
pub cap: usize,
pub lossy: bool,
pub slicer: SlicerId,
}
impl Condensate {
pub fn new(
schema: SchemaId,
payload: CondensatePayload,
raw: Digest,
cap: usize,
lossy: bool,
slicer: SlicerId,
) -> Self {
Self {
schema,
payload,
raw,
cap,
lossy,
slicer,
}
}
pub fn needs_raw_for_completeness(&self) -> bool {
self.lossy
}
pub const fn raw_digest(&self) -> &Digest {
&self.raw
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canonical::Canonical;
fn condensate(lossy: bool) -> Condensate {
Condensate::new(
SchemaId::new("boundgraph.log.v1", 1),
CondensatePayload::inline(&"3 errors").unwrap(),
Digest::of(b"raw output"),
400,
lossy,
SlicerId::new("log", 1),
)
}
#[test]
fn carries_schema_payload_raw_cap_lossy_and_slicer() {
let k = condensate(true);
assert_eq!(k.schema, SchemaId::new("boundgraph.log.v1", 1));
assert!(k.payload.is_inline());
assert_eq!(k.raw, Digest::of(b"raw output"));
assert_eq!(k.cap, 400);
assert!(k.lossy);
assert_eq!(k.slicer, SlicerId::new("log", 1));
}
#[test]
fn the_original_is_always_addressable() {
assert_eq!(*condensate(true).raw_digest(), Digest::of(b"raw output"));
assert_eq!(*condensate(false).raw_digest(), Digest::of(b"raw output"));
}
#[test]
fn lossy_flag_tells_a_consumer_whether_to_fetch_the_original() {
assert!(condensate(true).needs_raw_for_completeness());
assert!(!condensate(false).needs_raw_for_completeness());
}
#[test]
fn payload_may_be_stored_instead_of_inlined() {
let big = Condensate::new(
SchemaId::new("boundgraph.ast.v1", 1),
CondensatePayload::Stored(Digest::of(b"summary")),
Digest::of(b"raw"),
8_000,
true,
SlicerId::new("ast", 2),
);
assert!(!big.payload.is_inline());
assert_eq!(
big.payload,
CondensatePayload::Stored(Digest::of(b"summary"))
);
}
#[test]
fn is_typed_not_a_string() {
let k = Condensate::new(
SchemaId::new("boundgraph.testrun.v1", 1),
CondensatePayload::inline(&(3u32, vec!["test_a", "test_b"])).unwrap(),
Digest::of(b"raw"),
200,
false,
SlicerId::new("json", 1),
);
let CondensatePayload::Inline(v) = &k.payload else {
panic!("expected inline payload");
};
let (failures, names): (u32, Vec<String>) = v.deserialized().unwrap();
assert_eq!(failures, 3);
assert_eq!(names, vec!["test_a", "test_b"]);
}
#[test]
fn slicer_version_participates_in_identity() {
let mut other = condensate(true);
other.slicer = SlicerId::new("log", 2);
assert_ne!(
condensate(true).digest().unwrap(),
other.digest().unwrap(),
"a different slicer version is a different condensate"
);
}
#[test]
fn round_trips_through_canonical_cbor() {
let k = condensate(true);
let bytes = crate::to_canonical_cbor(&k).unwrap();
let back: Condensate = crate::from_canonical_cbor(&bytes).unwrap();
assert_eq!(k, back);
}
}