use crate::common::Timestamp;
#[cfg(feature = "uuid")]
use crate::org_id::OrgId;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::borrow::Cow;
#[cfg(all(not(feature = "std"), feature = "alloc", feature = "uuid"))]
use alloc::borrow::ToOwned;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::string::String;
#[cfg(all(not(feature = "std"), feature = "alloc", feature = "uuid"))]
use alloc::string::ToString;
#[cfg(all(not(feature = "std"), feature = "alloc", feature = "uuid"))]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::borrow::Cow;
#[cfg(feature = "uuid")]
use uuid::Uuid;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(transparent))]
pub struct PrincipalId(Cow<'static, str>);
impl PrincipalId {
#[must_use]
pub const fn static_str(s: &'static str) -> Self {
Self(Cow::Borrowed(s))
}
#[must_use]
pub fn from_owned(s: String) -> Self {
Self(Cow::Owned(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn from_uuid(uuid: Uuid) -> Self {
Self(Cow::Owned(uuid.to_string()))
}
}
impl core::fmt::Debug for PrincipalId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("PrincipalId").field(&self.as_str()).finish()
}
}
impl core::fmt::Display for PrincipalId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum PrincipalKind {
User,
Service,
System,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrincipalParseError {
pub input: String,
}
impl core::fmt::Display for PrincipalParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"invalid Principal: expected a UUID string, got {:?}",
self.input
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for PrincipalParseError {}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Principal {
pub id: PrincipalId,
pub kind: PrincipalKind,
#[cfg(feature = "uuid")]
#[cfg_attr(feature = "serde", serde(default))]
pub org_path: Vec<OrgId>,
}
impl Principal {
#[cfg(feature = "uuid")]
#[must_use]
pub fn human(uuid: Uuid) -> Self {
Self {
id: PrincipalId::from_uuid(uuid),
kind: PrincipalKind::User,
#[cfg(feature = "uuid")]
org_path: Vec::new(),
}
}
#[cfg(feature = "uuid")]
pub fn try_parse(s: &str) -> Result<Self, PrincipalParseError> {
Uuid::parse_str(s)
.map(Self::human)
.map_err(|_| PrincipalParseError {
input: s.to_owned(),
})
}
#[must_use]
pub fn system(id: &'static str) -> Self {
Self {
id: PrincipalId::static_str(id),
kind: PrincipalKind::System,
#[cfg(feature = "uuid")]
org_path: Vec::new(),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
self.id.as_str()
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn with_org_path(mut self, org_path: Vec<OrgId>) -> Self {
self.org_path = org_path;
self
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn org_path_display(&self) -> String {
self.org_path
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
}
}
impl core::fmt::Display for Principal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(all(feature = "arbitrary", feature = "uuid"))]
impl<'a> arbitrary::Arbitrary<'a> for Principal {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
Ok(Self::human(Uuid::from_bytes(bytes)))
}
}
#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
impl<'a> arbitrary::Arbitrary<'a> for Principal {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
Ok(Self {
id: PrincipalId::from_owned(s),
kind: PrincipalKind::System,
#[cfg(feature = "uuid")]
org_path: Vec::new(),
})
}
}
#[cfg(all(feature = "arbitrary", feature = "uuid"))]
impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
Ok(Self::from_uuid(Uuid::from_bytes(bytes)))
}
}
#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
Ok(Self::from_owned(s))
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for PrincipalKind {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
match <u8 as arbitrary::Arbitrary>::arbitrary(u)? % 3 {
0 => Ok(Self::User),
1 => Ok(Self::Service),
_ => Ok(Self::System),
}
}
}
#[cfg(all(feature = "proptest", feature = "uuid"))]
impl proptest::arbitrary::Arbitrary for Principal {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::prelude::*;
any::<[u8; 16]>()
.prop_map(|b| Self::human(Uuid::from_bytes(b)))
.boxed()
}
}
#[cfg(all(feature = "proptest", not(feature = "uuid")))]
impl proptest::arbitrary::Arbitrary for Principal {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::prelude::*;
any::<String>()
.prop_map(|s| Self {
id: PrincipalId::from_owned(s),
kind: PrincipalKind::System,
#[cfg(feature = "uuid")]
org_path: Vec::new(),
})
.boxed()
}
}
#[cfg(all(feature = "proptest", feature = "uuid"))]
impl proptest::arbitrary::Arbitrary for PrincipalId {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::prelude::*;
any::<[u8; 16]>()
.prop_map(|b| Self::from_uuid(Uuid::from_bytes(b)))
.boxed()
}
}
#[cfg(all(feature = "proptest", not(feature = "uuid")))]
impl proptest::arbitrary::Arbitrary for PrincipalId {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::prelude::*;
any::<String>().prop_map(|s| Self::from_owned(s)).boxed()
}
}
#[cfg(feature = "proptest")]
impl proptest::arbitrary::Arbitrary for PrincipalKind {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::prelude::*;
prop_oneof![Just(Self::User), Just(Self::Service), Just(Self::System),].boxed()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(
all(feature = "arbitrary", not(feature = "chrono")),
derive(arbitrary::Arbitrary)
)]
#[cfg_attr(
all(feature = "proptest", not(feature = "chrono")),
derive(proptest_derive::Arbitrary)
)]
pub struct AuditInfo {
#[cfg_attr(
feature = "utoipa",
schema(value_type = String, format = DateTime)
)]
pub created_at: Timestamp,
#[cfg_attr(
feature = "utoipa",
schema(value_type = String, format = DateTime)
)]
pub updated_at: Timestamp,
pub created_by: Principal,
pub updated_by: Principal,
}
impl AuditInfo {
#[must_use]
pub fn new(
created_at: Timestamp,
updated_at: Timestamp,
created_by: Principal,
updated_by: Principal,
) -> Self {
Self {
created_at,
updated_at,
created_by,
updated_by,
}
}
#[cfg(feature = "chrono")]
#[must_use]
pub fn now(created_by: Principal) -> Self {
let now = chrono::Utc::now();
let updated_by = created_by.clone();
Self {
created_at: now,
updated_at: now,
created_by,
updated_by,
}
}
#[cfg(feature = "chrono")]
pub fn touch(&mut self, updated_by: Principal) {
self.updated_at = chrono::Utc::now();
self.updated_by = updated_by;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ResolvedPrincipal {
pub id: Principal,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<String>,
}
impl ResolvedPrincipal {
#[must_use]
pub fn new(id: Principal, display_name: Option<String>) -> Self {
Self { id, display_name }
}
#[must_use]
pub fn display(&self) -> &str {
self.display_name
.as_deref()
.unwrap_or_else(|| self.id.as_str())
}
}
impl From<Principal> for ResolvedPrincipal {
fn from(id: Principal) -> Self {
Self {
id,
display_name: None,
}
}
}
#[cfg(all(feature = "arbitrary", feature = "chrono"))]
impl<'a> arbitrary::Arbitrary<'a> for AuditInfo {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let created_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
let updated_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
let created_at = chrono::DateTime::from_timestamp(created_secs.abs(), 0)
.unwrap_or_else(chrono::Utc::now);
let updated_at = chrono::DateTime::from_timestamp(updated_secs.abs(), 0)
.unwrap_or_else(chrono::Utc::now);
Ok(Self {
created_at,
updated_at,
created_by: Principal::arbitrary(u)?,
updated_by: Principal::arbitrary(u)?,
})
}
}
#[cfg(all(feature = "proptest", feature = "chrono"))]
impl proptest::arbitrary::Arbitrary for AuditInfo {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::prelude::*;
(
0i64..=32_503_680_000i64,
0i64..=32_503_680_000i64,
any::<Principal>(),
any::<Principal>(),
)
.prop_map(|(cs, us, cb, ub)| Self {
created_at: chrono::DateTime::from_timestamp(cs, 0)
.unwrap_or_else(chrono::Utc::now),
updated_at: chrono::DateTime::from_timestamp(us, 0)
.unwrap_or_else(chrono::Utc::now),
created_by: cb,
updated_by: ub,
})
.boxed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "uuid")]
use uuid::Uuid;
#[test]
fn principal_id_static_str() {
let id = PrincipalId::static_str("foo");
assert_eq!(id.as_str(), "foo");
}
#[test]
fn principal_id_from_owned() {
let id = PrincipalId::from_owned("bar".to_owned());
assert_eq!(id.as_str(), "bar");
}
#[cfg(feature = "uuid")]
#[test]
fn principal_id_from_uuid() {
let id = PrincipalId::from_uuid(Uuid::nil());
assert_eq!(id.as_str(), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn principal_id_display() {
let id = PrincipalId::static_str("test");
assert_eq!(format!("{id}"), "test");
}
#[cfg(feature = "serde")]
#[test]
fn principal_id_serde_transparent() {
let id = PrincipalId::static_str("myid");
let json = serde_json::to_value(&id).unwrap();
assert_eq!(json, serde_json::json!("myid"));
let back: PrincipalId = serde_json::from_value(json).unwrap();
assert_eq!(back, id);
}
#[test]
fn principal_kind_copy_and_eq() {
let k1 = PrincipalKind::User;
let k2 = k1;
assert_eq!(k1, k2);
}
#[test]
fn principal_kind_all_variants() {
let _ = PrincipalKind::User;
let _ = PrincipalKind::Service;
let _ = PrincipalKind::System;
}
#[cfg(feature = "uuid")]
#[test]
fn principal_human_has_user_kind() {
let p = Principal::human(Uuid::nil());
assert_eq!(p.kind, PrincipalKind::User);
}
#[cfg(feature = "uuid")]
#[test]
fn principal_human_has_empty_org_path() {
let p = Principal::human(Uuid::nil());
assert!(p.org_path.is_empty());
}
#[test]
fn principal_system_has_system_kind() {
let p = Principal::system("s");
assert_eq!(p.kind, PrincipalKind::System);
}
#[cfg(feature = "uuid")]
#[test]
fn principal_system_has_empty_org_path() {
let p = Principal::system("s");
assert!(p.org_path.is_empty());
}
#[cfg(feature = "uuid")]
#[test]
fn principal_with_org_path_builder() {
let org_id = crate::org_id::OrgId::generate();
let p = Principal::system("test").with_org_path(vec![org_id]);
assert_eq!(p.org_path.len(), 1);
assert_eq!(p.org_path[0], org_id);
}
#[cfg(feature = "uuid")]
#[test]
fn org_path_display_empty_for_system_principal() {
let p = Principal::system("svc");
assert_eq!(p.org_path_display(), "");
}
#[cfg(feature = "uuid")]
#[test]
fn org_path_display_single_org() {
let org_id = crate::org_id::OrgId::generate();
let p = Principal::system("svc").with_org_path(vec![org_id]);
assert_eq!(p.org_path_display(), org_id.to_string());
}
#[cfg(feature = "uuid")]
#[test]
fn org_path_display_multiple_orgs_comma_separated() {
let root = crate::org_id::OrgId::generate();
let child = crate::org_id::OrgId::generate();
let p = Principal::system("svc").with_org_path(vec![root, child]);
assert_eq!(p.org_path_display(), format!("{root},{child}"));
}
#[cfg(feature = "uuid")]
#[test]
fn principal_try_parse_accepts_valid_uuid() {
let s = "550e8400-e29b-41d4-a716-446655440000";
let p = Principal::try_parse(s).expect("valid UUID should parse");
assert_eq!(p.as_str(), s);
}
#[cfg(feature = "uuid")]
#[test]
fn principal_try_parse_sets_user_kind() {
let p = Principal::try_parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(p.kind, PrincipalKind::User);
}
#[cfg(feature = "uuid")]
#[test]
fn principal_try_parse_rejects_email_string() {
let err = Principal::try_parse("alice@example.com").expect_err("email must be rejected");
assert_eq!(err.input, "alice@example.com");
assert!(err.to_string().contains("alice@example.com"));
}
#[cfg(feature = "uuid")]
#[test]
fn principal_try_parse_rejects_empty_string() {
let err = Principal::try_parse("").expect_err("empty string must be rejected");
assert_eq!(err.input, "");
}
#[test]
fn principal_as_str_returns_id_str() {
let p = Principal::system("x");
assert_eq!(p.as_str(), "x");
}
#[cfg(feature = "uuid")]
#[test]
fn principal_display_forwards_to_as_str() {
let p = Principal::human(Uuid::nil());
let s = format!("{p}");
assert_eq!(s, Uuid::nil().to_string());
}
#[cfg(feature = "uuid")]
#[test]
fn principal_debug_is_not_redacted() {
let p = Principal::human(Uuid::nil());
let s = format!("{p:?}");
assert!(
s.contains(&Uuid::nil().to_string()),
"debug must not redact: {s}"
);
assert!(s.contains("Principal"), "debug must name the type: {s}");
}
#[test]
fn principal_equality_and_hash_across_owned_and_borrowed() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let p1 = Principal::system("orders.bootstrap");
let p2 = Principal::system("orders.bootstrap");
assert_eq!(p1, p2);
let mut h1 = DefaultHasher::new();
p1.hash(&mut h1);
let mut h2 = DefaultHasher::new();
p2.hash(&mut h2);
assert_eq!(h1.finish(), h2.finish());
}
#[cfg(feature = "uuid")]
#[test]
fn principal_clone_roundtrip() {
let p = Principal::human(Uuid::nil());
let q = p.clone();
assert_eq!(p, q);
}
#[cfg(all(feature = "serde", feature = "uuid"))]
#[test]
fn principal_serde_struct_roundtrip_human() {
let p = Principal::human(Uuid::nil());
let json = serde_json::to_value(&p).unwrap();
let back: Principal = serde_json::from_value(json).unwrap();
assert_eq!(back, p);
}
#[cfg(feature = "serde")]
#[test]
fn principal_serde_struct_roundtrip_system() {
let p = Principal::system("billing.rotation-engine");
let json = serde_json::to_value(&p).unwrap();
let back: Principal = serde_json::from_value(json).unwrap();
assert_eq!(back, p);
}
#[cfg(all(feature = "serde", feature = "uuid"))]
#[test]
fn principal_serde_includes_org_path() {
let p = Principal::system("test");
let json = serde_json::to_value(&p).unwrap();
assert!(json.get("org_path").is_some());
}
#[cfg(all(feature = "chrono", feature = "uuid"))]
#[test]
fn now_sets_created_at_and_updated_at() {
let actor = Principal::human(Uuid::nil());
let before = chrono::Utc::now();
let info = AuditInfo::now(actor.clone());
let after = chrono::Utc::now();
assert!(info.created_at >= before && info.created_at <= after);
assert!(info.updated_at >= before && info.updated_at <= after);
assert_eq!(info.created_by, actor);
assert_eq!(info.updated_by, actor);
}
#[cfg(all(feature = "chrono", feature = "serde"))]
#[test]
fn now_with_system_principal() {
let info = AuditInfo::now(Principal::system("billing.rotation-engine"));
let json = serde_json::to_value(&info).unwrap();
let back: AuditInfo = serde_json::from_value(json).unwrap();
assert_eq!(back, info);
}
#[cfg(all(feature = "chrono", feature = "uuid"))]
#[test]
fn touch_updates_updated_at_and_updated_by() {
let mut info = AuditInfo::now(Principal::human(Uuid::nil()));
let engine = Principal::system("billing.rotation-engine");
let before_touch = chrono::Utc::now();
info.touch(engine.clone());
let after_touch = chrono::Utc::now();
assert!(info.updated_at >= before_touch && info.updated_at <= after_touch);
assert_eq!(info.updated_by, engine);
}
#[cfg(all(feature = "chrono", feature = "uuid"))]
#[test]
fn new_constructor() {
let now = chrono::Utc::now();
let actor = Principal::human(Uuid::nil());
let engine = Principal::system("billing.rotation-engine");
let info = AuditInfo::new(now, now, actor.clone(), engine.clone());
assert_eq!(info.created_at, now);
assert_eq!(info.updated_at, now);
assert_eq!(info.created_by, actor);
assert_eq!(info.updated_by, engine);
}
#[cfg(all(feature = "chrono", feature = "serde"))]
#[test]
fn serde_round_trip_with_system_actor() {
let info = AuditInfo::now(Principal::system("orders.bootstrap"));
let json = serde_json::to_value(&info).unwrap();
let back: AuditInfo = serde_json::from_value(json).unwrap();
assert_eq!(back, info);
}
#[cfg(all(feature = "chrono", feature = "serde"))]
#[test]
fn serde_actor_fields_are_always_present() {
let info = AuditInfo::now(Principal::system("orders.bootstrap"));
let json = serde_json::to_value(&info).unwrap();
assert!(
json.get("created_by").is_some(),
"created_by must always serialize"
);
assert!(
json.get("updated_by").is_some(),
"updated_by must always serialize"
);
assert_eq!(
json["created_by"]["id"],
serde_json::json!("orders.bootstrap")
);
}
#[test]
fn principal_parse_error_display_contains_input() {
let err = PrincipalParseError {
input: "bad-value".to_owned(),
};
assert!(err.to_string().contains("bad-value"));
}
#[cfg(feature = "std")]
#[test]
fn principal_parse_error_is_std_error() {
let err = PrincipalParseError {
input: "x".to_owned(),
};
let _: &dyn std::error::Error = &err;
}
#[cfg(feature = "uuid")]
#[test]
fn resolved_principal_new_and_display_with_name() {
let p = Principal::human(Uuid::nil());
let r = ResolvedPrincipal::new(p, Some("Alice Martin".to_owned()));
assert_eq!(r.display(), "Alice Martin");
}
#[cfg(feature = "uuid")]
#[test]
fn resolved_principal_display_falls_back_to_uuid() {
let p = Principal::human(Uuid::nil());
let r = ResolvedPrincipal::new(p.clone(), None);
assert_eq!(r.display(), p.as_str());
}
#[cfg(feature = "uuid")]
#[test]
fn resolved_principal_from_principal() {
let p = Principal::human(Uuid::nil());
let r = ResolvedPrincipal::from(p.clone());
assert_eq!(r.id, p);
assert!(r.display_name.is_none());
}
#[cfg(all(feature = "uuid", feature = "serde"))]
#[test]
fn resolved_principal_serde_omits_none_display_name() {
let p = Principal::human(Uuid::nil());
let r = ResolvedPrincipal::from(p);
let json = serde_json::to_value(&r).unwrap();
assert!(
json.get("display_name").is_none(),
"display_name must be absent when None"
);
}
#[cfg(all(feature = "uuid", feature = "serde"))]
#[test]
fn resolved_principal_serde_includes_display_name_when_set() {
let p = Principal::human(Uuid::nil());
let r = ResolvedPrincipal::new(p, Some("Bob".to_owned()));
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json["display_name"], serde_json::json!("Bob"));
}
}