outbox_core/idempotency/
strategy.rs1use crate::config::IdempotencyStrategy;
9use crate::model::Event;
10use serde::Serialize;
11use std::fmt::Debug;
12
13impl<P> IdempotencyStrategy<P>
14where
15 P: Debug + Clone + Serialize,
16{
17 pub fn invoke<F>(&self, provided_token: Option<String>, get_event: F) -> Option<String>
42 where
43 F: FnOnce() -> Option<Event<P>>,
44 {
45 match self {
46 IdempotencyStrategy::Provided => provided_token,
47 IdempotencyStrategy::Custom(f) => {
48 let event = get_event().expect("Strategy is Custom, but no Event context provided");
49 Some(f(&event))
50 }
51 IdempotencyStrategy::Uuid => Some(uuid::Uuid::now_v7().to_string()),
52 IdempotencyStrategy::None => None,
56 }
57 }
58}
59
60#[cfg(test)]
61#[allow(clippy::unwrap_used)]
62mod tests {
63 use super::*;
64 use crate::object::{EventType, Payload};
65 use rstest::rstest;
66 use serde::{Deserialize, Serialize};
67 use std::cell::Cell;
68
69 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70 struct TestPayload(String);
71
72 fn test_event() -> Event<TestPayload> {
73 Event::new(
74 EventType::new("t"),
75 Payload::new(TestPayload("p".into())),
76 None,
77 )
78 }
79
80 #[rstest]
81 fn provided_returns_passed_token() {
82 let s = IdempotencyStrategy::<TestPayload>::Provided;
83 assert_eq!(
84 s.invoke(Some("abc".into()), || None),
85 Some("abc".to_string())
86 );
87 }
88
89 #[rstest]
90 fn provided_returns_none_when_no_token_passed() {
91 let s = IdempotencyStrategy::<TestPayload>::Provided;
92 assert_eq!(s.invoke(None, || None), None);
93 }
94
95 #[rstest]
96 fn provided_does_not_invoke_get_event() {
97 let s = IdempotencyStrategy::<TestPayload>::Provided;
98 let _ = s.invoke(Some("x".into()), || panic!("get_event must not be called"));
99 }
100
101 #[rstest]
102 fn uuid_generates_non_empty_token() {
103 let s = IdempotencyStrategy::<TestPayload>::Uuid;
104 let token = s.invoke(None, || None).expect("Uuid must yield Some");
105 assert!(!token.is_empty());
106 assert!(
108 uuid::Uuid::parse_str(&token).is_ok(),
109 "not a valid UUID: {token}"
110 );
111 }
112
113 #[rstest]
114 fn uuid_generates_unique_tokens_across_calls() {
115 let s = IdempotencyStrategy::<TestPayload>::Uuid;
116 let t1 = s.invoke(None, || None).unwrap();
117 let t2 = s.invoke(None, || None).unwrap();
118 assert_ne!(t1, t2);
119 }
120
121 #[rstest]
122 fn uuid_ignores_provided_token_and_does_not_invoke_get_event() {
123 let s = IdempotencyStrategy::<TestPayload>::Uuid;
124 let token = s
125 .invoke(Some("user-tok".into()), || {
126 panic!("get_event must not be called")
127 })
128 .unwrap();
129 assert_ne!(token, "user-tok");
130 }
131
132 #[rstest]
133 fn custom_invokes_closure_and_derives_token_from_event() {
134 fn derive(e: &Event<TestPayload>) -> String {
135 format!("d:{}", e.payload.as_value().0)
136 }
137 let s = IdempotencyStrategy::<TestPayload>::Custom(derive);
138 let called = Cell::new(false);
139 let result = s.invoke(None, || {
140 called.set(true);
141 Some(test_event())
142 });
143 assert!(called.get());
144 assert_eq!(result, Some("d:p".to_string()));
145 }
146
147 #[rstest]
148 fn custom_ignores_provided_token() {
149 fn derive(_: &Event<TestPayload>) -> String {
150 "from-closure".into()
151 }
152 let s = IdempotencyStrategy::<TestPayload>::Custom(derive);
153 let result = s.invoke(Some("user".into()), || Some(test_event()));
154 assert_eq!(result, Some("from-closure".to_string()));
155 }
156
157 #[rstest]
158 #[should_panic(expected = "Strategy is Custom, but no Event context provided")]
159 fn custom_panics_when_get_event_returns_none() {
160 fn derive(_: &Event<TestPayload>) -> String {
161 "x".into()
162 }
163 let s = IdempotencyStrategy::<TestPayload>::Custom(derive);
164 let _ = s.invoke(None, || None);
165 }
166
167 #[rstest]
168 fn none_returns_none_and_ignores_inputs() {
169 let s = IdempotencyStrategy::<TestPayload>::None;
170 assert_eq!(s.invoke(Some("x".into()), || None), None);
171 }
172
173 #[rstest]
174 fn none_does_not_invoke_get_event() {
175 let s = IdempotencyStrategy::<TestPayload>::None;
176 let _ = s.invoke(None, || panic!("get_event must not be called"));
177 }
178}