es_entity/
idempotent.rs

1//! Handle idempotency in event-sourced systems.
2
3/// Signals if a mutation is applied or was skipped.
4///
5/// Distinguishes between operations that were executed versus those that were
6/// ignored due to idempotency checks.
7/// The [`idempotency_guard`][crate::idempotency_guard] macro provides an easy way to do such checks.
8///
9/// # Examples
10///
11/// ```rust
12/// use es_entity::{idempotency_guard, Idempotent};
13/// pub enum UserEvent{
14///     Initialized {id: u64, name: String},
15///     NameUpdated {name: String}
16/// }
17///
18/// pub struct User{
19///     events: Vec<UserEvent>
20/// }
21///
22/// impl User{
23///     // This returns `Idempotent<T>` where T is the return value we get after the event is processed
24///     pub fn update_name(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
25///         let name = new_name.into();
26///         idempotency_guard!(
27///             self.events.iter().rev(),
28///             UserEvent::NameUpdated { name: existing_name } if existing_name == &name
29///         );
30///         self.events.push(UserEvent::NameUpdated{name});
31///         Idempotent::Executed(())
32///     }
33/// }
34///   
35/// fn example(){
36///     let mut user = User{ events: vec![] };
37///     assert!(user.update_name("Alice").did_execute());
38///     // updating "Alice" executes as no such event has been processed before.
39///     // Signalled by returning `Idempotent::Executed(T)`, validated with `did_execute` helper method
40///
41///     assert!(user.update_name("Alice").was_already_applied());
42///     // updating "Alice" again ignored because same event has been processed before.
43///     // Signalled by returning `Idempotent::AlreadyApplied` early, validated with `was_already_applied` helper method
44/// }
45/// ```
46#[must_use]
47pub enum Idempotent<T> {
48    // Signals if executed and returns T
49    Executed(T),
50    // Signals if ignored due to idempotency checks
51    AlreadyApplied,
52}
53
54impl<T> Idempotent<T> {
55    /// Returns true if the operation was ignored due to idempotency checks.
56    pub fn was_already_applied(&self) -> bool {
57        matches!(self, Idempotent::AlreadyApplied)
58    }
59
60    /// Returns true if the operation was executed.
61    pub fn did_execute(&self) -> bool {
62        matches!(self, Idempotent::Executed(_))
63    }
64
65    /// Unwraps the value if executed, panics if ignored.
66    pub fn unwrap(self) -> T {
67        match self {
68            Idempotent::Executed(t) => t,
69            Idempotent::AlreadyApplied => panic!("Idempotent::AlreadyApplied"),
70        }
71    }
72
73    /// Unwraps the value if executed, panics with custom message if ignored.
74    pub fn expect(self, msg: &str) -> T {
75        match self {
76            Idempotent::Executed(val) => val,
77            Idempotent::AlreadyApplied => panic!("{}", msg),
78        }
79    }
80}
81
82/// Internal trait used by the [`idempotency_guard`][crate::idempotency_guard] macro.
83///
84/// This internal-only trait is implemented on [`idempotency_guard`][crate::idempotency_guard] for it to create
85/// both `Idempotent<T>` and `Result<Idempotent<T>, E>` return types for returning `AlreadyApplied` variant.
86pub trait FromAlreadyApplied {
87    /// Creates a value representing an already applied idempotent operation.
88    fn from_already_applied() -> Self;
89}
90
91impl<T> FromAlreadyApplied for Idempotent<T> {
92    /// to handle `Idempotent<T>` return type
93    fn from_already_applied() -> Self {
94        Idempotent::AlreadyApplied
95    }
96}
97
98impl<T, E> FromAlreadyApplied for Result<Idempotent<T>, E> {
99    /// to handle `Result<Idempotent<T>, E>` return type
100    fn from_already_applied() -> Self {
101        Ok(Idempotent::AlreadyApplied)
102    }
103}