Skip to main content

emergent_client/types/
message_id.rs

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