1use crate::error::MessengerError;
25use serde::{Deserialize, Serialize};
26use std::fmt;
27
28fn split_sigil_id<'a>(
36 kind: &'static str,
37 value: &'a str,
38 sigil: char,
39) -> Result<(&'a str, &'a str), MessengerError> {
40 let invalid = |reason: &str| MessengerError::InvalidId {
41 kind,
42 value: value.to_string(),
43 reason: reason.to_string(),
44 };
45 let rest = value
46 .strip_prefix(sigil)
47 .ok_or_else(|| invalid(&format!("must start with '{sigil}'")))?;
48 let (localpart, server) = rest
49 .split_once(':')
50 .ok_or_else(|| invalid("missing ':' separating localpart and server name"))?;
51 if localpart.is_empty() {
52 return Err(invalid("localpart must not be empty"));
53 }
54 if server.is_empty() {
55 return Err(invalid("server name must not be empty"));
56 }
57 if localpart.chars().any(char::is_whitespace) || server.chars().any(char::is_whitespace) {
58 return Err(invalid("must not contain whitespace"));
59 }
60 Ok((localpart, server))
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
66#[serde(transparent)]
67pub struct UserId(String);
68
69impl UserId {
70 pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
72 let value = value.into();
73 split_sigil_id("UserId", &value, '@')?;
74 Ok(Self(value))
75 }
76
77 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81}
82
83impl fmt::Display for UserId {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str(&self.0)
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
93#[serde(transparent)]
94pub struct RoomId(String);
95
96impl RoomId {
97 pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
99 let value = value.into();
100 split_sigil_id("RoomId", &value, '!')?;
101 Ok(Self(value))
102 }
103
104 pub fn as_str(&self) -> &str {
106 &self.0
107 }
108}
109
110impl fmt::Display for RoomId {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(&self.0)
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
120#[serde(transparent)]
121pub struct EventId(String);
122
123impl EventId {
124 pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
126 let value = value.into();
127 let opaque = value.strip_prefix('$').ok_or_else(|| MessengerError::InvalidId {
128 kind: "EventId",
129 value: value.clone(),
130 reason: "must start with '$'".to_string(),
131 })?;
132 if opaque.is_empty() || opaque.chars().any(char::is_whitespace) {
133 return Err(MessengerError::InvalidId {
134 kind: "EventId",
135 value,
136 reason: "opaque part must be non-empty and contain no whitespace".to_string(),
137 });
138 }
139 Ok(Self(value))
140 }
141
142 pub fn as_str(&self) -> &str {
144 &self.0
145 }
146}
147
148impl fmt::Display for EventId {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(&self.0)
151 }
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
157#[serde(transparent)]
158pub struct DeviceId(String);
159
160impl DeviceId {
161 pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
163 let value = value.into();
164 if value.is_empty() || value.chars().any(char::is_whitespace) {
165 return Err(MessengerError::InvalidId {
166 kind: "DeviceId",
167 value,
168 reason: "must be non-empty and contain no whitespace".to_string(),
169 });
170 }
171 Ok(Self(value))
172 }
173
174 pub fn as_str(&self) -> &str {
176 &self.0
177 }
178}
179
180impl fmt::Display for DeviceId {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.write_str(&self.0)
183 }
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
191#[serde(transparent)]
192pub struct TxnId(String);
193
194impl TxnId {
195 pub fn new(seed: u64) -> Self {
201 Self(format!("m.txn.{seed:016x}"))
202 }
203
204 pub fn as_str(&self) -> &str {
206 &self.0
207 }
208
209 pub fn as_seed(&self) -> Option<u64> {
216 self.0.strip_prefix("m.txn.").and_then(|hex| u64::from_str_radix(hex, 16).ok())
217 }
218}
219
220impl fmt::Display for TxnId {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 f.write_str(&self.0)
223 }
224}
225
226impl From<String> for TxnId {
227 fn from(value: String) -> Self {
232 Self(value)
233 }
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
241#[serde(transparent)]
242pub struct RequestId(String);
243
244impl RequestId {
245 pub fn next(seed: u64) -> Self {
249 Self(format!("req.{seed:016x}"))
250 }
251
252 pub fn as_str(&self) -> &str {
254 &self.0
255 }
256
257 pub fn as_seed(&self) -> Option<u64> {
262 self.0.strip_prefix("req.").and_then(|hex| u64::from_str_radix(hex, 16).ok())
263 }
264}
265
266impl fmt::Display for RequestId {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 f.write_str(&self.0)
269 }
270}
271
272impl From<String> for RequestId {
273 fn from(value: String) -> Self {
278 Self(value)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn user_id_accepts_a_well_formed_sigil() {
288 let id = UserId::parse("@alice:example.org").expect("valid user id");
289 assert_eq!(id.as_str(), "@alice:example.org");
290 assert_eq!(id.to_string(), "@alice:example.org");
291 }
292
293 #[test]
294 fn user_id_accepts_a_server_name_with_a_port() {
295 let id = UserId::parse("@alice:example.org:8448").expect("port suffix is part of server_name");
296 assert_eq!(id.as_str(), "@alice:example.org:8448");
297 }
298
299 #[test]
300 fn user_id_rejects_a_missing_sigil() {
301 let err = UserId::parse("alice:example.org").unwrap_err();
302 assert!(matches!(err, MessengerError::InvalidId { kind: "UserId", .. }));
303 }
304
305 #[test]
306 fn user_id_rejects_a_missing_server_name() {
307 assert!(UserId::parse("@alice").is_err());
308 assert!(UserId::parse("@alice:").is_err());
309 }
310
311 #[test]
312 fn user_id_rejects_an_empty_localpart() {
313 assert!(UserId::parse("@:example.org").is_err());
314 }
315
316 #[test]
317 fn room_id_accepts_a_well_formed_sigil() {
318 let id = RoomId::parse("!abc123:example.org").expect("valid room id");
319 assert_eq!(id.as_str(), "!abc123:example.org");
320 }
321
322 #[test]
323 fn room_id_rejects_a_missing_sigil() {
324 assert!(RoomId::parse("abc123:example.org").is_err());
325 }
326
327 #[test]
328 fn event_id_accepts_a_well_formed_sigil() {
329 let id = EventId::parse("$abc123").expect("valid event id");
330 assert_eq!(id.as_str(), "$abc123");
331 }
332
333 #[test]
334 fn event_id_rejects_a_missing_sigil() {
335 assert!(EventId::parse("abc123").is_err());
336 }
337
338 #[test]
339 fn event_id_rejects_an_empty_opaque_part() {
340 assert!(EventId::parse("$").is_err());
341 }
342
343 #[test]
344 fn device_id_rejects_empty_and_whitespace() {
345 assert!(DeviceId::parse("").is_err());
346 assert!(DeviceId::parse("has space").is_err());
347 assert!(DeviceId::parse("ABCDEFGH").is_ok());
348 }
349
350 #[test]
351 fn txn_id_and_request_id_are_monotonic_and_distinct_namespaces() {
352 let a = TxnId::new(0);
353 let b = TxnId::new(1);
354 assert_ne!(a, b);
355 assert!(a.as_str() < b.as_str(), "fixed-width hex keeps lexicographic order monotonic");
356
357 let r0 = RequestId::next(0);
358 let r1 = RequestId::next(1);
359 assert_ne!(r0, r1);
360 assert_ne!(TxnId::new(0).as_str(), RequestId::next(0).as_str());
361 }
362
363 #[test]
364 fn request_id_from_string_round_trips_a_previously_minted_value() {
365 let minted = RequestId::next(7);
366 let recovered = RequestId::from(minted.as_str().to_string());
367 assert_eq!(minted, recovered);
368 }
369
370 #[test]
371 fn as_seed_recovers_the_minting_seed() {
372 assert_eq!(TxnId::new(42).as_seed(), Some(42));
373 assert_eq!(RequestId::next(42).as_seed(), Some(42));
374 assert_eq!(TxnId::from("opaque-server-value".to_string()).as_seed(), None);
375 assert_eq!(RequestId::from("opaque-value".to_string()).as_seed(), None);
376 }
377
378 #[test]
379 fn ids_round_trip_through_serde_as_a_bare_string() {
380 let id = RoomId::parse("!abc:example.org").expect("valid room id");
381 let json = serde_json::to_string(&id).expect("serialize");
382 assert_eq!(json, "\"!abc:example.org\"");
383 let back: RoomId = serde_json::from_str(&json).expect("deserialize");
384 assert_eq!(back, id);
385 }
386}