1use crate::config::OutboxConfig;
36#[cfg(feature = "dlq")]
37use crate::dlq::storage::DlqHeap;
38use crate::error::OutboxError;
39use crate::manager::OutboxManager;
40use crate::prelude::{OutboxStorage, Transport};
41use serde::Serialize;
42use std::fmt::Debug;
43use std::sync::Arc;
44use tokio::sync::watch::Receiver;
45
46pub struct OutboxManagerBuilder<S, P, PT>
67where
68 PT: Debug + Clone + Serialize,
69{
70 storage: Option<Arc<S>>,
71 publisher: Option<Arc<P>>,
72 config: Option<Arc<OutboxConfig<PT>>>,
73 shutdown_rx: Option<Receiver<bool>>,
74 #[cfg(feature = "dlq")]
75 dlq_heap: Option<Arc<dyn DlqHeap>>,
76}
77impl<S, P, PT> Default for OutboxManagerBuilder<S, P, PT>
78where
79 PT: Debug + Clone + Serialize,
80{
81 fn default() -> Self {
82 Self {
83 storage: None,
84 publisher: None,
85 config: None,
86 shutdown_rx: None,
87 #[cfg(feature = "dlq")]
88 dlq_heap: None,
89 }
90 }
91}
92
93impl<S, P, PT> OutboxManagerBuilder<S, P, PT>
94where
95 S: OutboxStorage<PT> + Send + Sync + 'static,
96 P: Transport<PT> + Send + Sync + 'static,
97 PT: Debug + Clone + Serialize + Send + Sync + 'static,
98{
99 #[must_use]
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 #[must_use]
111 pub fn storage(mut self, s: Arc<S>) -> Self {
112 self.storage = Some(s);
113 self
114 }
115
116 #[must_use]
119 pub fn publisher(mut self, p: Arc<P>) -> Self {
120 self.publisher = Some(p);
121 self
122 }
123
124 #[must_use]
127 pub fn config(mut self, config: Arc<OutboxConfig<PT>>) -> Self {
128 self.config = Some(config);
129 self
130 }
131
132 #[must_use]
135 pub fn shutdown_rx(mut self, rx: Receiver<bool>) -> Self {
136 self.shutdown_rx = Some(rx);
137 self
138 }
139
140 #[cfg(feature = "dlq")]
144 #[must_use]
145 pub fn dlq_heap(mut self, heap: Arc<dyn DlqHeap>) -> Self {
146 self.dlq_heap = Some(heap);
147 self
148 }
149
150 pub fn build(self) -> Result<OutboxManager<S, P, PT>, OutboxError> {
159 #[cfg(feature = "dlq")]
160 return Ok(OutboxManager::new(
161 self.storage
162 .ok_or_else(|| OutboxError::ConfigError("Storage is missing".to_string()))?,
163 self.publisher
164 .ok_or_else(|| OutboxError::ConfigError("Publisher is missing".to_string()))?,
165 self.config
166 .ok_or_else(|| OutboxError::ConfigError("Config is missing".to_string()))?,
167 self.dlq_heap
168 .ok_or_else(|| OutboxError::ConfigError("DLQ heap is missing".to_string()))?,
169 self.shutdown_rx.ok_or_else(|| {
170 OutboxError::ConfigError("Shutdown channel is missing".to_string())
171 })?,
172 ));
173 #[cfg(not(feature = "dlq"))]
174 return Ok(OutboxManager::new(
175 self.storage
176 .ok_or_else(|| OutboxError::ConfigError("Storage is missing".to_string()))?,
177 self.publisher
178 .ok_or_else(|| OutboxError::ConfigError("Publisher is missing".to_string()))?,
179 self.config
180 .ok_or_else(|| OutboxError::ConfigError("Config is missing".to_string()))?,
181 self.shutdown_rx.ok_or_else(|| {
182 OutboxError::ConfigError("Shutdown channel is missing".to_string())
183 })?,
184 ));
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::config::IdempotencyStrategy;
192 use crate::dlq::storage::MockDlqHeap;
193 use crate::publisher::MockTransport;
194 use crate::storage::MockOutboxStorage;
195 use rstest::rstest;
196 use serde::Deserialize;
197 use tokio::sync::watch;
198
199 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200 enum SomeDomainEvent {
201 SomeEvent(String),
202 }
203
204 #[rstest]
205 fn test_success_build() {
206 let config = OutboxConfig {
207 batch_size: 100,
208 retention_days: 7,
209 gc_interval_secs: 3600,
210 poll_interval_secs: 5,
211 lock_timeout_mins: 5,
212 idempotency_strategy: IdempotencyStrategy::<SomeDomainEvent>::None,
213 ..Default::default()
214 };
215
216 let storage_mock = MockOutboxStorage::<SomeDomainEvent>::new();
217 let transport_mock = MockTransport::<SomeDomainEvent>::new();
218
219 #[cfg(feature = "dlq")]
220 let dlq_heap_mock: MockDlqHeap = MockDlqHeap::new();
221
222 let (_shutdown_tx, shutdown_rx) = watch::channel(false);
223
224 #[cfg(feature = "dlq")]
225 let outbox_manager = OutboxManagerBuilder::new()
226 .storage(Arc::new(storage_mock))
227 .publisher(Arc::new(transport_mock))
228 .config(Arc::new(config))
229 .shutdown_rx(shutdown_rx)
230 .dlq_heap(Arc::new(dlq_heap_mock))
231 .build();
232
233 #[cfg(not(feature = "dlq"))]
234 let outbox_manager = OutboxManagerBuilder::new()
235 .storage(Arc::new(storage_mock))
236 .publisher(Arc::new(transport_mock))
237 .config(Arc::new(config))
238 .shutdown_rx(shutdown_rx)
239 .build();
240
241 assert!(outbox_manager.is_ok());
242 }
243
244 type Builder = OutboxManagerBuilder<
245 MockOutboxStorage<SomeDomainEvent>,
246 MockTransport<SomeDomainEvent>,
247 SomeDomainEvent,
248 >;
249
250 fn default_config() -> Arc<OutboxConfig<SomeDomainEvent>> {
251 Arc::new(OutboxConfig {
252 batch_size: 100,
253 retention_days: 7,
254 gc_interval_secs: 3600,
255 poll_interval_secs: 5,
256 lock_timeout_mins: 5,
257 idempotency_strategy: IdempotencyStrategy::None,
258 ..Default::default()
259 })
260 }
261
262 #[rstest]
263 fn default_builder_fails_on_build_with_config_error() {
264 let result = Builder::default().build();
265 assert!(matches!(result, Err(OutboxError::ConfigError(_))));
266 }
267
268 fn assert_config_error_with(
269 result: Result<
270 OutboxManager<
271 MockOutboxStorage<SomeDomainEvent>,
272 MockTransport<SomeDomainEvent>,
273 SomeDomainEvent,
274 >,
275 OutboxError,
276 >,
277 needle: &str,
278 ) {
279 match result {
280 Err(OutboxError::ConfigError(msg)) => {
281 assert!(msg.contains(needle), "expected '{needle}' in '{msg}'");
282 }
283 Err(other) => panic!("expected ConfigError, got {other:?}"),
284 Ok(_) => panic!("expected ConfigError, got Ok(..)"),
285 }
286 }
287
288 #[rstest]
289 fn new_matches_default() {
290 let r1 = Builder::new().build();
291 let r2 = Builder::default().build();
292 let m1 = match r1 {
293 Err(OutboxError::ConfigError(m)) => m,
294 _ => panic!("new().build() should fail with ConfigError"),
295 };
296 let m2 = match r2 {
297 Err(OutboxError::ConfigError(m)) => m,
298 _ => panic!("default().build() should fail with ConfigError"),
299 };
300 assert_eq!(m1, m2);
301 }
302
303 #[rstest]
304 fn build_fails_without_storage() {
305 let (_tx, rx) = watch::channel(false);
306 let b = Builder::new()
307 .publisher(Arc::new(MockTransport::new()))
308 .config(default_config())
309 .shutdown_rx(rx);
310 #[cfg(feature = "dlq")]
311 let b = b.dlq_heap(Arc::new(MockDlqHeap::new()));
312
313 assert_config_error_with(b.build(), "Storage");
314 }
315
316 #[rstest]
317 fn build_fails_without_publisher() {
318 let (_tx, rx) = watch::channel(false);
319 let b = Builder::new()
320 .storage(Arc::new(MockOutboxStorage::new()))
321 .config(default_config())
322 .shutdown_rx(rx);
323 #[cfg(feature = "dlq")]
324 let b = b.dlq_heap(Arc::new(MockDlqHeap::new()));
325
326 assert_config_error_with(b.build(), "Publisher");
327 }
328
329 #[rstest]
330 fn build_fails_without_config() {
331 let (_tx, rx) = watch::channel(false);
332 let b = Builder::new()
333 .storage(Arc::new(MockOutboxStorage::new()))
334 .publisher(Arc::new(MockTransport::new()))
335 .shutdown_rx(rx);
336 #[cfg(feature = "dlq")]
337 let b = b.dlq_heap(Arc::new(MockDlqHeap::new()));
338
339 assert_config_error_with(b.build(), "Config");
340 }
341
342 #[rstest]
343 fn build_fails_without_shutdown_rx() {
344 let b = Builder::new()
345 .storage(Arc::new(MockOutboxStorage::new()))
346 .publisher(Arc::new(MockTransport::new()))
347 .config(default_config());
348 #[cfg(feature = "dlq")]
349 let b = b.dlq_heap(Arc::new(MockDlqHeap::new()));
350
351 assert_config_error_with(b.build(), "Shutdown");
352 }
353
354 #[cfg(feature = "dlq")]
355 #[rstest]
356 fn build_fails_without_dlq_heap() {
357 let (_tx, rx) = watch::channel(false);
358 let result = Builder::new()
359 .storage(Arc::new(MockOutboxStorage::new()))
360 .publisher(Arc::new(MockTransport::new()))
361 .config(default_config())
362 .shutdown_rx(rx)
363 .build();
364 assert_config_error_with(result, "DLQ");
365 }
366
367 #[rstest]
368 fn build_is_insensitive_to_setter_order() {
369 let (_tx, rx) = watch::channel(false);
370 let b = Builder::new()
371 .shutdown_rx(rx)
372 .config(default_config())
373 .publisher(Arc::new(MockTransport::new()))
374 .storage(Arc::new(MockOutboxStorage::new()));
375 #[cfg(feature = "dlq")]
376 let b = b.dlq_heap(Arc::new(MockDlqHeap::new()));
377
378 assert!(b.build().is_ok());
379 }
380}