use derive_more::{Display, IsVariant};
use jiff::Timestamp as JiffTimestamp;
use smol_str::SmolStr;
use crate::domain::{vo::VoiceFingerprint, Uuid7};
#[derive(Debug, Clone, PartialEq)]
pub struct Person<Id = Uuid7> {
id: Id,
name: SmolStr,
voiceprint: Option<VoiceFingerprint<Id>>,
confidence: PersonConfidence,
created_at: JiffTimestamp,
updated_at: JiffTimestamp,
}
impl Person<Uuid7> {
pub fn try_new(
id: Uuid7,
name: impl Into<SmolStr>,
created_at: JiffTimestamp,
updated_at: JiffTimestamp,
) -> Result<Self, PersonError> {
if id.is_nil() {
return Err(PersonError::NilId);
}
Ok(Self {
id,
name: name.into(),
voiceprint: None,
confidence: PersonConfidence::default(),
created_at,
updated_at,
})
}
}
impl<Id> Person<Id> {
#[inline(always)]
#[must_use]
pub fn from_parts(
id: Id,
name: SmolStr,
voiceprint: Option<VoiceFingerprint<Id>>,
confidence: PersonConfidence,
created_at: JiffTimestamp,
updated_at: JiffTimestamp,
) -> Self {
Self {
id,
name,
voiceprint,
confidence,
created_at,
updated_at,
}
}
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub fn name(&self) -> &str {
self.name.as_str()
}
#[inline(always)]
pub const fn voiceprint_ref(&self) -> Option<&VoiceFingerprint<Id>> {
self.voiceprint.as_ref()
}
#[inline(always)]
pub const fn confidence(&self) -> PersonConfidence {
self.confidence
}
#[inline(always)]
pub const fn created_at(&self) -> JiffTimestamp {
self.created_at
}
#[inline(always)]
pub const fn updated_at(&self) -> JiffTimestamp {
self.updated_at
}
#[inline(always)]
#[must_use]
pub fn with_name(mut self, name: impl Into<SmolStr>) -> Self {
self.name = name.into();
self
}
#[inline(always)]
#[must_use]
pub fn with_voiceprint(mut self, voiceprint: VoiceFingerprint<Id>) -> Self {
self.voiceprint = Some(voiceprint);
self
}
#[inline(always)]
#[must_use]
pub fn maybe_voiceprint(mut self, voiceprint: Option<VoiceFingerprint<Id>>) -> Self {
self.voiceprint = voiceprint;
self
}
#[inline(always)]
#[must_use]
pub const fn with_confidence(mut self, confidence: PersonConfidence) -> Self {
self.confidence = confidence;
self
}
#[inline(always)]
#[must_use]
pub const fn with_created_at(mut self, t: JiffTimestamp) -> Self {
self.created_at = t;
self
}
#[inline(always)]
#[must_use]
pub const fn with_updated_at(mut self, t: JiffTimestamp) -> Self {
self.updated_at = t;
self
}
#[inline(always)]
pub fn set_name(&mut self, name: impl Into<SmolStr>) -> &mut Self {
self.name = name.into();
self
}
#[inline(always)]
pub fn set_voiceprint(&mut self, voiceprint: VoiceFingerprint<Id>) -> &mut Self {
self.voiceprint = Some(voiceprint);
self
}
#[inline(always)]
pub fn update_voiceprint(&mut self, voiceprint: Option<VoiceFingerprint<Id>>) -> &mut Self {
self.voiceprint = voiceprint;
self
}
#[inline(always)]
pub fn clear_voiceprint(&mut self) -> &mut Self {
self.voiceprint = None;
self
}
#[inline(always)]
pub const fn set_confidence(&mut self, confidence: PersonConfidence) -> &mut Self {
self.confidence = confidence;
self
}
#[inline(always)]
pub const fn set_created_at(&mut self, t: JiffTimestamp) -> &mut Self {
self.created_at = t;
self
}
#[inline(always)]
pub const fn set_updated_at(&mut self, t: JiffTimestamp) -> &mut Self {
self.updated_at = t;
self
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum PersonConfidence {
#[default]
AutoMatched,
UserConfirmed,
}
impl PersonConfidence {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::AutoMatched => "auto_matched",
Self::UserConfirmed => "user_confirmed",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"auto_matched" => Self::AutoMatched,
"user_confirmed" => Self::UserConfirmed,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum PersonError {
#[error("Person id must not be the nil UUID")]
NilId,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::domain::vo::Provenance;
#[test]
fn person_confidence_slug_roundtrip() {
for v in [
PersonConfidence::AutoMatched,
PersonConfidence::UserConfirmed,
] {
assert_eq!(PersonConfidence::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(PersonConfidence::from_str("not_a_slug"), None);
}
fn ts() -> JiffTimestamp {
JiffTimestamp::from_millisecond(1_700_000_000_000).expect("valid timestamp")
}
fn vfp() -> VoiceFingerprint<Uuid7> {
VoiceFingerprint::try_new(
Uuid7::new(),
192,
ts(),
Some(0.83),
Provenance::from_parts("ecapa-tdnn", "v1.0.0", "", "findit-indexer-0.1.0"),
)
.expect("valid voiceprint")
}
#[test]
fn try_new_happy_path() {
let id = Uuid7::new();
let p = Person::try_new(id, "", ts(), ts()).expect("valid construction must succeed");
assert_eq!(p.id_ref(), &id);
assert!(p.name().is_empty());
assert!(p.voiceprint_ref().is_none());
assert_eq!(p.confidence(), PersonConfidence::AutoMatched);
assert_eq!(p.created_at(), ts());
assert_eq!(p.updated_at(), ts());
}
#[test]
fn try_new_accepts_named_person() {
let p = Person::try_new(Uuid7::new(), "Jane Doe", ts(), ts()).unwrap();
assert_eq!(p.name(), "Jane Doe");
}
#[test]
fn try_new_rejects_nil_id() {
let r = Person::try_new(Uuid7::nil(), "", ts(), ts());
assert_eq!(r.err(), Some(PersonError::NilId));
assert!(PersonError::NilId.is_nil_id());
}
#[test]
fn person_confidence_default_is_auto_matched() {
assert_eq!(PersonConfidence::default(), PersonConfidence::AutoMatched);
assert!(PersonConfidence::AutoMatched.is_auto_matched());
assert!(PersonConfidence::UserConfirmed.is_user_confirmed());
}
#[test]
fn from_parts_round_trips_a_validated_instance() {
let v = vfp();
let original = Person::try_new(Uuid7::new(), "Jane", ts(), ts())
.unwrap()
.with_voiceprint(v.clone())
.with_confidence(PersonConfidence::UserConfirmed);
let rebuilt: Person<Uuid7> = Person::from_parts(
*original.id_ref(),
SmolStr::new(original.name()),
original.voiceprint_ref().cloned(),
original.confidence(),
original.created_at(),
original.updated_at(),
);
assert_eq!(rebuilt, original);
}
#[test]
fn voiceprint_full_option_mutator_vocabulary() {
let v = vfp();
let p = Person::try_new(Uuid7::new(), "", ts(), ts())
.unwrap()
.with_voiceprint(v.clone());
assert!(p.voiceprint_ref().is_some());
let p = p.maybe_voiceprint(None);
assert!(p.voiceprint_ref().is_none());
let p = p.maybe_voiceprint(Some(v.clone()));
assert_eq!(p.voiceprint_ref(), Some(&v));
let mut p = p;
p.clear_voiceprint();
assert!(p.voiceprint_ref().is_none());
p.set_voiceprint(v.clone());
assert_eq!(p.voiceprint_ref(), Some(&v));
p.update_voiceprint(None);
assert!(p.voiceprint_ref().is_none());
p.update_voiceprint(Some(v.clone()));
assert_eq!(p.voiceprint_ref(), Some(&v));
}
#[test]
fn confidence_builder_and_setter() {
let p = Person::try_new(Uuid7::new(), "", ts(), ts())
.unwrap()
.with_confidence(PersonConfidence::UserConfirmed);
assert_eq!(p.confidence(), PersonConfidence::UserConfirmed);
let mut p = p;
p.set_confidence(PersonConfidence::AutoMatched);
assert_eq!(p.confidence(), PersonConfidence::AutoMatched);
}
#[test]
fn timestamp_builders_and_setters() {
let later = JiffTimestamp::from_millisecond(1_800_000_000_000).expect("valid timestamp");
let p = Person::try_new(Uuid7::new(), "", ts(), ts())
.unwrap()
.with_created_at(later)
.with_updated_at(later);
assert_eq!(p.created_at(), later);
assert_eq!(p.updated_at(), later);
let mut p = p;
p.set_created_at(ts());
p.set_updated_at(ts());
assert_eq!(p.created_at(), ts());
assert_eq!(p.updated_at(), ts());
}
#[test]
fn name_builder_and_setter() {
let p = Person::try_new(Uuid7::new(), "old", ts(), ts())
.unwrap()
.with_name("new");
assert_eq!(p.name(), "new");
let mut p = p;
p.set_name("");
assert!(p.name().is_empty());
}
}