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}