use derive_more::IsVariant;
use jiff::Timestamp;
use smol_str::SmolStr;
use crate::domain::{Rgba, Uuid7};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserTag<Id = Uuid7> {
id: Id,
name: SmolStr,
color: Option<Rgba>,
created_at: Timestamp,
}
impl UserTag<Uuid7> {
pub fn try_new(
id: Uuid7,
name: impl Into<SmolStr>,
created_at: Timestamp,
) -> Result<Self, NilIdError> {
if id.is_nil() {
return Err(NilIdError::Nil);
}
Ok(Self {
id,
name: name.into(),
color: None,
created_at,
})
}
}
impl<Id> UserTag<Id> {
#[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 color(&self) -> Option<Rgba> {
self.color
}
#[inline(always)]
pub const fn created_at_ref(&self) -> &Timestamp {
&self.created_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 const fn with_color(mut self, color: Option<Rgba>) -> Self {
self.color = color;
self
}
#[inline(always)]
pub fn set_name(&mut self, name: impl Into<SmolStr>) -> &mut Self {
self.name = name.into();
self
}
#[inline(always)]
pub const fn set_color(&mut self, color: Option<Rgba>) -> &mut Self {
self.color = color;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum NilIdError {
#[error("id must not be the nil UUID")]
Nil,
}
impl Default for NilIdError {
#[inline]
fn default() -> Self {
Self::Nil
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SceneAnnotation<Id = Uuid7> {
id: Id,
scene_id: Id,
favorite: bool,
user_tags: std::vec::Vec<Id>,
rating: Option<u8>,
note: SmolStr,
updated_at: Timestamp,
}
impl SceneAnnotation<Uuid7> {
pub fn try_new(
id: Uuid7,
scene_id: Uuid7,
updated_at: Timestamp,
) -> Result<Self, SceneAnnotationError> {
if id.is_nil() {
return Err(SceneAnnotationError::NilId);
}
if scene_id.is_nil() {
return Err(SceneAnnotationError::NilSceneId);
}
Ok(Self {
id,
scene_id,
favorite: false,
user_tags: std::vec::Vec::new(),
rating: None,
note: SmolStr::default(),
updated_at,
})
}
}
impl<Id> SceneAnnotation<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn scene_id_ref(&self) -> &Id {
&self.scene_id
}
#[inline(always)]
pub const fn is_favorite(&self) -> bool {
self.favorite
}
#[inline(always)]
pub const fn user_tags_slice(&self) -> &[Id] {
self.user_tags.as_slice()
}
#[inline(always)]
pub const fn rating(&self) -> Option<u8> {
self.rating
}
#[inline(always)]
pub fn note(&self) -> &str {
self.note.as_str()
}
#[inline(always)]
pub const fn updated_at_ref(&self) -> &Timestamp {
&self.updated_at
}
#[inline(always)]
#[must_use]
pub const fn with_favorite(mut self, favorite: bool) -> Self {
self.favorite = favorite;
self
}
#[inline(always)]
#[must_use]
pub fn with_user_tags(mut self, tags: impl Into<std::vec::Vec<Id>>) -> Self {
self.user_tags = tags.into();
self
}
#[inline(always)]
#[must_use]
pub const fn with_rating(mut self, rating: Option<u8>) -> Self {
self.rating = rating;
self
}
#[inline(always)]
#[must_use]
pub fn with_note(mut self, note: impl Into<SmolStr>) -> Self {
self.note = note.into();
self
}
#[inline(always)]
pub const fn set_favorite(&mut self, favorite: bool) -> &mut Self {
self.favorite = favorite;
self
}
#[inline(always)]
pub fn set_user_tags(&mut self, tags: impl Into<std::vec::Vec<Id>>) -> &mut Self {
self.user_tags = tags.into();
self
}
#[inline(always)]
pub const fn set_rating(&mut self, rating: Option<u8>) -> &mut Self {
self.rating = rating;
self
}
#[inline(always)]
pub fn set_note(&mut self, note: impl Into<SmolStr>) -> &mut Self {
self.note = note.into();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum SceneAnnotationError {
#[error("SceneAnnotation id must not be the nil UUID")]
NilId,
#[error("SceneAnnotation `scene_id` (FK → Scene) must not be the nil UUID")]
NilSceneId,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn user_tag_try_new_happy_path() {
let t = UserTag::try_new(Uuid7::new(), "Vacation", Timestamp::default()).unwrap();
assert_eq!(t.name(), "Vacation");
assert!(t.color().is_none());
}
#[test]
fn user_tag_try_new_rejects_nil_id() {
let err = UserTag::try_new(Uuid7::nil(), "x", Timestamp::default()).unwrap_err();
assert_eq!(err, NilIdError::Nil);
assert!(err.is_nil());
}
#[test]
fn user_tag_colour_via_builder_and_setter() {
let t = UserTag::try_new(Uuid7::new(), "Vacation", Timestamp::default())
.unwrap()
.with_color(Some(Rgba::from_components(0xff, 0x88, 0x00, 0xff)));
let c = t.color().expect("colour");
assert_eq!(c.r(), 0xff);
assert_eq!(c.g(), 0x88);
let mut t = t;
t.set_color(None);
assert!(t.color().is_none());
}
#[test]
fn scene_annotation_try_new_is_pristine() {
let a = SceneAnnotation::try_new(Uuid7::new(), Uuid7::new(), Timestamp::default()).unwrap();
assert!(!a.is_favorite());
assert!(a.user_tags_slice().is_empty());
assert!(a.rating().is_none());
assert!(a.note().is_empty());
}
#[test]
fn scene_annotation_try_new_rejects_nil_id_or_scene_id() {
assert_eq!(
SceneAnnotation::try_new(Uuid7::nil(), Uuid7::new(), Timestamp::default()).err(),
Some(SceneAnnotationError::NilId)
);
assert_eq!(
SceneAnnotation::try_new(Uuid7::new(), Uuid7::nil(), Timestamp::default()).err(),
Some(SceneAnnotationError::NilSceneId)
);
assert!(SceneAnnotationError::NilId.is_nil_id());
assert!(SceneAnnotationError::NilSceneId.is_nil_scene_id());
}
#[test]
fn scene_annotation_tags_are_id_refs_not_strings() {
let scene_id = Uuid7::new();
let t1 = Uuid7::new();
let t2 = Uuid7::new();
let a = SceneAnnotation::try_new(Uuid7::new(), scene_id, Timestamp::default())
.unwrap()
.with_favorite(true)
.with_user_tags(std::vec![t1, t2])
.with_rating(Some(4))
.with_note("great driving scene");
assert_eq!(a.scene_id_ref(), &scene_id);
assert!(a.is_favorite());
assert_eq!(a.rating(), Some(4));
assert_eq!(a.user_tags_slice().len(), 2);
assert!(a.user_tags_slice().contains(&t1));
assert!(a.user_tags_slice().contains(&t2));
assert_eq!(a.note(), "great driving scene");
}
}