repo/
state_attachments.rs1pub use objects::object::StateAttachmentKind;
7use objects::{
8 object::{StateAttachment, StateAttachmentId, StateId},
9 store::ObjectStore,
10};
11use oplog::OpLogBackend;
12use refs::RefBackend;
13
14use crate::{HeddleError, Repository, Result};
15
16impl<R, O, S> Repository<R, O, S>
17where
18 R: RefBackend,
19 O: OpLogBackend,
20 S: ObjectStore,
21{
22 pub fn put_state_attachment(&self, attachment: &StateAttachment) -> Result<StateAttachmentId> {
23 if !self.store().has_state(&attachment.state_id)? {
24 return Err(HeddleError::StateNotFound(attachment.state_id));
25 }
26
27 if let Some(prior_id) = attachment.supersedes {
28 if attachment.body.kind() == StateAttachmentKind::Signature {
29 return Err(HeddleError::InvalidObject(
30 "state-signature evidence is append-only and cannot supersede another signature"
31 .to_string(),
32 ));
33 }
34 let prior = self
35 .get_state_attachment(&attachment.state_id, &prior_id)?
36 .ok_or_else(|| HeddleError::NotFound(format!("state attachment {prior_id}")))?;
37 if prior.body.kind() != attachment.body.kind() {
38 return Err(HeddleError::InvalidObject(
39 "state attachment can only supersede the same attachment kind".to_string(),
40 ));
41 }
42 }
43
44 let id = attachment.id();
45 if let Some(existing) = self
46 .store()
47 .get_state_attachment(&attachment.state_id, &id)?
48 {
49 return match self.get_state_attachment(&attachment.state_id, &id)? {
50 Some(_) if existing == *attachment => Ok(id),
51 _ => Err(HeddleError::InvalidObject(
52 "state attachment address collision".to_string(),
53 )),
54 };
55 }
56 self.store().put_state_attachment(attachment)
57 }
58
59 pub fn get_state_attachment(
60 &self,
61 state_id: &StateId,
62 attachment_id: &StateAttachmentId,
63 ) -> Result<Option<StateAttachment>> {
64 self.store().get_state_attachment(state_id, attachment_id)
65 }
66
67 pub fn list_state_attachments(&self, state_id: &StateId) -> Result<Vec<StateAttachment>> {
68 let mut attachments = self.store().list_state_attachments(state_id)?;
69 attachments.sort_by_key(|attachment| (attachment.created_at, attachment.id()));
70 Ok(attachments)
71 }
72
73 pub fn latest_state_attachment(
74 &self,
75 state_id: &StateId,
76 kind: StateAttachmentKind,
77 ) -> Result<Option<StateAttachment>> {
78 let attachments: Vec<_> = self
79 .list_state_attachments(state_id)?
80 .into_iter()
81 .filter(|attachment| attachment.body.kind() == kind)
82 .collect();
83 let superseded: std::collections::HashSet<_> = attachments
84 .iter()
85 .filter_map(|attachment| attachment.supersedes)
86 .collect();
87 Ok(attachments
88 .into_iter()
89 .filter(|attachment| !superseded.contains(&attachment.id()))
90 .max_by_key(StateAttachment::id))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use chrono::{Duration, Utc};
97 use objects::object::{
98 Attribution, ContentHash, Principal, StateAttachment, StateAttachmentBody,
99 };
100 use tempfile::TempDir;
101
102 use super::*;
103
104 fn attachment(state_id: StateId, body: StateAttachmentBody) -> StateAttachment {
105 StateAttachment {
106 state_id,
107 body,
108 attribution: Attribution::human(Principal::new("Test", "test@example.com")),
109 created_at: Utc::now(),
110 supersedes: None,
111 }
112 }
113
114 #[test]
115 fn attachment_history_preserves_state_bytes() {
116 let temp = TempDir::new().unwrap();
117 let repo = Repository::init_default(temp.path()).unwrap();
118 let state_id = repo.head().unwrap().unwrap();
119 let before = repo.store().get_state(&state_id).unwrap().unwrap();
120
121 let first = attachment(
122 state_id,
123 StateAttachmentBody::Context(ContentHash::compute(b"first")),
124 );
125 let first_id = repo.put_state_attachment(&first).unwrap();
126 let mut second = attachment(
127 state_id,
128 StateAttachmentBody::Context(ContentHash::compute(b"second")),
129 );
130 second.created_at = first.created_at + Duration::seconds(1);
131 second.supersedes = Some(first_id);
132 repo.put_state_attachment(&second).unwrap();
133
134 let latest = repo
135 .latest_state_attachment(&state_id, StateAttachmentKind::Context)
136 .unwrap()
137 .unwrap();
138 assert_eq!(latest, second);
139 assert_eq!(repo.list_state_attachments(&state_id).unwrap().len(), 2);
140 assert_eq!(repo.store().get_state(&state_id).unwrap().unwrap(), before);
141 }
142
143 #[test]
144 fn latest_follows_supersession_heads_not_wall_clock() {
145 let temp = TempDir::new().unwrap();
146 let repo = Repository::init_default(temp.path()).unwrap();
147 let state_id = repo.head().unwrap().unwrap();
148 let root = attachment(
149 state_id,
150 StateAttachmentBody::Context(ContentHash::compute(b"root")),
151 );
152 let root_id = repo.put_state_attachment(&root).unwrap();
153
154 let mut older_clock = attachment(
155 state_id,
156 StateAttachmentBody::Context(ContentHash::compute(b"older-clock")),
157 );
158 older_clock.created_at = root.created_at - Duration::seconds(10);
159 older_clock.supersedes = Some(root_id);
160 repo.put_state_attachment(&older_clock).unwrap();
161
162 let mut fork = attachment(
163 state_id,
164 StateAttachmentBody::Context(ContentHash::compute(b"fork")),
165 );
166 fork.created_at = root.created_at + Duration::seconds(10);
167 fork.supersedes = Some(root_id);
168 repo.put_state_attachment(&fork).unwrap();
169
170 let expected = [older_clock.clone(), fork.clone()]
171 .into_iter()
172 .max_by_key(StateAttachment::id)
173 .unwrap();
174 assert_eq!(
175 repo.latest_state_attachment(&state_id, StateAttachmentKind::Context)
176 .unwrap()
177 .unwrap(),
178 expected
179 );
180 }
181}