//! Typed TMDB IDs. #![expect(clippy::module_name_repetitions, reason = "ID types end with Id")] use core::cmp::Ordering; use core::fmt; use core::hash::{Hash, Hasher}; use core::marker::PhantomData; #[cfg(feature = "sea-orm")] use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr}; #[cfg(feature = "sea-orm")] use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value}; /// The internal representation used by TMDB. pub type TmdbRepr = u32; /// An opaque type representing a TMDB ID. #[derive(serde::Serialize, serde::Deserialize)] #[serde(transparent)] #[repr(transparent)] pub struct Id { /// Internal ID representation. id: TmdbRepr, /// Marker to indicate an invariant T. #[serde(skip_serializing, default)] _phantom: PhantomData T>, } // Manual implementation since `T: Clone` is not required #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl Clone for Id { #[inline] fn clone(&self) -> Self { *self } } // Manual implementation since `T: Copy` is not required impl Copy for Id {} // Manual implementation since `T: PartialEq` is not required #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl PartialEq for Id { #[inline] fn eq(&self, other: &Self) -> bool { self.id == other.id } } // Manual implementation since `T: Eq` is not required #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl Eq for Id {} // Manual implementation since `T: PartialOrd` is not required #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl PartialOrd for Id { #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } // Manual implementation since `T: Ord` is not required #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl Ord for Id { #[inline] fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } // Manual implementation since `T: Hash` is not required #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl Hash for Id { #[inline] fn hash(&self, state: &mut H) { self.id.hash(state); } } impl Id { /// Allows the conversion from a raw value to [Id], though the use is discouraged. #[inline] #[must_use] pub fn from_raw(raw: TmdbRepr) -> Self { Self { id: raw, _phantom: PhantomData, } } /// Allows extracting the raw value, though the use is discouraged. #[inline] #[must_use] pub const fn into_raw(self) -> TmdbRepr { self.id } } impl fmt::Debug for Id { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Id") .field("T", &core::any::type_name::()) .field("id", &self.id) .finish() } } #[cfg(feature = "sea-orm")] #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl ValueType for Id { #[inline] fn try_from(v: Value) -> Result { ::try_from(v).map(|id| Self { id, _phantom: PhantomData, }) } #[inline] fn type_name() -> String { format!("Id<{}>", core::any::type_name::()) } #[inline] fn array_type() -> ArrayType { TmdbRepr::array_type() } #[inline] fn column_type() -> ColumnType { TmdbRepr::column_type() } } #[cfg(feature = "sea-orm")] impl From> for Value { #[inline] fn from(value: Id) -> Self { value.id.into() } } #[cfg(feature = "sea-orm")] #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl TryGetable for Id { #[inline] fn try_get_by(res: &QueryResult, index: I) -> Result { TmdbRepr::try_get_by(res, index).map(|id| Self { id, _phantom: PhantomData, }) } } #[cfg(feature = "sea-orm")] impl TryFromU64 for Id { #[inline] fn try_from_u64(n: u64) -> Result { TmdbRepr::try_from_u64(n).map(|id| Self { id, _phantom: PhantomData, }) } } #[cfg(feature = "sea-orm")] impl Nullable for Id { #[inline] fn null() -> Value { TmdbRepr::null() } } #[cfg(test)] mod tests { #[cfg(feature = "sea-orm")] #[expect(dead_code, reason = "structs test derive macros")] mod test_seaorm { use sea_orm::{ ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, EnumIter, PrimaryKeyTrait, }; use super::super::Id; #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "ids")] #[expect(clippy::use_self, reason = "derive macros cause Self to be invalid")] struct Model { #[sea_orm(primary_key, auto_increment = false)] id: Id, nullable: Option>, } #[expect(clippy::missing_trait_methods, reason = "accept default methods")] impl ActiveModelBehavior for ActiveModel {} #[derive(Debug, EnumIter, DeriveRelation)] enum Relation {} } #[test] fn serde() { use super::Id; let id: Id<()> = Id::from_raw(1234); serde_test::assert_tokens(&id, &[serde_test::Token::U32(1234)]); } } #[doc(hidden)] #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum Collection {} /// Type alias for a collection ID. pub type CollectionId = Id; impl From for flix_model::id::CollectionId { #[inline] fn from(value: CollectionId) -> Self { Self::from_raw(value.into_raw().into()) } } impl TryFrom for CollectionId { type Error = >::Error; #[inline] fn try_from(value: flix_model::id::CollectionId) -> Result { value.into_raw().try_into().map(Self::from_raw) } } #[doc(hidden)] #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum Movie {} /// Type alias for a movie ID. pub type MovieId = Id; impl From for flix_model::id::MovieId { #[inline] fn from(value: MovieId) -> Self { Self::from_raw(value.into_raw().into()) } } impl TryFrom for MovieId { type Error = >::Error; #[inline] fn try_from(value: flix_model::id::MovieId) -> Result { value.into_raw().try_into().map(Self::from_raw) } } #[doc(hidden)] #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum Show {} /// Type alias for a show ID. pub type ShowId = Id; impl From for flix_model::id::ShowId { #[inline] fn from(value: ShowId) -> Self { Self::from_raw(value.into_raw().into()) } } impl TryFrom for ShowId { type Error = >::Error; #[inline] fn try_from(value: flix_model::id::ShowId) -> Result { value.into_raw().try_into().map(Self::from_raw) } }