mecomp_storage/db/schemas/
collection.rs

1//! A collection is an auto currated list of similar songs.
2
3use super::Id;
4#[cfg(not(feature = "db"))]
5use super::RecordId;
6use std::time::Duration;
7#[cfg(feature = "db")]
8use surrealdb::RecordId;
9
10pub type CollectionId = RecordId;
11
12pub const TABLE_NAME: &str = "collection";
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15#[cfg_attr(feature = "db", derive(surrealqlx::Table))]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "db", Table("collection"))]
18pub struct Collection {
19    /// the unique identifier for this [`Collection`].
20    #[cfg_attr(feature = "db", field(dt = "record"))]
21    pub id: CollectionId,
22
23    /// The name of the collection.
24    #[cfg_attr(feature = "db", field(dt = "string"))]
25    #[cfg_attr(feature = "db", index(unique))]
26    pub name: String,
27
28    /// Total runtime.
29    #[cfg_attr(
30        feature = "db",
31        field(
32            "TYPE any VALUE <future> {
33LET $songs = (SELECT runtime FROM $this.id->?->song);
34RETURN IF $songs IS NONE { 0s } ELSE { $songs.fold(0s, |$acc, $song| $acc + $song.runtime) };
35}"
36        )
37    )]
38    #[cfg_attr(
39        feature = "db",
40        serde(
41            serialize_with = "super::serialize_duration_as_sql_duration",
42            deserialize_with = "super::deserialize_duration_from_sql_duration"
43        )
44    )]
45    pub runtime: Duration,
46
47    /// the number of songs this collection has.
48    #[cfg_attr(
49        feature = "db",
50        field(
51            "TYPE any VALUE <future> { 
52LET $count = (SELECT count() FROM $this.id->?->song GROUP ALL);
53RETURN IF $count IS NONE { 0 } ELSE IF $count.len() == 0 { 0 } ELSE { ($count[0]).count };
54}"
55        )
56    )]
57    pub song_count: usize,
58}
59
60impl Collection {
61    pub const BRIEF_FIELDS: &'static str = "id,name";
62
63    #[must_use]
64    #[inline]
65    pub fn generate_id() -> CollectionId {
66        RecordId::from_table_key(TABLE_NAME, Id::ulid())
67    }
68}
69
70#[derive(Debug, Default)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize))]
72pub struct CollectionChangeSet {
73    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
74    pub name: Option<String>,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct CollectionBrief {
80    pub id: CollectionId,
81    pub name: String,
82}
83
84impl From<Collection> for CollectionBrief {
85    #[inline]
86    fn from(collection: Collection) -> Self {
87        Self {
88            id: collection.id,
89            name: collection.name,
90        }
91    }
92}
93
94impl From<&Collection> for CollectionBrief {
95    #[inline]
96    fn from(collection: &Collection) -> Self {
97        Self {
98            id: collection.id.clone(),
99            name: collection.name.clone(),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    use pretty_assertions::assert_eq;
109    use rstest::{fixture, rstest};
110
111    #[fixture]
112    fn collection() -> Collection {
113        Collection {
114            id: RecordId::from((TABLE_NAME, "id")),
115            name: "collection".into(),
116            runtime: Duration::from_secs(3600),
117            song_count: 100,
118        }
119    }
120
121    #[fixture]
122    fn collection_brief() -> CollectionBrief {
123        CollectionBrief {
124            id: RecordId::from((TABLE_NAME, "id")),
125            name: "collection".into(),
126        }
127    }
128
129    #[rstest]
130    #[case(collection(), collection_brief())]
131    #[case(&collection(), collection_brief())]
132    fn test_collection_brief_from_collection<T: Into<CollectionBrief>>(
133        #[case] collection: T,
134        #[case] brief: CollectionBrief,
135    ) {
136        let actual: CollectionBrief = collection.into();
137        assert_eq!(actual, brief);
138    }
139}