heddle_object_model/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, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub enum StateAttachmentKind {
49 Context,
50 RiskSignals,
51 ReviewSignatures,
52 Discussions,
53 StructuredConflicts,
54 SemanticIndex,
55 Signature,
56}
57
58impl StateAttachmentBody {
59 pub fn kind(&self) -> StateAttachmentKind {
63 match self {
64 StateAttachmentBody::Context(_) => StateAttachmentKind::Context,
65 StateAttachmentBody::RiskSignals(_) => StateAttachmentKind::RiskSignals,
66 StateAttachmentBody::ReviewSignatures(_) => StateAttachmentKind::ReviewSignatures,
67 StateAttachmentBody::Discussions(_) => StateAttachmentKind::Discussions,
68 StateAttachmentBody::StructuredConflicts(_) => StateAttachmentKind::StructuredConflicts,
69 StateAttachmentBody::SemanticIndex(_) => StateAttachmentKind::SemanticIndex,
70 StateAttachmentBody::Signature(_) => StateAttachmentKind::Signature,
71 }
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
76pub struct StateAttachment {
77 pub state_id: StateId,
78 pub body: StateAttachmentBody,
79 pub attribution: Attribution,
80 pub created_at: DateTime<Utc>,
81 pub supersedes: Option<StateAttachmentId>,
82}
83
84impl StateAttachment {
85 pub fn encode_current_msgpack(&self) -> crate::error::Result<Vec<u8>> {
88 Ok(rmp_serde::to_vec_named(self)?)
89 }
90
91 pub fn decode_current_msgpack(bytes: &[u8]) -> crate::error::Result<Self> {
93 Ok(rmp_serde::from_slice(bytes)?)
94 }
95
96 pub fn id(&self) -> StateAttachmentId {
97 let bytes = rmp_serde::to_vec_named(self).expect("state attachment encoding is infallible");
98 StateAttachmentId::from_hash(ContentHash::compute_typed("state-attachment", &bytes))
99 }
100}
101
102#[cfg(test)]
103mod wire_compat_tests {
104 use chrono::{DateTime, Utc};
117 use serde::Deserialize;
118
119 use super::*;
120 use crate::object::{Attribution, Principal};
121
122 #[derive(Debug, Deserialize)]
127 #[allow(dead_code)]
128 enum LegacyBody {
129 Context(ContentHash),
130 RiskSignals(ContentHash),
131 ReviewSignatures(ContentHash),
132 Discussions(ContentHash),
133 StructuredConflicts(ContentHash),
134 Signature(StateSignature),
135 }
136
137 #[derive(Debug, Deserialize)]
140 #[allow(dead_code)]
141 struct LegacyAttachment {
142 state_id: StateId,
143 body: LegacyBody,
144 attribution: Attribution,
145 created_at: DateTime<Utc>,
146 supersedes: Option<StateAttachmentId>,
147 }
148
149 fn sample(body: StateAttachmentBody) -> StateAttachment {
150 StateAttachment {
151 state_id: StateId::from_bytes([7; 32]),
152 body,
153 attribution: Attribution::human(Principal::new("Test", "test@example.com")),
154 created_at: Utc::now(),
155 supersedes: None,
156 }
157 }
158
159 #[test]
160 fn semantic_index_attachment_is_undecodable_by_0_10_2_consumer() {
161 let attachment = sample(StateAttachmentBody::SemanticIndex(ContentHash::compute(
162 b"root",
163 )));
164 let bytes = rmp_serde::to_vec_named(&attachment).expect("encode");
165
166 let current: StateAttachment = rmp_serde::from_slice(&bytes).expect("current decoder");
168 assert_eq!(current, attachment);
169
170 let legacy: Result<LegacyAttachment, _> = rmp_serde::from_slice(&bytes);
174 assert!(
175 legacy.is_err(),
176 "a 0.10.2 decoder must fail on a SemanticIndex-tagged attachment"
177 );
178 }
179
180 #[test]
181 fn known_variant_still_decodes_on_0_10_2_consumer() {
182 let attachment = sample(StateAttachmentBody::Context(ContentHash::compute(b"ctx")));
186 let bytes = rmp_serde::to_vec_named(&attachment).expect("encode");
187 let legacy: Result<LegacyAttachment, _> = rmp_serde::from_slice(&bytes);
188 assert!(
189 legacy.is_ok(),
190 "a 0.10.2 decoder must still handle the Context variant it knows"
191 );
192 }
193}