use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::{Attribution, ContentHash, StateId, StateSignature};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct StateAttachmentId(ContentHash);
impl StateAttachmentId {
pub fn from_hash(hash: ContentHash) -> Self {
Self(hash)
}
pub fn as_hash(&self) -> &ContentHash {
&self.0
}
}
impl std::fmt::Display for StateAttachmentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ha-{}", self.0.short())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum StateAttachmentBody {
Context(ContentHash),
RiskSignals(ContentHash),
ReviewSignatures(ContentHash),
Discussions(ContentHash),
StructuredConflicts(ContentHash),
SemanticIndex(ContentHash),
Signature(StateSignature),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StateAttachment {
pub state_id: StateId,
pub body: StateAttachmentBody,
pub attribution: Attribution,
pub created_at: DateTime<Utc>,
pub supersedes: Option<StateAttachmentId>,
}
impl StateAttachment {
pub fn id(&self) -> StateAttachmentId {
let bytes = rmp_serde::to_vec_named(self).expect("state attachment encoding is infallible");
StateAttachmentId::from_hash(ContentHash::compute_typed("state-attachment", &bytes))
}
}
#[cfg(test)]
mod wire_compat_tests {
use chrono::{DateTime, Utc};
use serde::Deserialize;
use super::*;
use crate::object::{Attribution, Principal};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
enum LegacyBody {
Context(ContentHash),
RiskSignals(ContentHash),
ReviewSignatures(ContentHash),
Discussions(ContentHash),
StructuredConflicts(ContentHash),
Signature(StateSignature),
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct LegacyAttachment {
state_id: StateId,
body: LegacyBody,
attribution: Attribution,
created_at: DateTime<Utc>,
supersedes: Option<StateAttachmentId>,
}
fn sample(body: StateAttachmentBody) -> StateAttachment {
StateAttachment {
state_id: StateId::from_bytes([7; 32]),
body,
attribution: Attribution::human(Principal::new("Test", "test@example.com")),
created_at: Utc::now(),
supersedes: None,
}
}
#[test]
fn semantic_index_attachment_is_undecodable_by_0_10_2_consumer() {
let attachment = sample(StateAttachmentBody::SemanticIndex(ContentHash::compute(b"root")));
let bytes = rmp_serde::to_vec_named(&attachment).expect("encode");
let current: StateAttachment = rmp_serde::from_slice(&bytes).expect("current decoder");
assert_eq!(current, attachment);
let legacy: Result<LegacyAttachment, _> = rmp_serde::from_slice(&bytes);
assert!(
legacy.is_err(),
"a 0.10.2 decoder must fail on a SemanticIndex-tagged attachment"
);
}
#[test]
fn known_variant_still_decodes_on_0_10_2_consumer() {
let attachment = sample(StateAttachmentBody::Context(ContentHash::compute(b"ctx")));
let bytes = rmp_serde::to_vec_named(&attachment).expect("encode");
let legacy: Result<LegacyAttachment, _> = rmp_serde::from_slice(&bytes);
assert!(
legacy.is_ok(),
"a 0.10.2 decoder must still handle the Context variant it knows"
);
}
}