diff --git a/crates/domain/src/id.rs b/crates/domain/src/id.rs new file mode 100644 index 0000000..382c249 --- /dev/null +++ b/crates/domain/src/id.rs @@ -0,0 +1,66 @@ +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Identifier for an organization (tenant). +/// +/// A newtype over [`Uuid`] so it can never be confused with another entity's id. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct OrgId(Uuid); + +impl OrgId { + /// Generate a fresh random id. + pub fn new() -> Self { + Self(Uuid::new_v4()) + } + + /// Wrap an existing [`Uuid`]. + pub fn from_uuid(uuid: Uuid) -> Self { + Self(uuid) + } + + /// The underlying [`Uuid`]. + pub fn as_uuid(&self) -> Uuid { + self.0 + } +} + +impl Default for OrgId { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for OrgId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for OrgId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(Uuid::parse_str(s)?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_and_displays_round_trip() { + let text = "550e8400-e29b-41d4-a716-446655440000"; + let id: OrgId = text.parse().expect("valid uuid should parse"); + assert_eq!(id.to_string(), text); + } + + #[test] + fn rejects_invalid_uuid() { + assert!("not-a-uuid".parse::().is_err()); + } +} diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 9c37b95..5972e72 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -1 +1,5 @@ //! Core domain types and invariants. No I/O dependencies. + +mod id; + +pub use id::OrgId;