//! Identifier newtypes shared by both consumers. //! //! Both apps key users and rooms by UUID (Multithreaded's `users.mnw_account_id` //! and `communities.id`; the MNW server's UUID domain entities), so the crate can //! name those concretely without constraining either schema. //! //! Messages are the exception. `MessageId` is a monotonic `i64` rather than a //! UUID because the reconnect cursor (`?after=`) needs a total order, and //! because chat messages are log-shaped, which is the case the MNW server already //! spells `BIGINT GENERATED ALWAYS AS IDENTITY`. use serde::{Deserialize, Serialize}; use uuid::Uuid; macro_rules! uuid_id { ($name:ident, $doc:literal) => { #[doc = $doc] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct $name(pub Uuid); impl From for $name { fn from(id: Uuid) -> Self { Self(id) } } impl From<$name> for Uuid { fn from(id: $name) -> Self { id.0 } } impl std::fmt::Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } }; } uuid_id!( RoomId, "A chat room. One per community in Multithreaded; one per stream in MNW." ); uuid_id!( UserId, "A chat participant, in the host app's user namespace." ); /// A single message, ordered. Doubles as the reconnect cursor. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct MessageId(pub i64); impl std::fmt::Display for MessageId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } /// Client-generated send nonce, echoed back so an optimistically rendered /// message reconciles instead of appearing twice. /// /// Client-supplied and therefore untrusted: it is opaque to the server, capped /// in length, and only ever compared for equality. It never reaches a query. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Nonce(pub String); impl Nonce { /// Longest accepted nonce. A client only needs enough to disambiguate its own /// in-flight sends; anything longer is someone probing. pub const MAX_LEN: usize = 64; /// Returns `None` if the nonce is empty or over [`Nonce::MAX_LEN`]. pub fn parse(raw: impl Into) -> Option { let raw = raw.into(); (!raw.is_empty() && raw.len() <= Self::MAX_LEN).then_some(Self(raw)) } } #[cfg(test)] mod tests { use super::*; #[test] fn nonce_rejects_empty_and_overlong() { assert!(Nonce::parse("").is_none()); assert!(Nonce::parse("a").is_some()); assert!(Nonce::parse("x".repeat(Nonce::MAX_LEN)).is_some()); assert!(Nonce::parse("x".repeat(Nonce::MAX_LEN + 1)).is_none()); } #[test] fn message_ids_order_by_value() { assert!(MessageId(1) < MessageId(2)); } }