1use crate::error::GriteError;
2use crate::types::event::{Event, EventKind};
3use crate::types::issue::{Attachment, Comment, Dependency, IssueProjection, Link, Version};
4
5impl IssueProjection {
6 pub fn apply(&mut self, event: &Event) -> Result<(), GriteError> {
8 let new_version = Version::new(event.ts_unix_ms, event.actor, event.event_id);
9
10 match &event.kind {
11 EventKind::IssueCreated { .. } => {
12 return Err(GriteError::Internal(
14 "Cannot apply IssueCreated to existing projection".to_string(),
15 ));
16 }
17
18 EventKind::IssueUpdated { title, body } => {
19 if let Some(new_title) = title {
21 if new_version.is_newer_than(&self.title_version) {
22 self.title = new_title.clone();
23 self.title_version = new_version.clone();
24 }
25 }
26 if let Some(new_body) = body {
28 if new_version.is_newer_than(&self.body_version) {
29 self.body = new_body.clone();
30 self.body_version = new_version.clone();
31 }
32 }
33 }
34
35 EventKind::CommentAdded { body } => {
36 self.comments.push(Comment {
38 event_id: event.event_id,
39 actor: event.actor,
40 ts_unix_ms: event.ts_unix_ms,
41 body: body.clone(),
42 });
43 }
44
45 EventKind::LabelAdded { label } => {
46 self.labels.insert(label.clone());
48 }
49
50 EventKind::LabelRemoved { label } => {
51 self.labels.remove(label);
53 }
54
55 EventKind::StateChanged { state } => {
56 if new_version.is_newer_than(&self.state_version) {
58 self.state = *state;
59 self.state_version = new_version.clone();
60 }
61 }
62
63 EventKind::LinkAdded { url, note } => {
64 self.links.push(Link {
66 event_id: event.event_id,
67 url: url.clone(),
68 note: note.clone(),
69 });
70 }
71
72 EventKind::AssigneeAdded { user } => {
73 self.assignees.insert(user.clone());
75 }
76
77 EventKind::AssigneeRemoved { user } => {
78 self.assignees.remove(user);
80 }
81
82 EventKind::AttachmentAdded { name, sha256, mime } => {
83 self.attachments.push(Attachment {
85 event_id: event.event_id,
86 name: name.clone(),
87 sha256: *sha256,
88 mime: mime.clone(),
89 });
90 }
91
92 EventKind::DependencyAdded { target, dep_type } => {
93 self.dependencies.insert(Dependency {
95 target: *target,
96 dep_type: *dep_type,
97 });
98 }
99
100 EventKind::DependencyRemoved { target, dep_type } => {
101 self.dependencies.remove(&Dependency {
103 target: *target,
104 dep_type: *dep_type,
105 });
106 }
107
108 EventKind::ContextUpdated { .. } | EventKind::ProjectContextUpdated { .. } => {
109 return Ok(());
111 }
112 }
113
114 if event.ts_unix_ms > self.updated_ts {
116 self.updated_ts = event.ts_unix_ms;
117 }
118
119 Ok(())
120 }
121
122 pub fn from_event(event: &Event) -> Result<Self, GriteError> {
124 match &event.kind {
125 EventKind::IssueCreated { title, body, labels } => {
126 Ok(Self::new(
127 event.issue_id,
128 title.clone(),
129 body.clone(),
130 labels.clone(),
131 event.ts_unix_ms,
132 event.actor,
133 event.event_id,
134 ))
135 }
136 _ => Err(GriteError::Internal(
137 "Expected IssueCreated event".to_string(),
138 )),
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::hash::compute_event_id;
147 use crate::types::event::IssueState;
148 use crate::types::ids::generate_issue_id;
149
150 fn make_event(
151 issue_id: [u8; 16],
152 actor: [u8; 16],
153 ts: u64,
154 kind: EventKind,
155 ) -> Event {
156 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
157 Event::new(event_id, issue_id, actor, ts, None, kind)
158 }
159
160 #[test]
161 fn test_apply_issue_updated_title() {
162 let issue_id = generate_issue_id();
163 let actor = [1u8; 16];
164
165 let create_event = make_event(
166 issue_id,
167 actor,
168 1000,
169 EventKind::IssueCreated {
170 title: "Original".to_string(),
171 body: "Body".to_string(),
172 labels: vec![],
173 },
174 );
175
176 let mut proj = IssueProjection::from_event(&create_event).unwrap();
177 assert_eq!(proj.title, "Original");
178
179 let update_event = make_event(
180 issue_id,
181 actor,
182 2000,
183 EventKind::IssueUpdated {
184 title: Some("Updated".to_string()),
185 body: None,
186 },
187 );
188
189 proj.apply(&update_event).unwrap();
190 assert_eq!(proj.title, "Updated");
191 assert_eq!(proj.body, "Body"); }
193
194 #[test]
195 fn test_apply_lww_older_update_ignored() {
196 let issue_id = generate_issue_id();
197 let actor = [1u8; 16];
198
199 let create_event = make_event(
200 issue_id,
201 actor,
202 2000, EventKind::IssueCreated {
204 title: "Original".to_string(),
205 body: "Body".to_string(),
206 labels: vec![],
207 },
208 );
209
210 let mut proj = IssueProjection::from_event(&create_event).unwrap();
211
212 let old_update = make_event(
214 issue_id,
215 actor,
216 1000, EventKind::IssueUpdated {
218 title: Some("Old".to_string()),
219 body: None,
220 },
221 );
222
223 proj.apply(&old_update).unwrap();
224 assert_eq!(proj.title, "Original"); }
226
227 #[test]
228 fn test_apply_comment_added() {
229 let issue_id = generate_issue_id();
230 let actor = [1u8; 16];
231
232 let create_event = make_event(
233 issue_id,
234 actor,
235 1000,
236 EventKind::IssueCreated {
237 title: "Test".to_string(),
238 body: "Body".to_string(),
239 labels: vec![],
240 },
241 );
242
243 let mut proj = IssueProjection::from_event(&create_event).unwrap();
244 assert_eq!(proj.comments.len(), 0);
245
246 let comment_event = make_event(
247 issue_id,
248 actor,
249 2000,
250 EventKind::CommentAdded {
251 body: "Nice work!".to_string(),
252 },
253 );
254
255 proj.apply(&comment_event).unwrap();
256 assert_eq!(proj.comments.len(), 1);
257 assert_eq!(proj.comments[0].body, "Nice work!");
258 }
259
260 #[test]
261 fn test_apply_labels_commutative() {
262 let issue_id = generate_issue_id();
263 let actor = [1u8; 16];
264
265 let create_event = make_event(
266 issue_id,
267 actor,
268 1000,
269 EventKind::IssueCreated {
270 title: "Test".to_string(),
271 body: "Body".to_string(),
272 labels: vec!["initial".to_string()],
273 },
274 );
275
276 let mut proj = IssueProjection::from_event(&create_event).unwrap();
277 assert!(proj.labels.contains("initial"));
278
279 let add_event = make_event(
281 issue_id,
282 actor,
283 2000,
284 EventKind::LabelAdded {
285 label: "bug".to_string(),
286 },
287 );
288 proj.apply(&add_event).unwrap();
289 assert!(proj.labels.contains("bug"));
290
291 let remove_event = make_event(
293 issue_id,
294 actor,
295 3000,
296 EventKind::LabelRemoved {
297 label: "initial".to_string(),
298 },
299 );
300 proj.apply(&remove_event).unwrap();
301 assert!(!proj.labels.contains("initial"));
302 assert!(proj.labels.contains("bug"));
303 }
304
305 #[test]
306 fn test_apply_state_changed() {
307 let issue_id = generate_issue_id();
308 let actor = [1u8; 16];
309
310 let create_event = make_event(
311 issue_id,
312 actor,
313 1000,
314 EventKind::IssueCreated {
315 title: "Test".to_string(),
316 body: "Body".to_string(),
317 labels: vec![],
318 },
319 );
320
321 let mut proj = IssueProjection::from_event(&create_event).unwrap();
322 assert_eq!(proj.state, IssueState::Open);
323
324 let close_event = make_event(
325 issue_id,
326 actor,
327 2000,
328 EventKind::StateChanged {
329 state: IssueState::Closed,
330 },
331 );
332
333 proj.apply(&close_event).unwrap();
334 assert_eq!(proj.state, IssueState::Closed);
335 }
336
337 #[test]
338 fn test_deterministic_rebuild() {
339 let issue_id = generate_issue_id();
340 let actor1 = [1u8; 16];
341 let actor2 = [2u8; 16];
342
343 let events = vec![
345 make_event(
346 issue_id,
347 actor1,
348 1000,
349 EventKind::IssueCreated {
350 title: "Test".to_string(),
351 body: "Body".to_string(),
352 labels: vec!["bug".to_string()],
353 },
354 ),
355 make_event(
356 issue_id,
357 actor2,
358 2000,
359 EventKind::CommentAdded {
360 body: "Comment 1".to_string(),
361 },
362 ),
363 make_event(
364 issue_id,
365 actor1,
366 3000,
367 EventKind::LabelAdded {
368 label: "p0".to_string(),
369 },
370 ),
371 make_event(
372 issue_id,
373 actor2,
374 4000,
375 EventKind::IssueUpdated {
376 title: Some("Updated Title".to_string()),
377 body: None,
378 },
379 ),
380 ];
381
382 let mut proj1 = IssueProjection::from_event(&events[0]).unwrap();
384 for event in &events[1..] {
385 proj1.apply(event).unwrap();
386 }
387
388 let mut proj2 = IssueProjection::from_event(&events[0]).unwrap();
390 for event in &events[1..] {
391 proj2.apply(event).unwrap();
392 }
393
394 assert_eq!(proj1.title, proj2.title);
396 assert_eq!(proj1.body, proj2.body);
397 assert_eq!(proj1.state, proj2.state);
398 assert_eq!(proj1.labels, proj2.labels);
399 assert_eq!(proj1.comments.len(), proj2.comments.len());
400 }
401}