1#![warn(unused_import_braces, unused_imports, unused_qualifications)]
7#![deny(
8 missing_debug_implementations,
9 trivial_casts,
10 trivial_numeric_casts,
11 unsafe_code,
12 unused_must_use,
13 missing_docs
14)]
15
16use cqrs_core::{
17 Aggregate, AggregateEvent, AggregateId, DeserializableEvent, Event, SerializableEvent,
18};
19use serde::{Deserialize, Serialize};
20
21pub mod commands;
22pub mod domain;
23pub mod error;
24pub mod events;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub enum TodoAggregate {
29 Created(TodoData),
31
32 Uninitialized,
34}
35
36impl Default for TodoAggregate {
37 fn default() -> Self {
38 TodoAggregate::Uninitialized
39 }
40}
41
42impl TodoAggregate {
43 pub fn get_data(&self) -> Option<&TodoData> {
45 match *self {
46 TodoAggregate::Uninitialized => None,
47 TodoAggregate::Created(ref x) => Some(x),
48 }
49 }
50}
51
52impl Aggregate for TodoAggregate {
53 #[inline(always)]
54 fn aggregate_type() -> &'static str
55 where
56 Self: Sized,
57 {
58 "todo"
59 }
60}
61
62#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct TodoId(pub String);
65
66impl AggregateId<TodoAggregate> for TodoId {
67 fn as_str(&self) -> &str {
68 &self.0
69 }
70}
71
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
74pub struct TodoIdRef<'a>(pub &'a str);
75
76impl<'a> AsRef<str> for TodoIdRef<'a> {
77 fn as_ref(&self) -> &str {
78 &self.0
79 }
80}
81
82impl<'a> AggregateId<TodoAggregate> for TodoIdRef<'a> {
83 fn as_str(&self) -> &str {
84 self.0
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct TodoMetadata {
91 pub initiated_by: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct TodoData {
98 pub description: domain::Description,
100
101 pub reminder: Option<domain::Reminder>,
103
104 pub status: TodoStatus,
106}
107
108impl TodoData {
109 fn with_description(description: domain::Description) -> Self {
110 TodoData {
111 description,
112 reminder: None,
113 status: TodoStatus::NotCompleted,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
120pub enum TodoStatus {
121 Completed,
123
124 NotCompleted,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub enum TodoEvent {
131 Created(events::Created),
133
134 DescriptionUpdated(events::DescriptionUpdated),
136
137 ReminderUpdated(events::ReminderUpdated),
139
140 Completed(events::Completed),
142
143 Uncompleted(events::Uncompleted),
145}
146
147impl Event for TodoEvent {
148 fn event_type(&self) -> &'static str {
149 match *self {
150 TodoEvent::Created(ref evt) => evt.event_type(),
151 TodoEvent::DescriptionUpdated(ref evt) => evt.event_type(),
152 TodoEvent::ReminderUpdated(ref evt) => evt.event_type(),
153 TodoEvent::Completed(ref evt) => evt.event_type(),
154 TodoEvent::Uncompleted(ref evt) => evt.event_type(),
155 }
156 }
157}
158
159impl AggregateEvent<TodoAggregate> for events::Created {
160 fn apply_to(self, aggregate: &mut TodoAggregate) {
161 if TodoAggregate::Uninitialized == *aggregate {
162 *aggregate =
163 TodoAggregate::Created(TodoData::with_description(self.initial_description))
164 }
165 }
166}
167
168impl AggregateEvent<TodoAggregate> for events::DescriptionUpdated {
169 fn apply_to(self, aggregate: &mut TodoAggregate) {
170 if let TodoAggregate::Created(ref mut data) = aggregate {
171 data.description = self.new_description;
172 }
173 }
174}
175
176impl AggregateEvent<TodoAggregate> for events::ReminderUpdated {
177 fn apply_to(self, aggregate: &mut TodoAggregate) {
178 if let TodoAggregate::Created(ref mut data) = aggregate {
179 data.reminder = self.new_reminder;
180 }
181 }
182}
183
184impl AggregateEvent<TodoAggregate> for events::Completed {
185 fn apply_to(self, aggregate: &mut TodoAggregate) {
186 if let TodoAggregate::Created(ref mut data) = aggregate {
187 data.status = TodoStatus::Completed;
188 }
189 }
190}
191
192impl AggregateEvent<TodoAggregate> for events::Uncompleted {
193 fn apply_to(self, aggregate: &mut TodoAggregate) {
194 if let TodoAggregate::Created(ref mut data) = aggregate {
195 data.status = TodoStatus::NotCompleted;
196 }
197 }
198}
199impl AggregateEvent<TodoAggregate> for TodoEvent {
200 fn apply_to(self, aggregate: &mut TodoAggregate) {
201 match self {
202 TodoEvent::Created(evt) => evt.apply_to(aggregate),
203 TodoEvent::DescriptionUpdated(evt) => evt.apply_to(aggregate),
204 TodoEvent::ReminderUpdated(evt) => evt.apply_to(aggregate),
205 TodoEvent::Completed(evt) => evt.apply_to(aggregate),
206 TodoEvent::Uncompleted(evt) => evt.apply_to(aggregate),
207 }
208 }
209}
210
211impl SerializableEvent for TodoEvent {
212 type Error = serde_json::Error;
213
214 fn serialize_event_to_buffer(&self, buffer: &mut Vec<u8>) -> Result<(), Self::Error> {
215 buffer.clear();
216 buffer.reserve(128);
217 match *self {
218 TodoEvent::Created(ref inner) => {
219 serde_json::to_writer(buffer, inner)?;
220 }
221 TodoEvent::ReminderUpdated(ref inner) => {
222 serde_json::to_writer(buffer, inner)?;
223 }
224 TodoEvent::DescriptionUpdated(ref inner) => {
225 serde_json::to_writer(buffer, inner)?;
226 }
227 TodoEvent::Completed(ref inner) => {
228 serde_json::to_writer(buffer, inner)?;
229 }
230 TodoEvent::Uncompleted(ref inner) => {
231 serde_json::to_writer(buffer, inner)?;
232 }
233 }
234 Ok(())
235 }
236}
237
238impl DeserializableEvent for TodoEvent {
239 type Error = serde_json::Error;
240
241 fn deserialize_event_from_buffer(
242 data: &[u8],
243 event_type: &str,
244 ) -> Result<Option<Self>, Self::Error> {
245 let deserialized = match event_type {
246 "todo_created" => TodoEvent::Created(serde_json::from_slice(data)?),
247 "todo_reminder_updated" => TodoEvent::ReminderUpdated(serde_json::from_slice(data)?),
248 "todo_description_updated" => {
249 TodoEvent::DescriptionUpdated(serde_json::from_slice(data)?)
250 }
251 "todo_completed" => TodoEvent::Completed(serde_json::from_slice(data)?),
252 "todo_uncompleted" => TodoEvent::Uncompleted(serde_json::from_slice(data)?),
253 _ => return Ok(None),
254 };
255 Ok(Some(deserialized))
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 pub use super::*;
262 use arrayvec::ArrayVec;
263 use chrono::{Duration, TimeZone, Utc};
264 use pretty_assertions::assert_eq;
265
266 fn create_basic_aggregate() -> TodoAggregate {
267 let now = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0);
268 let reminder = now + Duration::seconds(10000);
269
270 let events = ArrayVec::from([
271 TodoEvent::Completed(events::Completed {}),
272 TodoEvent::Created(events::Created {
273 initial_description: domain::Description::new("Hello!").unwrap(),
274 }),
275 TodoEvent::ReminderUpdated(events::ReminderUpdated {
276 new_reminder: Some(domain::Reminder::new(reminder, now).unwrap()),
277 }),
278 TodoEvent::DescriptionUpdated(events::DescriptionUpdated {
279 new_description: domain::Description::new("New text").unwrap(),
280 }),
281 TodoEvent::Created(events::Created {
282 initial_description: domain::Description::new("Ignored!").unwrap(),
283 }),
284 TodoEvent::ReminderUpdated(events::ReminderUpdated { new_reminder: None }),
285 ]);
286
287 let mut agg = TodoAggregate::default();
288 for event in events {
289 agg.apply(event);
290 }
291 agg
292 }
293
294 #[test]
295 fn example_event_sequence() {
296 let expected_data = TodoData {
297 description: domain::Description::new("New text").unwrap(),
298 reminder: None,
299 status: TodoStatus::NotCompleted,
300 };
301 let expected_state = TodoAggregate::Created(expected_data);
302
303 let agg = create_basic_aggregate();
304
305 assert_eq!(expected_state, agg);
306 }
307
308 #[test]
309 fn cancel_reminder_on_default_aggregate() {
310 let agg = TodoAggregate::default();
311
312 let cmd = commands::CancelReminder;
313
314 let result = agg.execute(cmd).unwrap_err();
315
316 assert_eq!(error::CommandError::NotInitialized, result);
317 }
318
319 #[test]
320 fn cancel_reminder_on_basic_aggregate() {
321 let agg = create_basic_aggregate();
322
323 let cmd = commands::CancelReminder;
324
325 let result = agg.execute(cmd).unwrap();
326
327 assert_eq!(ArrayVec::new(), result);
328 }
329
330 #[test]
331 fn set_reminder_on_basic_aggregate() {
332 let agg = create_basic_aggregate();
333
334 let now = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0);
335 let reminder_time = now + Duration::seconds(20000);
336 let new_reminder = domain::Reminder::new(reminder_time, now).unwrap();
337 let cmd = commands::SetReminder { new_reminder };
338
339 let result = agg.execute(cmd).unwrap();
340
341 let mut expected = ArrayVec::new();
342 expected.push(TodoEvent::ReminderUpdated(events::ReminderUpdated {
343 new_reminder: Some(new_reminder),
344 }));
345 assert_eq!(expected, result);
346 }
347
348 #[test]
349 fn ensure_created_event_stays_same() -> Result<(), serde_json::Error> {
350 let initial_description = domain::Description::new("test description").unwrap();
351 run_snapshot_test(
352 "created_event",
353 TodoEvent::Created(events::Created {
354 initial_description,
355 }),
356 )
357 }
358
359 #[test]
360 fn ensure_reminder_updated_event_stays_same() -> Result<(), serde_json::Error> {
361 let current_time = Utc.ymd(2000, 1, 1).and_hms(0, 0, 0);
362 let reminder_time = Utc.ymd(2100, 1, 1).and_hms(0, 0, 0);
363 let reminder = domain::Reminder::new(reminder_time, current_time).unwrap();
364 run_snapshot_test(
365 "reminder_updated_event",
366 TodoEvent::ReminderUpdated(events::ReminderUpdated {
367 new_reminder: Some(reminder),
368 }),
369 )
370 }
371
372 #[test]
373 fn ensure_reminder_removed_event_stays_same() -> Result<(), serde_json::Error> {
374 run_snapshot_test(
375 "reminder_updated_none_event",
376 TodoEvent::ReminderUpdated(events::ReminderUpdated { new_reminder: None }),
377 )
378 }
379
380 #[test]
381 fn ensure_text_updated_event_stays_same() -> Result<(), serde_json::Error> {
382 let new_description = domain::Description::new("alt test description").unwrap();
383 run_snapshot_test(
384 "description_updated_event",
385 TodoEvent::DescriptionUpdated(events::DescriptionUpdated { new_description }),
386 )
387 }
388
389 #[test]
390 fn ensure_completed_event_stays_same() -> Result<(), serde_json::Error> {
391 run_snapshot_test(
392 "completed_event",
393 TodoEvent::Completed(events::Completed {}),
394 )
395 }
396
397 #[test]
398 fn ensure_uncompleted_event_stays_same() -> Result<(), serde_json::Error> {
399 run_snapshot_test(
400 "uncompleted_event",
401 TodoEvent::Uncompleted(events::Uncompleted {}),
402 )
403 }
404
405 fn run_snapshot_test<E: SerializableEvent>(
406 name: &'static str,
407 event: E,
408 ) -> Result<(), E::Error> {
409 let mut buffer = Vec::default();
410 event.serialize_event_to_buffer(&mut buffer)?;
411
412 #[derive(Serialize)]
413 struct RawEventWithType {
414 event_type: &'static str,
415 raw: String,
416 }
417
418 let data = RawEventWithType {
419 event_type: event.event_type(),
420 raw: String::from_utf8(buffer).unwrap(),
421 };
422
423 insta::assert_json_snapshot_matches!(name, data);
424 Ok(())
425 }
426
427 #[test]
428 fn roundtrip_created() {
429 let original = TodoEvent::Created(events::Created {
430 initial_description: domain::Description::new("test description").unwrap(),
431 });
432 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&original);
433 assert_eq!(original, roundtrip);
434 }
435
436 #[test]
437 fn roundtrip_reminder_updated() {
438 let original = TodoEvent::ReminderUpdated(events::ReminderUpdated {
439 new_reminder: Some(
440 domain::Reminder::new(
441 Utc.ymd(2100, 1, 1).and_hms(0, 0, 0),
442 Utc.ymd(2000, 1, 1).and_hms(0, 0, 0),
443 )
444 .unwrap(),
445 ),
446 });
447 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&original);
448 assert_eq!(original, roundtrip);
449 }
450
451 #[test]
452 fn roundtrip_reminder_updated_none() {
453 let original = TodoEvent::ReminderUpdated(events::ReminderUpdated { new_reminder: None });
454 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&original);
455 assert_eq!(original, roundtrip);
456 }
457
458 #[test]
459 fn roundtrip_description_updated() {
460 let original = TodoEvent::DescriptionUpdated(events::DescriptionUpdated {
461 new_description: domain::Description::new("alt test description").unwrap(),
462 });
463 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&original);
464 assert_eq!(original, roundtrip);
465 }
466
467 #[test]
468 fn roundtrip_completed() {
469 let original = TodoEvent::Completed(events::Completed {});
470 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&original);
471 assert_eq!(original, roundtrip);
472 }
473
474 #[test]
475 fn roundtrip_uncompleted() {
476 let original = TodoEvent::Uncompleted(events::Uncompleted {});
477 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&original);
478 assert_eq!(original, roundtrip);
479 }
480
481 mod property_tests {
482 use super::*;
483 use cqrs_proptest::AggregateFromEventSequence;
484 use pretty_assertions::assert_eq;
485 use proptest::{prelude::*, prop_oneof, proptest, proptest_helper};
486 use std::fmt;
487
488 impl Arbitrary for domain::Description {
489 type Parameters = proptest::string::StringParam;
490 type Strategy = BoxedStrategy<Self>;
491
492 fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
493 let s: &'static str = args.into();
494 s.prop_filter_map("invalid description", |d| domain::Description::new(d).ok())
495 .boxed()
496 }
497 }
498
499 impl Arbitrary for domain::Reminder {
500 type Parameters = ();
501 type Strategy = BoxedStrategy<Self>;
502
503 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
504 let current_time = Utc.ymd(2000, 1, 1).and_hms(0, 0, 0);
505
506 (2000..2500_i32, 1..=366_u32, 0..86400_u32)
507 .prop_filter_map("invalid date", move |(y, o, s)| {
508 let time = chrono::NaiveTime::from_num_seconds_from_midnight(s, 0);
509 let date = Utc.yo_opt(y, o).single()?.and_time(time)?;
510 domain::Reminder::new(date, current_time).ok()
511 })
512 .boxed()
513 }
514 }
515
516 impl Arbitrary for events::Created {
517 type Parameters = ();
518 type Strategy = BoxedStrategy<Self>;
519
520 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
521 any::<domain::Description>()
522 .prop_map(|initial_description| events::Created {
523 initial_description,
524 })
525 .boxed()
526 }
527 }
528
529 impl Arbitrary for events::ReminderUpdated {
530 type Parameters = ();
531 type Strategy = BoxedStrategy<Self>;
532
533 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
534 any::<Option<domain::Reminder>>()
535 .prop_map(|new_reminder| events::ReminderUpdated { new_reminder })
536 .boxed()
537 }
538 }
539
540 impl Arbitrary for events::DescriptionUpdated {
541 type Parameters = ();
542 type Strategy = BoxedStrategy<Self>;
543
544 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
545 any::<domain::Description>()
546 .prop_map(|new_description| events::DescriptionUpdated { new_description })
547 .boxed()
548 }
549 }
550
551 impl Arbitrary for events::Completed {
552 type Parameters = ();
553 type Strategy = Just<Self>;
554
555 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
556 Just(events::Completed {})
557 }
558 }
559
560 impl Arbitrary for events::Uncompleted {
561 type Parameters = ();
562 type Strategy = Just<Self>;
563
564 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
565 Just(events::Uncompleted {})
566 }
567 }
568
569 impl Arbitrary for TodoEvent {
570 type Parameters = ();
571 type Strategy = BoxedStrategy<Self>;
572
573 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
574 prop_oneof![
575 any::<events::Created>().prop_map(TodoEvent::Created),
576 any::<events::ReminderUpdated>().prop_map(TodoEvent::ReminderUpdated),
577 any::<events::DescriptionUpdated>().prop_map(TodoEvent::DescriptionUpdated),
578 any::<events::Completed>().prop_map(TodoEvent::Completed),
579 any::<events::Uncompleted>().prop_map(TodoEvent::Uncompleted),
580 ]
581 .boxed()
582 }
583 }
584
585 fn verify_serializable_roundtrips_through_serialization<
586 V: Serialize + for<'de> Deserialize<'de> + Eq + fmt::Debug,
587 >(
588 original: V,
589 ) {
590 let data = serde_json::to_string(&original).expect("serialization");
591 let roundtrip: V = serde_json::from_str(&data).expect("deserialization");
592 assert_eq!(original, roundtrip);
593 }
594
595 type ArbitraryTodoAggregate = AggregateFromEventSequence<TodoAggregate, TodoEvent>;
596
597 proptest! {
598 #[test]
599 fn can_create_arbitrary_aggregate(_agg in any::<ArbitraryTodoAggregate>()) {
600 }
601
602 #[test]
603 fn arbitrary_aggregate_roundtrips_through_serialization(arg in any::<ArbitraryTodoAggregate>()) {
604 verify_serializable_roundtrips_through_serialization(arg.into_aggregate());
605 }
606
607 #[test]
608 fn arbitrary_event_roundtrips_through_serialization(event in any::<TodoEvent>()) {
609 let roundtrip = cqrs_proptest::roundtrip_through_serialization(&event);
610 assert_eq!(event, roundtrip);
611 }
612 }
613 }
614}