camel_api/idempotent.rs
1//! IdempotentRepository trait — pluggable key store for Idempotent Consumer EIP.
2//!
3//! Contract C1: `contains()` returns `Result<bool, CamelError>` (NOT `bool`)
4//! because backends (Redis, JDBC) can have transient read failures. The
5//! Idempotent Consumer propagates `Err`, never treats a failed read as
6//! "not a duplicate."
7
8use crate::CamelError;
9
10/// Pluggable key store for the Idempotent Consumer EIP (and similar patterns).
11///
12/// # Contract (C1)
13///
14/// - `contains` returns `Result<bool, CamelError>`. A transient backend failure
15/// MUST propagate as `Err(CamelError)`, NOT as `Ok(false)`.
16/// - `add` returns `Ok(true)` if the key was newly inserted, `Ok(false)` if it
17/// was already present.
18/// - `remove` succeeds even if the key does not exist.
19/// - `clear` removes all keys.
20#[async_trait::async_trait]
21pub trait IdempotentRepository: Send + Sync + std::fmt::Debug + 'static {
22 /// Human-readable name for diagnostics.
23 fn name(&self) -> &str;
24
25 /// Check whether `key` exists in the repository.
26 ///
27 /// Returns `Err` on transient backend failure (contract C1).
28 async fn contains(&self, key: &str) -> Result<bool, CamelError>;
29
30 /// Insert `key` if absent.
31 ///
32 /// Returns `Ok(true)` if newly added, `Ok(false)` if already present.
33 async fn add(&self, key: &str) -> Result<bool, CamelError>;
34
35 /// Remove `key` if present. Succeeds even if the key does not exist.
36 async fn remove(&self, key: &str) -> Result<(), CamelError>;
37
38 /// Remove all keys from the repository.
39 async fn clear(&self) -> Result<(), CamelError>;
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45 use std::sync::Arc;
46
47 /// Stub that always returns Ok(false) for contains, Ok(true) for add.
48 #[derive(Debug)]
49 struct StubRepo;
50
51 #[async_trait::async_trait]
52 impl IdempotentRepository for StubRepo {
53 fn name(&self) -> &str {
54 "stub"
55 }
56
57 async fn contains(&self, _key: &str) -> Result<bool, CamelError> {
58 Ok(false)
59 }
60
61 async fn add(&self, _key: &str) -> Result<bool, CamelError> {
62 Ok(true)
63 }
64
65 async fn remove(&self, _key: &str) -> Result<(), CamelError> {
66 Ok(())
67 }
68
69 async fn clear(&self) -> Result<(), CamelError> {
70 Ok(())
71 }
72 }
73
74 /// Stub compiles and can be stored as `Arc<dyn IdempotentRepository>`.
75 #[test]
76 fn stub_is_object_safe() {
77 let repo: Arc<dyn IdempotentRepository> = Arc::new(StubRepo);
78 assert_eq!(repo.name(), "stub");
79 }
80
81 #[tokio::test]
82 async fn stub_contains_returns_false() {
83 let repo = StubRepo;
84 let result = repo.contains("any-key").await.unwrap();
85 assert!(!result);
86 }
87
88 #[tokio::test]
89 async fn stub_add_returns_true() {
90 let repo = StubRepo;
91 let result = repo.add("any-key").await.unwrap();
92 assert!(result);
93 }
94}