Skip to main content

stygian_plugin/domain/
idempotency.rs

1//! Idempotency key type for safe extraction retries
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7/// An idempotency key for deduplicating extraction operations
8///
9/// Uses ULID format (chronological, sortable, unique).
10/// Enables safe retries: if the same extraction is run twice with the same key,
11/// the cached result is returned instead of re-executing.
12///
13/// # Example
14///
15/// ```
16/// use stygian_plugin::domain::IdempotencyKey;
17///
18/// let key1 = IdempotencyKey::new();
19/// let key2 = IdempotencyKey::new();
20/// assert_ne!(key1, key2);
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
23pub struct IdempotencyKey(ulid::Ulid);
24
25impl IdempotencyKey {
26    /// Generate a new idempotency key
27    pub fn new() -> Self {
28        Self(ulid::Ulid::new())
29    }
30
31    /// Create from an existing ULID
32    pub const fn from_ulid(ulid: ulid::Ulid) -> Self {
33        Self(ulid)
34    }
35
36    /// Get the inner ULID
37    pub const fn inner(&self) -> ulid::Ulid {
38        self.0
39    }
40
41    /// Get timestamp when this key was generated
42    pub const fn timestamp(&self) -> u64 {
43        self.0.timestamp_ms()
44    }
45}
46
47impl FromStr for IdempotencyKey {
48    type Err = crate::error::PluginError;
49
50    fn from_str(s: &str) -> crate::Result<Self> {
51        ulid::Ulid::from_str(s)
52            .map(Self)
53            .map_err(|e| crate::error::PluginError::Other(format!("Invalid ULID: {e}")))
54    }
55}
56
57impl Default for IdempotencyKey {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl fmt::Display for IdempotencyKey {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.0)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_new_key_is_unique() {
75        let key1 = IdempotencyKey::new();
76        let key2 = IdempotencyKey::new();
77        assert_ne!(key1, key2);
78    }
79
80    #[test]
81    fn test_key_serialization() -> std::result::Result<(), Box<dyn std::error::Error>> {
82        let key = IdempotencyKey::new();
83        let json = serde_json::to_string(&key)?;
84        let key2: IdempotencyKey = serde_json::from_str(&json)?;
85        assert_eq!(key, key2);
86        Ok(())
87    }
88
89    #[test]
90    fn test_key_display() {
91        let key = IdempotencyKey::new();
92        let s = format!("{key}");
93        assert!(!s.is_empty());
94    }
95
96    #[test]
97    fn test_key_timestamp() {
98        let key = IdempotencyKey::new();
99        let ts = key.timestamp();
100        assert!(ts > 0);
101    }
102}