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    #[must_use]
28    pub fn new() -> Self {
29        Self(ulid::Ulid::new())
30    }
31
32    /// Create from an existing ULID
33    #[must_use]
34    pub const fn from_ulid(ulid: ulid::Ulid) -> Self {
35        Self(ulid)
36    }
37
38    /// Get the inner ULID
39    #[must_use]
40    pub const fn inner(&self) -> ulid::Ulid {
41        self.0
42    }
43
44    /// Get timestamp when this key was generated
45    #[must_use]
46    pub const fn timestamp(&self) -> u64 {
47        self.0.timestamp_ms()
48    }
49}
50
51impl FromStr for IdempotencyKey {
52    type Err = crate::error::PluginError;
53
54    fn from_str(s: &str) -> crate::Result<Self> {
55        ulid::Ulid::from_str(s)
56            .map(Self)
57            .map_err(|e| crate::error::PluginError::Other(format!("Invalid ULID: {e}")))
58    }
59}
60
61impl Default for IdempotencyKey {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl fmt::Display for IdempotencyKey {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{}", self.0)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_new_key_is_unique() {
79        let key1 = IdempotencyKey::new();
80        let key2 = IdempotencyKey::new();
81        assert_ne!(key1, key2);
82    }
83
84    #[test]
85    fn test_key_serialization() -> std::result::Result<(), Box<dyn std::error::Error>> {
86        let key = IdempotencyKey::new();
87        let json = serde_json::to_string(&key)?;
88        let key2: IdempotencyKey = serde_json::from_str(&json)?;
89        assert_eq!(key, key2);
90        Ok(())
91    }
92
93    #[test]
94    fn test_key_display() {
95        let key = IdempotencyKey::new();
96        let s = format!("{key}");
97        assert!(!s.is_empty());
98    }
99
100    #[test]
101    fn test_key_timestamp() {
102        let key = IdempotencyKey::new();
103        let ts = key.timestamp();
104        assert!(ts > 0);
105    }
106}