use crate::error::MessengerError;
use serde::{Deserialize, Serialize};
use std::fmt;
fn split_sigil_id<'a>(
kind: &'static str,
value: &'a str,
sigil: char,
) -> Result<(&'a str, &'a str), MessengerError> {
let invalid = |reason: &str| MessengerError::InvalidId {
kind,
value: value.to_string(),
reason: reason.to_string(),
};
let rest = value
.strip_prefix(sigil)
.ok_or_else(|| invalid(&format!("must start with '{sigil}'")))?;
let (localpart, server) = rest
.split_once(':')
.ok_or_else(|| invalid("missing ':' separating localpart and server name"))?;
if localpart.is_empty() {
return Err(invalid("localpart must not be empty"));
}
if server.is_empty() {
return Err(invalid("server name must not be empty"));
}
if localpart.chars().any(char::is_whitespace) || server.chars().any(char::is_whitespace) {
return Err(invalid("must not contain whitespace"));
}
Ok((localpart, server))
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UserId(String);
impl UserId {
pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
let value = value.into();
split_sigil_id("UserId", &value, '@')?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RoomId(String);
impl RoomId {
pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
let value = value.into();
split_sigil_id("RoomId", &value, '!')?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RoomId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EventId(String);
impl EventId {
pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
let value = value.into();
let opaque = value.strip_prefix('$').ok_or_else(|| MessengerError::InvalidId {
kind: "EventId",
value: value.clone(),
reason: "must start with '$'".to_string(),
})?;
if opaque.is_empty() || opaque.chars().any(char::is_whitespace) {
return Err(MessengerError::InvalidId {
kind: "EventId",
value,
reason: "opaque part must be non-empty and contain no whitespace".to_string(),
});
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for EventId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DeviceId(String);
impl DeviceId {
pub fn parse(value: impl Into<String>) -> Result<Self, MessengerError> {
let value = value.into();
if value.is_empty() || value.chars().any(char::is_whitespace) {
return Err(MessengerError::InvalidId {
kind: "DeviceId",
value,
reason: "must be non-empty and contain no whitespace".to_string(),
});
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for DeviceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TxnId(String);
impl TxnId {
pub fn new(seed: u64) -> Self {
Self(format!("m.txn.{seed:016x}"))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_seed(&self) -> Option<u64> {
self.0.strip_prefix("m.txn.").and_then(|hex| u64::from_str_radix(hex, 16).ok())
}
}
impl fmt::Display for TxnId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for TxnId {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RequestId(String);
impl RequestId {
pub fn next(seed: u64) -> Self {
Self(format!("req.{seed:016x}"))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_seed(&self) -> Option<u64> {
self.0.strip_prefix("req.").and_then(|hex| u64::from_str_radix(hex, 16).ok())
}
}
impl fmt::Display for RequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for RequestId {
fn from(value: String) -> Self {
Self(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_id_accepts_a_well_formed_sigil() {
let id = UserId::parse("@alice:example.org").expect("valid user id");
assert_eq!(id.as_str(), "@alice:example.org");
assert_eq!(id.to_string(), "@alice:example.org");
}
#[test]
fn user_id_accepts_a_server_name_with_a_port() {
let id = UserId::parse("@alice:example.org:8448").expect("port suffix is part of server_name");
assert_eq!(id.as_str(), "@alice:example.org:8448");
}
#[test]
fn user_id_rejects_a_missing_sigil() {
let err = UserId::parse("alice:example.org").unwrap_err();
assert!(matches!(err, MessengerError::InvalidId { kind: "UserId", .. }));
}
#[test]
fn user_id_rejects_a_missing_server_name() {
assert!(UserId::parse("@alice").is_err());
assert!(UserId::parse("@alice:").is_err());
}
#[test]
fn user_id_rejects_an_empty_localpart() {
assert!(UserId::parse("@:example.org").is_err());
}
#[test]
fn room_id_accepts_a_well_formed_sigil() {
let id = RoomId::parse("!abc123:example.org").expect("valid room id");
assert_eq!(id.as_str(), "!abc123:example.org");
}
#[test]
fn room_id_rejects_a_missing_sigil() {
assert!(RoomId::parse("abc123:example.org").is_err());
}
#[test]
fn event_id_accepts_a_well_formed_sigil() {
let id = EventId::parse("$abc123").expect("valid event id");
assert_eq!(id.as_str(), "$abc123");
}
#[test]
fn event_id_rejects_a_missing_sigil() {
assert!(EventId::parse("abc123").is_err());
}
#[test]
fn event_id_rejects_an_empty_opaque_part() {
assert!(EventId::parse("$").is_err());
}
#[test]
fn device_id_rejects_empty_and_whitespace() {
assert!(DeviceId::parse("").is_err());
assert!(DeviceId::parse("has space").is_err());
assert!(DeviceId::parse("ABCDEFGH").is_ok());
}
#[test]
fn txn_id_and_request_id_are_monotonic_and_distinct_namespaces() {
let a = TxnId::new(0);
let b = TxnId::new(1);
assert_ne!(a, b);
assert!(a.as_str() < b.as_str(), "fixed-width hex keeps lexicographic order monotonic");
let r0 = RequestId::next(0);
let r1 = RequestId::next(1);
assert_ne!(r0, r1);
assert_ne!(TxnId::new(0).as_str(), RequestId::next(0).as_str());
}
#[test]
fn request_id_from_string_round_trips_a_previously_minted_value() {
let minted = RequestId::next(7);
let recovered = RequestId::from(minted.as_str().to_string());
assert_eq!(minted, recovered);
}
#[test]
fn as_seed_recovers_the_minting_seed() {
assert_eq!(TxnId::new(42).as_seed(), Some(42));
assert_eq!(RequestId::next(42).as_seed(), Some(42));
assert_eq!(TxnId::from("opaque-server-value".to_string()).as_seed(), None);
assert_eq!(RequestId::from("opaque-value".to_string()).as_seed(), None);
}
#[test]
fn ids_round_trip_through_serde_as_a_bare_string() {
let id = RoomId::parse("!abc:example.org").expect("valid room id");
let json = serde_json::to_string(&id).expect("serialize");
assert_eq!(json, "\"!abc:example.org\"");
let back: RoomId = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, id);
}
}