use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum IdempotencyStatus {
Executed,
Replayed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ScopedKey {
pub team_id: String,
pub user_id: String,
pub method: String,
pub idempotency_key: String,
}
impl ScopedKey {
pub fn new(team_id: String, user_id: String, method: String, idempotency_key: String) -> Self {
Self {
team_id,
user_id,
method,
idempotency_key,
}
}
}
impl std::fmt::Display for ScopedKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}/{}",
self.team_id, self.user_id, self.method, self.idempotency_key
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RequestFingerprint {
pub hash: String,
}
impl RequestFingerprint {
pub fn from_params(params: &serde_json::Map<String, Value>) -> Self {
use sha2::{Digest, Sha256};
let mut sorted_params: Vec<_> = params.iter().collect();
sorted_params.sort_by_key(|(k, _)| *k);
let mut hasher = Sha256::new();
for (key, value) in sorted_params {
hasher.update(key.as_bytes());
hasher.update(b":");
let value_str = serde_json::to_string(value).unwrap_or_default();
hasher.update(value_str.as_bytes());
hasher.update(b";");
}
let result = hasher.finalize();
Self {
hash: format!("{:x}", result),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdempotencyEntry {
pub fingerprint: RequestFingerprint,
pub response: Value,
pub created_at: u64,
pub expires_at: u64,
}
impl IdempotencyEntry {
pub fn new(fingerprint: RequestFingerprint, response: Value, ttl_seconds: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
fingerprint,
response,
created_at: now,
expires_at: now + ttl_seconds,
}
}
pub fn is_expired(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
now > self.expires_at
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_scoped_key_creation() {
let key = ScopedKey::new(
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
"my-key".into(),
);
assert_eq!(key.team_id, "T123");
assert_eq!(key.user_id, "U456");
assert_eq!(key.method, "chat.postMessage");
assert_eq!(key.idempotency_key, "my-key");
}
#[test]
fn test_scoped_key_to_string() {
let key = ScopedKey::new(
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
"my-key".into(),
);
assert_eq!(key.to_string(), "T123/U456/chat.postMessage/my-key");
}
#[test]
fn test_fingerprint_same_params() {
let mut params1 = serde_json::Map::new();
params1.insert("channel".into(), json!("C123"));
params1.insert("text".into(), json!("hello"));
let mut params2 = serde_json::Map::new();
params2.insert("channel".into(), json!("C123"));
params2.insert("text".into(), json!("hello"));
let fp1 = RequestFingerprint::from_params(¶ms1);
let fp2 = RequestFingerprint::from_params(¶ms2);
assert_eq!(fp1.hash, fp2.hash);
}
#[test]
fn test_fingerprint_different_params() {
let mut params1 = serde_json::Map::new();
params1.insert("channel".into(), json!("C123"));
params1.insert("text".into(), json!("hello"));
let mut params2 = serde_json::Map::new();
params2.insert("channel".into(), json!("C123"));
params2.insert("text".into(), json!("goodbye"));
let fp1 = RequestFingerprint::from_params(¶ms1);
let fp2 = RequestFingerprint::from_params(¶ms2);
assert_ne!(fp1.hash, fp2.hash);
}
#[test]
fn test_fingerprint_order_independence() {
let mut params1 = serde_json::Map::new();
params1.insert("channel".into(), json!("C123"));
params1.insert("text".into(), json!("hello"));
params1.insert("thread_ts".into(), json!("1234567890.123456"));
let mut params2 = serde_json::Map::new();
params2.insert("text".into(), json!("hello"));
params2.insert("thread_ts".into(), json!("1234567890.123456"));
params2.insert("channel".into(), json!("C123"));
let fp1 = RequestFingerprint::from_params(¶ms1);
let fp2 = RequestFingerprint::from_params(¶ms2);
assert_eq!(fp1.hash, fp2.hash);
}
#[test]
fn test_entry_expiration() {
let mut params = serde_json::Map::new();
params.insert("test".into(), json!("value"));
let fingerprint = RequestFingerprint::from_params(¶ms);
let response = json!({"ok": true});
let entry = IdempotencyEntry::new(fingerprint, response, 1);
assert!(!entry.is_expired());
std::thread::sleep(std::time::Duration::from_secs(2));
assert!(entry.is_expired());
}
}