use derive_more::IsVariant;
use crate::domain::{vo::IndexProgress, Uuid7};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Video<Id = Uuid7> {
id: Id,
media_id: Id,
total_scenes: u32,
tracks: std::vec::Vec<Id>,
track_progress: IndexProgress,
}
impl Video<Uuid7> {
pub fn try_new(id: Uuid7, media_id: Uuid7) -> Result<Self, VideoError> {
if id.is_nil() {
return Err(VideoError::NilId);
}
if media_id.is_nil() {
return Err(VideoError::NilMediaId);
}
Ok(Self {
id,
media_id,
total_scenes: 0,
tracks: std::vec::Vec::new(),
track_progress: IndexProgress::new(),
})
}
}
impl<Id> Video<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 total_scenes(&self) -> u32 {
self.total_scenes
}
#[inline(always)]
pub const fn tracks_slice(&self) -> &[Id] {
self.tracks.as_slice()
}
#[inline(always)]
pub const fn track_progress_ref(&self) -> &IndexProgress {
&self.track_progress
}
#[must_use]
#[inline(always)]
pub fn with_tracks(mut self, tracks: impl Into<std::vec::Vec<Id>>) -> Self {
self.tracks = tracks.into();
self
}
#[must_use]
#[inline(always)]
pub const fn with_total_scenes(mut self, n: u32) -> Self {
self.total_scenes = n;
self
}
#[must_use]
#[inline(always)]
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_scenes(&mut self, n: u32) -> &mut Self {
self.total_scenes = n;
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 VideoError {
#[error("Video facet id must not be the nil UUID")]
NilId,
#[error("Video `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 v = Video::try_new(id, media_id).unwrap();
assert_eq!(v.id_ref(), &id);
assert_eq!(v.media_id_ref(), &media_id);
assert_eq!(v.total_scenes(), 0);
assert!(v.tracks_slice().is_empty());
assert_eq!(v.track_progress_ref(), &IndexProgress::new());
}
#[test]
fn try_new_rejects_nil_id() {
let r = Video::try_new(Uuid7::nil(), Uuid7::new());
assert_eq!(r.err(), Some(VideoError::NilId));
assert!(VideoError::NilId.is_nil_id());
}
#[test]
fn try_new_rejects_nil_media_id() {
let r = Video::try_new(Uuid7::new(), Uuid7::nil());
assert_eq!(r.err(), Some(VideoError::NilMediaId));
assert!(VideoError::NilMediaId.is_nil_media_id());
}
#[test]
fn media_id_ref_returns_constructed_media_id() {
let media_id = Uuid7::new();
let v = Video::try_new(Uuid7::new(), media_id).unwrap();
assert_eq!(v.media_id_ref(), &media_id);
}
#[test]
fn builders_and_setters_chain() {
let t1 = Uuid7::new();
let t2 = Uuid7::new();
let p = IndexProgress::try_new(2, 1, 0).unwrap();
let v = Video::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_tracks(std::vec![t1, t2])
.with_total_scenes(7)
.with_track_progress(p);
assert_eq!(v.total_scenes(), 7);
assert_eq!(v.tracks_slice().len(), 2);
assert!(v.tracks_slice().contains(&t1));
assert_eq!(v.track_progress_ref(), &p);
let mut v = v;
v.set_total_scenes(0);
v.set_tracks(std::vec::Vec::<Uuid7>::new());
v.set_track_progress(IndexProgress::new());
assert_eq!(v.total_scenes(), 0);
assert!(v.tracks_slice().is_empty());
assert_eq!(v.track_progress_ref(), &IndexProgress::new());
}
#[test]
fn fields_are_independent_across_mutators() {
let mut v = Video::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_tracks(std::vec![Uuid7::new(), Uuid7::new()])
.with_total_scenes(7)
.with_track_progress(IndexProgress::try_new(2, 1, 1).unwrap());
v.set_tracks(std::vec![Uuid7::new()]);
assert_eq!(v.tracks_slice().len(), 1);
assert_eq!(v.total_scenes(), 7);
assert_eq!(
v.track_progress_ref(),
&IndexProgress::try_new(2, 1, 1).unwrap()
);
let v2 = Video::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_total_scenes(3);
assert_eq!(v2.total_scenes(), 3);
assert!(v2.tracks_slice().is_empty());
}
#[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());
}
}