Skip to main content

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}