1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "lowercase")]
10pub enum IdempotencyStatus {
11 Executed,
13 Replayed,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
21pub struct ScopedKey {
22 pub team_id: String,
23 pub user_id: String,
24 pub method: String,
25 pub idempotency_key: String,
26}
27
28impl ScopedKey {
29 pub fn new(team_id: String, user_id: String, method: String, idempotency_key: String) -> Self {
31 Self {
32 team_id,
33 user_id,
34 method,
35 idempotency_key,
36 }
37 }
38}
39
40impl std::fmt::Display for ScopedKey {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(
43 f,
44 "{}/{}/{}/{}",
45 self.team_id, self.user_id, self.method, self.idempotency_key
46 )
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52pub struct RequestFingerprint {
53 pub hash: String,
55}
56
57impl RequestFingerprint {
58 pub fn from_params(params: &serde_json::Map<String, Value>) -> Self {
60 use sha2::{Digest, Sha256};
61
62 let mut sorted_params: Vec<_> = params.iter().collect();
64 sorted_params.sort_by_key(|(k, _)| *k);
65
66 let mut hasher = Sha256::new();
67 for (key, value) in sorted_params {
68 hasher.update(key.as_bytes());
69 hasher.update(b":");
70 let value_str = serde_json::to_string(value).unwrap_or_default();
72 hasher.update(value_str.as_bytes());
73 hasher.update(b";");
74 }
75
76 let result = hasher.finalize();
77 Self {
78 hash: format!("{:x}", result),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct IdempotencyEntry {
86 pub fingerprint: RequestFingerprint,
88
89 pub response: Value,
91
92 pub created_at: u64,
94
95 pub expires_at: u64,
97}
98
99impl IdempotencyEntry {
100 pub fn new(fingerprint: RequestFingerprint, response: Value, ttl_seconds: u64) -> Self {
102 let now = SystemTime::now()
103 .duration_since(UNIX_EPOCH)
104 .unwrap()
105 .as_secs();
106
107 Self {
108 fingerprint,
109 response,
110 created_at: now,
111 expires_at: now + ttl_seconds,
112 }
113 }
114
115 pub fn is_expired(&self) -> bool {
117 let now = SystemTime::now()
118 .duration_since(UNIX_EPOCH)
119 .unwrap()
120 .as_secs();
121 now > self.expires_at
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use serde_json::json;
129
130 #[test]
131 fn test_scoped_key_creation() {
132 let key = ScopedKey::new(
133 "T123".into(),
134 "U456".into(),
135 "chat.postMessage".into(),
136 "my-key".into(),
137 );
138
139 assert_eq!(key.team_id, "T123");
140 assert_eq!(key.user_id, "U456");
141 assert_eq!(key.method, "chat.postMessage");
142 assert_eq!(key.idempotency_key, "my-key");
143 }
144
145 #[test]
146 fn test_scoped_key_to_string() {
147 let key = ScopedKey::new(
148 "T123".into(),
149 "U456".into(),
150 "chat.postMessage".into(),
151 "my-key".into(),
152 );
153
154 assert_eq!(key.to_string(), "T123/U456/chat.postMessage/my-key");
155 }
156
157 #[test]
158 fn test_fingerprint_same_params() {
159 let mut params1 = serde_json::Map::new();
160 params1.insert("channel".into(), json!("C123"));
161 params1.insert("text".into(), json!("hello"));
162
163 let mut params2 = serde_json::Map::new();
164 params2.insert("channel".into(), json!("C123"));
165 params2.insert("text".into(), json!("hello"));
166
167 let fp1 = RequestFingerprint::from_params(¶ms1);
168 let fp2 = RequestFingerprint::from_params(¶ms2);
169
170 assert_eq!(fp1.hash, fp2.hash);
171 }
172
173 #[test]
174 fn test_fingerprint_different_params() {
175 let mut params1 = serde_json::Map::new();
176 params1.insert("channel".into(), json!("C123"));
177 params1.insert("text".into(), json!("hello"));
178
179 let mut params2 = serde_json::Map::new();
180 params2.insert("channel".into(), json!("C123"));
181 params2.insert("text".into(), json!("goodbye"));
182
183 let fp1 = RequestFingerprint::from_params(¶ms1);
184 let fp2 = RequestFingerprint::from_params(¶ms2);
185
186 assert_ne!(fp1.hash, fp2.hash);
187 }
188
189 #[test]
190 fn test_fingerprint_order_independence() {
191 let mut params1 = serde_json::Map::new();
192 params1.insert("channel".into(), json!("C123"));
193 params1.insert("text".into(), json!("hello"));
194 params1.insert("thread_ts".into(), json!("1234567890.123456"));
195
196 let mut params2 = serde_json::Map::new();
197 params2.insert("text".into(), json!("hello"));
198 params2.insert("thread_ts".into(), json!("1234567890.123456"));
199 params2.insert("channel".into(), json!("C123"));
200
201 let fp1 = RequestFingerprint::from_params(¶ms1);
202 let fp2 = RequestFingerprint::from_params(¶ms2);
203
204 assert_eq!(fp1.hash, fp2.hash);
206 }
207
208 #[test]
209 fn test_entry_expiration() {
210 let mut params = serde_json::Map::new();
211 params.insert("test".into(), json!("value"));
212 let fingerprint = RequestFingerprint::from_params(¶ms);
213
214 let response = json!({"ok": true});
215
216 let entry = IdempotencyEntry::new(fingerprint, response, 1);
218 assert!(!entry.is_expired());
219
220 std::thread::sleep(std::time::Duration::from_secs(2));
222 assert!(entry.is_expired());
223 }
224}