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/// already_applied: 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 /// Returns `Some(value)` if the operation was executed, `None` if it was ignored.
66 pub fn into_executed(self) -> Option<T> {
67 match self {
68 Idempotent::Executed(t) => Some(t),
69 Idempotent::AlreadyApplied => None,
70 }
71 }
72
73 /// Unwraps the value if executed, panics if ignored.
74 pub fn unwrap(self) -> T {
75 match self {
76 Idempotent::Executed(t) => t,
77 Idempotent::AlreadyApplied => panic!("Idempotent::AlreadyApplied"),
78 }
79 }
80
81 /// Unwraps the value if executed, panics with custom message if ignored.
82 pub fn expect(self, msg: &str) -> T {
83 match self {
84 Idempotent::Executed(val) => val,
85 Idempotent::AlreadyApplied => panic!("{}", msg),
86 }
87 }
88}
89
90/// Internal trait used by the [`idempotency_guard`][crate::idempotency_guard] macro.
91///
92/// This internal-only trait is implemented on [`idempotency_guard`][crate::idempotency_guard] for it to create
93/// both `Idempotent<T>` and `Result<Idempotent<T>, E>` return types for returning `AlreadyApplied` variant.
94pub trait FromAlreadyApplied {
95 /// Creates a value representing an already applied idempotent operation.
96 fn from_already_applied() -> Self;
97}
98
99impl<T> FromAlreadyApplied for Idempotent<T> {
100 /// to handle `Idempotent<T>` return type
101 fn from_already_applied() -> Self {
102 Idempotent::AlreadyApplied
103 }
104}
105
106impl<T, E> FromAlreadyApplied for Result<Idempotent<T>, E> {
107 /// to handle `Result<Idempotent<T>, E>` return type
108 fn from_already_applied() -> Self {
109 Ok(Idempotent::AlreadyApplied)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn into_executed() {
119 assert_eq!(Idempotent::Executed(1).into_executed(), Some(1));
120 assert_eq!(Idempotent::<u32>::AlreadyApplied.into_executed(), None);
121 }
122}