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