objects/object/
state_attachment.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::{Attribution, ContentHash, StateId, StateSignature};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
9#[serde(transparent)]
10pub struct StateAttachmentId(ContentHash);
11
12impl StateAttachmentId {
13 pub fn from_hash(hash: ContentHash) -> Self {
14 Self(hash)
15 }
16
17 pub fn as_hash(&self) -> &ContentHash {
18 &self.0
19 }
20}
21
22impl std::fmt::Display for StateAttachmentId {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "ha-{}", self.0.short())
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub enum StateAttachmentBody {
30 Context(ContentHash),
31 RiskSignals(ContentHash),
32 ReviewSignatures(ContentHash),
33 Discussions(ContentHash),
34 StructuredConflicts(ContentHash),
35 SemanticIndex(ContentHash),
37 Signature(StateSignature),
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct StateAttachment {
42 pub state_id: StateId,
43 pub body: StateAttachmentBody,
44 pub attribution: Attribution,
45 pub created_at: DateTime<Utc>,
46 pub supersedes: Option<StateAttachmentId>,
47}
48
49impl StateAttachment {
50 pub fn id(&self) -> StateAttachmentId {
51 let bytes = rmp_serde::to_vec_named(self).expect("state attachment encoding is infallible");
52 StateAttachmentId::from_hash(ContentHash::compute_typed("state-attachment", &bytes))
53 }
54}
55
56#[cfg(test)]
57mod wire_compat_tests {
58 use chrono::{DateTime, Utc};
71 use serde::Deserialize;
72
73 use super::*;
74 use crate::object::{Attribution, Principal};
75
76 #[derive(Debug, Deserialize)]
81 #[allow(dead_code)]
82 enum LegacyBody {
83 Context(ContentHash),
84 RiskSignals(ContentHash),
85 ReviewSignatures(ContentHash),
86 Discussions(ContentHash),
87 StructuredConflicts(ContentHash),
88 Signature(StateSignature),
89 }
90
91 #[derive(Debug, Deserialize)]
94 #[allow(dead_code)]
95 struct LegacyAttachment {
96 state_id: StateId,
97 body: LegacyBody,
98 attribution: Attribution,
99 created_at: DateTime<Utc>,
100 supersedes: Option<StateAttachmentId>,
101 }
102
103 fn sample(body: StateAttachmentBody) -> StateAttachment {
104 StateAttachment {
105 state_id: StateId::from_bytes([7; 32]),
106 body,
107 attribution: Attribution::human(Principal::new("Test", "test@example.com")),
108 created_at: Utc::now(),
109 supersedes: None,
110 }
111 }
112
113 #[test]
114 fn semantic_index_attachment_is_undecodable_by_0_10_2_consumer() {
115 let attachment = sample(StateAttachmentBody::SemanticIndex(ContentHash::compute(b"root")));
116 let bytes = rmp_serde::to_vec_named(&attachment).expect("encode");
117
118 let current: StateAttachment = rmp_serde::from_slice(&bytes).expect("current decoder");
120 assert_eq!(current, attachment);
121
122 let legacy: Result<LegacyAttachment, _> = rmp_serde::from_slice(&bytes);
126 assert!(
127 legacy.is_err(),
128 "a 0.10.2 decoder must fail on a SemanticIndex-tagged attachment"
129 );
130 }
131
132 #[test]
133 fn known_variant_still_decodes_on_0_10_2_consumer() {
134 let attachment = sample(StateAttachmentBody::Context(ContentHash::compute(b"ctx")));
138 let bytes = rmp_serde::to_vec_named(&attachment).expect("encode");
139 let legacy: Result<LegacyAttachment, _> = rmp_serde::from_slice(&bytes);
140 assert!(
141 legacy.is_ok(),
142 "a 0.10.2 decoder must still handle the Context variant it knows"
143 );
144 }
145}