use derive_more::IsVariant;
use crate::domain::{vo::IndexProgress, Uuid7};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Audio<Id = Uuid7> {
id: Id,
media_id: Id,
tracks: std::vec::Vec<Id>,
total_segments: u32,
track_progress: IndexProgress,
}
impl Audio<Uuid7> {
pub fn try_new(id: Uuid7, media_id: Uuid7) -> Result<Self, AudioError> {
if id.is_nil() {
return Err(AudioError::NilId);
}
if media_id.is_nil() {
return Err(AudioError::NilMediaId);
}
Ok(Self {
id,
media_id,
tracks: std::vec::Vec::new(),
total_segments: 0,
track_progress: IndexProgress::new(),
})
}
}
impl<Id> Audio<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn media_id_ref(&self) -> &Id {
&self.media_id
}
#[inline(always)]
pub const fn tracks_slice(&self) -> &[Id] {
self.tracks.as_slice()
}
#[inline(always)]
pub const fn total_segments(&self) -> u32 {
self.total_segments
}
#[inline(always)]
pub const fn track_progress_ref(&self) -> &IndexProgress {
&self.track_progress
}
#[inline(always)]
#[must_use]
pub fn with_tracks(mut self, tracks: impl Into<std::vec::Vec<Id>>) -> Self {
self.tracks = tracks.into();
self
}
#[inline(always)]
#[must_use]
pub const fn with_total_segments(mut self, total: u32) -> Self {
self.total_segments = total;
self
}
#[inline(always)]
#[must_use]
pub const fn with_track_progress(mut self, p: IndexProgress) -> Self {
self.track_progress = p;
self
}
#[inline(always)]
pub fn set_tracks(&mut self, tracks: impl Into<std::vec::Vec<Id>>) -> &mut Self {
self.tracks = tracks.into();
self
}
#[inline(always)]
pub const fn set_total_segments(&mut self, total: u32) -> &mut Self {
self.total_segments = total;
self
}
#[inline(always)]
pub const fn set_track_progress(&mut self, p: IndexProgress) -> &mut Self {
self.track_progress = p;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum AudioError {
#[error("Audio id must not be the nil UUID")]
NilId,
#[error("Audio `media_id` (FK → Media) must not be the nil UUID")]
NilMediaId,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::domain::vo::IndexProgressError;
#[test]
fn try_new_happy_path() {
let id = Uuid7::new();
let media_id = Uuid7::new();
let a = Audio::try_new(id, media_id).expect("valid construction must succeed");
assert_eq!(a.id_ref(), &id);
assert_eq!(a.media_id_ref(), &media_id);
assert!(a.tracks_slice().is_empty());
assert_eq!(a.total_segments(), 0);
assert_eq!(a.track_progress_ref(), &IndexProgress::new());
}
#[test]
fn try_new_rejects_nil_id() {
let r = Audio::try_new(Uuid7::nil(), Uuid7::new());
assert_eq!(r.err(), Some(AudioError::NilId));
assert!(AudioError::NilId.is_nil_id());
}
#[test]
fn try_new_rejects_nil_media_id() {
let r = Audio::try_new(Uuid7::new(), Uuid7::nil());
assert_eq!(r.err(), Some(AudioError::NilMediaId));
assert!(AudioError::NilMediaId.is_nil_media_id());
}
#[test]
fn media_id_ref_returns_constructed_media_id() {
let media_id = Uuid7::new();
let a = Audio::try_new(Uuid7::new(), media_id).unwrap();
assert_eq!(a.media_id_ref(), &media_id);
}
#[test]
fn builders_chain_tracks_and_rollup() {
let t1 = Uuid7::new();
let t2 = Uuid7::new();
let p = IndexProgress::try_new(2, 1, 0).unwrap();
let a = Audio::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_tracks(std::vec![t1, t2])
.with_total_segments(42)
.with_track_progress(p);
assert_eq!(a.tracks_slice().len(), 2);
assert!(a.tracks_slice().contains(&t1));
assert_eq!(a.total_segments(), 42);
assert_eq!(a.track_progress_ref(), &p);
}
#[test]
fn setters_mutate_in_place() {
let mut a = Audio::try_new(Uuid7::new(), Uuid7::new()).unwrap();
a.set_tracks(std::vec![Uuid7::new()]);
a.set_total_segments(7);
a.set_track_progress(IndexProgress::try_new(1, 1, 0).unwrap());
assert_eq!(a.tracks_slice().len(), 1);
assert_eq!(a.total_segments(), 7);
assert_eq!(a.track_progress_ref().total(), 1);
assert!(!a.track_progress_ref().has_failures());
}
#[test]
fn fields_are_independent_across_mutators() {
let mut a = Audio::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_tracks(std::vec![Uuid7::new(), Uuid7::new()])
.with_total_segments(17)
.with_track_progress(IndexProgress::try_new(2, 1, 1).unwrap());
a.set_tracks(std::vec![Uuid7::new()]);
assert_eq!(a.tracks_slice().len(), 1);
assert_eq!(a.total_segments(), 17);
assert_eq!(
a.track_progress_ref(),
&IndexProgress::try_new(2, 1, 1).unwrap()
);
}
#[test]
fn index_progress_invariant_rejects_overcount() {
let r = IndexProgress::try_new(2, 2, 1);
assert_eq!(r.err(), Some(IndexProgressError::SumExceedsTotal));
assert!(IndexProgressError::SumExceedsTotal.is_sum_exceeds_total());
}
#[test]
fn index_progress_invariant_rejects_overflow() {
let r = IndexProgress::try_new(u32::MAX, u32::MAX, 1);
assert_eq!(r.err(), Some(IndexProgressError::SumOverflows));
assert!(IndexProgressError::SumOverflows.is_sum_overflows());
}
#[test]
fn index_progress_has_failures() {
let none = IndexProgress::try_new(5, 5, 0).unwrap();
let some = IndexProgress::try_new(5, 3, 2).unwrap();
assert!(!none.has_failures());
assert!(some.has_failures());
}
}