Skip to main content

emergent_client/types/
causation_id.rs

1//! Causation ID type using TypeID format.
2
3use super::MessageId;
4use mti::prelude::*;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::fmt;
7use std::str::FromStr;
8
9/// A causation identifier tracking which message triggered this one.
10///
11/// Format: `cau_<uuid_v7>` or `msg_<uuid_v7>` (when derived from a MessageId)
12/// Example: `cau_01h455vb4pex5vsknk084sn02q`
13///
14/// Causation IDs form a chain that can be traced back to understand
15/// the sequence of events that led to a particular message.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct CausationId(MagicTypeId);
18
19/// Error returned when parsing an invalid causation ID.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum InvalidCausationId {
22    /// TypeID parsing failed.
23    Parse(MagicTypeIdError),
24    /// Wrong prefix (expected "cau" or "msg").
25    WrongPrefix {
26        expected: &'static str,
27        actual: String,
28    },
29}
30
31impl fmt::Display for InvalidCausationId {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Parse(e) => write!(f, "invalid causation ID: {e}"),
35            Self::WrongPrefix { expected, actual } => {
36                write!(f, "expected prefix '{expected}', got '{actual}'")
37            }
38        }
39    }
40}
41
42impl std::error::Error for InvalidCausationId {}
43
44impl CausationId {
45    /// The TypeID prefix for causation identifiers.
46    pub const PREFIX: &'static str = "cau";
47
48    /// Creates a new causation ID with a fresh UUIDv7 (time-sortable).
49    #[must_use]
50    pub fn new() -> Self {
51        Self(Self::PREFIX.create_type_id::<V7>())
52    }
53
54    /// Parses a causation ID from a string.
55    ///
56    /// Accepts both "cau_" and "msg_" prefixes since causation IDs
57    /// often reference message IDs.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if the string is not a valid TypeID or has an invalid prefix.
62    pub fn parse(s: &str) -> Result<Self, InvalidCausationId> {
63        let id = MagicTypeId::from_str(s).map_err(InvalidCausationId::Parse)?;
64
65        let prefix = id.prefix().as_str();
66        if prefix != Self::PREFIX && prefix != "msg" {
67            return Err(InvalidCausationId::WrongPrefix {
68                expected: "cau or msg",
69                actual: prefix.to_string(),
70            });
71        }
72
73        Ok(Self(id))
74    }
75
76    /// Returns the underlying MagicTypeId.
77    #[must_use]
78    pub fn inner(&self) -> &MagicTypeId {
79        &self.0
80    }
81
82    /// Returns the TypeID suffix (base32-encoded UUID).
83    #[must_use]
84    pub fn suffix(&self) -> String {
85        self.0.suffix().to_string()
86    }
87}
88
89impl Default for CausationId {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl fmt::Display for CausationId {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "{}", self.0)
98    }
99}
100
101impl FromStr for CausationId {
102    type Err = InvalidCausationId;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        Self::parse(s)
106    }
107}
108
109impl AsRef<MagicTypeId> for CausationId {
110    fn as_ref(&self) -> &MagicTypeId {
111        &self.0
112    }
113}
114
115/// Allow converting MessageId to CausationId.
116///
117/// This is a common pattern where the ID of the message that triggered
118/// a new message becomes the causation ID of that new message.
119impl From<MessageId> for CausationId {
120    fn from(msg_id: MessageId) -> Self {
121        Self(msg_id.inner().clone())
122    }
123}
124
125/// Allow converting a reference to MessageId to CausationId.
126impl From<&MessageId> for CausationId {
127    fn from(msg_id: &MessageId) -> Self {
128        Self(msg_id.inner().clone())
129    }
130}
131
132impl Serialize for CausationId {
133    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134    where
135        S: Serializer,
136    {
137        self.0.to_string().serialize(serializer)
138    }
139}
140
141impl<'de> Deserialize<'de> for CausationId {
142    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
143    where
144        D: Deserializer<'de>,
145    {
146        let s = String::deserialize(deserializer)?;
147        Self::parse(&s).map_err(serde::de::Error::custom)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn new_creates_valid_causation_id() {
157        let id = CausationId::new();
158        assert!(id.to_string().starts_with("cau_"));
159    }
160
161    #[test]
162    fn parse_valid_causation_id() {
163        let id_str = CausationId::new().to_string();
164        let parsed = CausationId::parse(&id_str);
165        assert!(parsed.is_ok());
166    }
167
168    #[test]
169    fn parse_accepts_msg_prefix() {
170        let result = CausationId::parse("msg_01h455vb4pex5vsknk084sn02q");
171        assert!(result.is_ok());
172    }
173
174    #[test]
175    fn parse_wrong_prefix_fails() {
176        let result = CausationId::parse("cor_01h455vb4pex5vsknk084sn02q");
177        assert!(matches!(
178            result,
179            Err(InvalidCausationId::WrongPrefix { .. })
180        ));
181    }
182
183    #[test]
184    fn causation_ids_are_unique() {
185        let id1 = CausationId::new();
186        let id2 = CausationId::new();
187        assert_ne!(id1, id2);
188    }
189
190    #[test]
191    fn from_message_id() {
192        let msg_id = MessageId::new();
193        let msg_id_str = msg_id.to_string();
194        let causation_id: CausationId = msg_id.into();
195        // The causation ID preserves the msg_ prefix
196        assert_eq!(causation_id.to_string(), msg_id_str);
197    }
198
199    #[test]
200    fn from_message_id_ref() {
201        let msg_id = MessageId::new();
202        let causation_id: CausationId = (&msg_id).into();
203        assert_eq!(causation_id.to_string(), msg_id.to_string());
204    }
205
206    #[test]
207    fn serde_roundtrip() -> Result<(), serde_json::Error> {
208        let id = CausationId::new();
209        let json = serde_json::to_string(&id)?;
210        let restored: CausationId = serde_json::from_str(&json)?;
211        assert_eq!(id, restored);
212        Ok(())
213    }
214}