cqrs_todo_core/
lib.rs

1//! # cqrs-todo-core
2//!
3//! `cqrs-todo-core` is a demonstration crate, showing how to construct an aggregate, with the associated events
4//! and commands, using the CQRS system.
5
6#![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/// An aggregate representing the view of a to-do item.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub enum TodoAggregate {
29    /// A to-do item that has been properly initialized.
30    Created(TodoData),
31
32    /// An uninitialized to-do item.
33    Uninitialized,
34}
35
36impl Default for TodoAggregate {
37    fn default() -> Self {
38        TodoAggregate::Uninitialized
39    }
40}
41
42impl TodoAggregate {
43    /// Get the underlying to-do data if the aggregate has been initialized.
44    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/// An identifier for an item to be done.
63#[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/// An identifier for an item to be done.
73#[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/// Metadata about events.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct TodoMetadata {
91    /// The actor that caused this event to be added to the event stream.
92    pub initiated_by: String,
93}
94
95/// Data relating to a to-do item.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct TodoData {
98    /// The to-do item description.
99    pub description: domain::Description,
100
101    /// The reminder time for this to-do item.
102    pub reminder: Option<domain::Reminder>,
103
104    /// The current status of this item.
105    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/// The completion status of a to-do item.
119#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
120pub enum TodoStatus {
121    /// The item has been completed.
122    Completed,
123
124    /// The item has not been completed.
125    NotCompleted,
126}
127
128/// A combined roll-up of the events that can be applied to a [TodoAggregate].
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub enum TodoEvent {
131    /// Created
132    Created(events::Created),
133
134    /// Description updated
135    DescriptionUpdated(events::DescriptionUpdated),
136
137    /// Reminder updated
138    ReminderUpdated(events::ReminderUpdated),
139
140    /// Item completed
141    Completed(events::Completed),
142
143    /// Item completion undone
144    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}