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 {
126 title,
127 body,
128 labels,
129 } => Ok(Self::new(
130 event.issue_id,
131 title.clone(),
132 body.clone(),
133 labels.clone(),
134 event.ts_unix_ms,
135 event.actor,
136 event.event_id,
137 )),
138 _ => Err(GriteError::Internal(
139 "Expected IssueCreated event".to_string(),
140 )),
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::hash::compute_event_id;
149 use crate::types::event::IssueState;
150 use crate::types::ids::generate_issue_id;
151
152 fn make_event(issue_id: [u8; 16], actor: [u8; 16], ts: u64, kind: EventKind) -> Event {
153 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
154 Event::new(event_id, issue_id, actor, ts, None, kind)
155 }
156
157 #[test]
158 fn test_apply_issue_updated_title() {
159 let issue_id = generate_issue_id();
160 let actor = [1u8; 16];
161
162 let create_event = make_event(
163 issue_id,
164 actor,
165 1000,
166 EventKind::IssueCreated {
167 title: "Original".to_string(),
168 body: "Body".to_string(),
169 labels: vec![],
170 },
171 );
172
173 let mut proj = IssueProjection::from_event(&create_event).unwrap();
174 assert_eq!(proj.title, "Original");
175
176 let update_event = make_event(
177 issue_id,
178 actor,
179 2000,
180 EventKind::IssueUpdated {
181 title: Some("Updated".to_string()),
182 body: None,
183 },
184 );
185
186 proj.apply(&update_event).unwrap();
187 assert_eq!(proj.title, "Updated");
188 assert_eq!(proj.body, "Body"); }
190
191 #[test]
192 fn test_apply_lww_older_update_ignored() {
193 let issue_id = generate_issue_id();
194 let actor = [1u8; 16];
195
196 let create_event = make_event(
197 issue_id,
198 actor,
199 2000, EventKind::IssueCreated {
201 title: "Original".to_string(),
202 body: "Body".to_string(),
203 labels: vec![],
204 },
205 );
206
207 let mut proj = IssueProjection::from_event(&create_event).unwrap();
208
209 let old_update = make_event(
211 issue_id,
212 actor,
213 1000, EventKind::IssueUpdated {
215 title: Some("Old".to_string()),
216 body: None,
217 },
218 );
219
220 proj.apply(&old_update).unwrap();
221 assert_eq!(proj.title, "Original"); }
223
224 #[test]
225 fn test_apply_comment_added() {
226 let issue_id = generate_issue_id();
227 let actor = [1u8; 16];
228
229 let create_event = make_event(
230 issue_id,
231 actor,
232 1000,
233 EventKind::IssueCreated {
234 title: "Test".to_string(),
235 body: "Body".to_string(),
236 labels: vec![],
237 },
238 );
239
240 let mut proj = IssueProjection::from_event(&create_event).unwrap();
241 assert_eq!(proj.comments.len(), 0);
242
243 let comment_event = make_event(
244 issue_id,
245 actor,
246 2000,
247 EventKind::CommentAdded {
248 body: "Nice work!".to_string(),
249 },
250 );
251
252 proj.apply(&comment_event).unwrap();
253 assert_eq!(proj.comments.len(), 1);
254 assert_eq!(proj.comments[0].body, "Nice work!");
255 }
256
257 #[test]
258 fn test_apply_labels_commutative() {
259 let issue_id = generate_issue_id();
260 let actor = [1u8; 16];
261
262 let create_event = make_event(
263 issue_id,
264 actor,
265 1000,
266 EventKind::IssueCreated {
267 title: "Test".to_string(),
268 body: "Body".to_string(),
269 labels: vec!["initial".to_string()],
270 },
271 );
272
273 let mut proj = IssueProjection::from_event(&create_event).unwrap();
274 assert!(proj.labels.contains("initial"));
275
276 let add_event = make_event(
278 issue_id,
279 actor,
280 2000,
281 EventKind::LabelAdded {
282 label: "bug".to_string(),
283 },
284 );
285 proj.apply(&add_event).unwrap();
286 assert!(proj.labels.contains("bug"));
287
288 let remove_event = make_event(
290 issue_id,
291 actor,
292 3000,
293 EventKind::LabelRemoved {
294 label: "initial".to_string(),
295 },
296 );
297 proj.apply(&remove_event).unwrap();
298 assert!(!proj.labels.contains("initial"));
299 assert!(proj.labels.contains("bug"));
300 }
301
302 #[test]
303 fn test_apply_state_changed() {
304 let issue_id = generate_issue_id();
305 let actor = [1u8; 16];
306
307 let create_event = make_event(
308 issue_id,
309 actor,
310 1000,
311 EventKind::IssueCreated {
312 title: "Test".to_string(),
313 body: "Body".to_string(),
314 labels: vec![],
315 },
316 );
317
318 let mut proj = IssueProjection::from_event(&create_event).unwrap();
319 assert_eq!(proj.state, IssueState::Open);
320
321 let close_event = make_event(
322 issue_id,
323 actor,
324 2000,
325 EventKind::StateChanged {
326 state: IssueState::Closed,
327 },
328 );
329
330 proj.apply(&close_event).unwrap();
331 assert_eq!(proj.state, IssueState::Closed);
332 }
333
334 #[test]
335 fn test_deterministic_rebuild() {
336 let issue_id = generate_issue_id();
337 let actor1 = [1u8; 16];
338 let actor2 = [2u8; 16];
339
340 let events = [
342 make_event(
343 issue_id,
344 actor1,
345 1000,
346 EventKind::IssueCreated {
347 title: "Test".to_string(),
348 body: "Body".to_string(),
349 labels: vec!["bug".to_string()],
350 },
351 ),
352 make_event(
353 issue_id,
354 actor2,
355 2000,
356 EventKind::CommentAdded {
357 body: "Comment 1".to_string(),
358 },
359 ),
360 make_event(
361 issue_id,
362 actor1,
363 3000,
364 EventKind::LabelAdded {
365 label: "p0".to_string(),
366 },
367 ),
368 make_event(
369 issue_id,
370 actor2,
371 4000,
372 EventKind::IssueUpdated {
373 title: Some("Updated Title".to_string()),
374 body: None,
375 },
376 ),
377 ];
378
379 let mut proj1 = IssueProjection::from_event(&events[0]).unwrap();
381 for event in &events[1..] {
382 proj1.apply(event).unwrap();
383 }
384
385 let mut proj2 = IssueProjection::from_event(&events[0]).unwrap();
387 for event in &events[1..] {
388 proj2.apply(event).unwrap();
389 }
390
391 assert_eq!(proj1.title, proj2.title);
393 assert_eq!(proj1.body, proj2.body);
394 assert_eq!(proj1.state, proj2.state);
395 assert_eq!(proj1.labels, proj2.labels);
396 assert_eq!(proj1.comments.len(), proj2.comments.len());
397 }
398}