shared/domain/
asset.rs

1//! Types for Assets, Jig and LearningPath.
2
3use std::{
4    collections::HashMap,
5    fmt::{self, Debug},
6    str::FromStr,
7};
8
9use anyhow::anyhow;
10use chrono::{DateTime, Utc};
11// use dyn_clone::DynClone;
12use serde::{Deserialize, Serialize};
13use strum_macros::Display;
14use uuid::Uuid;
15
16use crate::{
17    api::endpoints::PathPart,
18    domain::{
19        category::CategoryId,
20        meta::{AffiliationId, AgeRangeId},
21        module::LiteModule,
22    },
23};
24
25use super::{
26    additional_resource::AdditionalResource,
27    course::{CourseId, CourseResponse},
28    jig::{JigId, JigResponse},
29    module::body::ThemeId,
30    playlist::{PlaylistId, PlaylistResponse},
31    resource::{ResourceId, ResourceResponse},
32    user::UserId,
33};
34
35/// AssetType
36#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Display)]
37#[serde(rename_all = "kebab-case")]
38#[strum(serialize_all = "kebab-case")]
39pub enum AssetType {
40    /// JIG
41    Jig,
42
43    /// Resource
44    Resource,
45
46    /// Playlist
47    Playlist,
48
49    /// Course
50    Course,
51}
52
53impl AssetType {
54    /// check if jig
55    pub fn is_jig(&self) -> bool {
56        matches!(self, Self::Jig)
57    }
58
59    /// check if resource
60    pub fn is_resource(&self) -> bool {
61        matches!(self, Self::Resource)
62    }
63
64    /// check if playlist
65    pub fn is_playlist(&self) -> bool {
66        matches!(self, Self::Playlist)
67    }
68
69    /// check if course
70    pub fn is_course(&self) -> bool {
71        matches!(self, Self::Course)
72    }
73
74    /// Represents the asset type as a `str`
75    pub fn as_str(&self) -> &'static str {
76        match self {
77            Self::Jig => "jig",
78            Self::Resource => "resource",
79            Self::Playlist => "playlist",
80            Self::Course => "course",
81        }
82    }
83
84    /// asset type display name
85    pub fn display_name(&self) -> &'static str {
86        match self {
87            Self::Jig => "JIG",
88            Self::Resource => "resource",
89            Self::Playlist => "playlist",
90            Self::Course => "course",
91        }
92    }
93
94    /// asset type display name capitalized
95    pub fn display_name_capitalized(&self) -> &'static str {
96        match self {
97            Self::Jig => "JIG",
98            Self::Resource => "Resource",
99            Self::Playlist => "Playlist",
100            Self::Course => "course",
101        }
102    }
103
104    /// return to gallery button on sidebar
105    pub fn sidebar_header(&self) -> &'static str {
106        match self {
107            Self::Jig => "JIG",
108            Self::Resource => "resource",
109            Self::Playlist => "playlist",
110            Self::Course => "course",
111        }
112    }
113
114    /// Create asset id from self and uuid
115    pub fn to_asset_id(&self, uuid: Uuid) -> AssetId {
116        match self {
117            AssetType::Jig => JigId(uuid).into(),
118            AssetType::Playlist => PlaylistId(uuid).into(),
119            AssetType::Resource => ResourceId(uuid).into(),
120            AssetType::Course => CourseId(uuid).into(),
121        }
122    }
123}
124
125impl From<&AssetId> for AssetType {
126    fn from(asset_id: &AssetId) -> Self {
127        match asset_id {
128            AssetId::JigId(_) => AssetType::Jig,
129            AssetId::PlaylistId(_) => AssetType::Playlist,
130            AssetId::ResourceId(_) => AssetType::Resource,
131            AssetId::CourseId(_) => AssetType::Course,
132        }
133    }
134}
135
136impl TryFrom<&str> for AssetType {
137    type Error = ();
138
139    fn try_from(s: &str) -> Result<Self, Self::Error> {
140        match s {
141            "jig" => Ok(Self::Jig),
142            "playlist" => Ok(Self::Playlist),
143            "resource" => Ok(Self::Resource),
144            "course" => Ok(Self::Course),
145            _ => Err(()),
146        }
147    }
148}
149
150impl PathPart for AssetType {
151    fn get_path_string(&self) -> String {
152        self.as_str().to_string()
153    }
154}
155
156/// AssetId
157#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
158#[serde(rename_all = "camelCase")]
159pub enum AssetId {
160    /// JIG ID
161    JigId(JigId),
162
163    /// Playlist ID
164    PlaylistId(PlaylistId),
165
166    /// Resource ID
167    ResourceId(ResourceId),
168
169    /// Course ID
170    CourseId(CourseId),
171}
172
173impl From<JigId> for AssetId {
174    fn from(jig_id: JigId) -> Self {
175        Self::JigId(jig_id)
176    }
177}
178
179impl From<PlaylistId> for AssetId {
180    fn from(playlist_id: PlaylistId) -> Self {
181        Self::PlaylistId(playlist_id)
182    }
183}
184
185impl From<ResourceId> for AssetId {
186    fn from(resource_id: ResourceId) -> Self {
187        Self::ResourceId(resource_id)
188    }
189}
190
191impl From<CourseId> for AssetId {
192    fn from(course_id: CourseId) -> Self {
193        Self::CourseId(course_id)
194    }
195}
196
197impl AssetId {
198    /// get asset type
199    pub fn asset_type(&self) -> AssetType {
200        self.into()
201    }
202
203    /// get jig id value as ref
204    pub fn unwrap_jig(&self) -> &JigId {
205        match self {
206            Self::JigId(jig_id) => jig_id,
207            _ => panic!(),
208        }
209    }
210
211    /// get playlist id value as ref
212    pub fn unwrap_playlist(&self) -> &PlaylistId {
213        match self {
214            Self::PlaylistId(playlist_id) => playlist_id,
215            _ => panic!(),
216        }
217    }
218
219    /// get resource id value as ref
220    pub fn unwrap_resource(&self) -> &ResourceId {
221        match self {
222            Self::ResourceId(resource_id) => resource_id,
223            _ => panic!(),
224        }
225    }
226
227    /// get course id value as ref
228    pub fn unwrap_course(&self) -> &CourseId {
229        match self {
230            Self::CourseId(course_id) => course_id,
231            _ => panic!(),
232        }
233    }
234
235    /// get the id uuid
236    pub fn uuid(&self) -> &Uuid {
237        match self {
238            Self::JigId(jig_id) => &jig_id.0,
239            Self::PlaylistId(playlist_id) => &playlist_id.0,
240            Self::ResourceId(resource_id) => &resource_id.0,
241            Self::CourseId(course_id) => &course_id.0,
242        }
243    }
244
245    /// check if jig
246    pub fn is_jig_id(&self) -> bool {
247        matches!(self, Self::JigId(_))
248    }
249
250    /// check if playlist
251    pub fn is_playlist_id(&self) -> bool {
252        matches!(self, Self::PlaylistId(_))
253    }
254
255    /// check if resource
256    pub fn is_resource_id(&self) -> bool {
257        matches!(self, Self::ResourceId(_))
258    }
259
260    /// check if course
261    pub fn is_course_id(&self) -> bool {
262        matches!(self, Self::CourseId(_))
263    }
264}
265
266/// Asset
267#[derive(Clone, Serialize, Deserialize, Debug)]
268#[serde(rename_all = "camelCase")]
269pub enum Asset {
270    /// JIG ID associated with the module.
271    Jig(JigResponse),
272
273    /// Playlist ID associated with the module.
274    Playlist(PlaylistResponse),
275
276    /// Resource ID associated with the module.
277    Resource(ResourceResponse),
278
279    /// Course ID associated with the module.
280    Course(CourseResponse),
281}
282
283impl From<JigResponse> for Asset {
284    fn from(jig: JigResponse) -> Self {
285        Self::Jig(jig)
286    }
287}
288
289impl From<PlaylistResponse> for Asset {
290    fn from(playlist: PlaylistResponse) -> Self {
291        Self::Playlist(playlist)
292    }
293}
294
295impl From<ResourceResponse> for Asset {
296    fn from(resource: ResourceResponse) -> Self {
297        Self::Resource(resource)
298    }
299}
300
301impl From<CourseResponse> for Asset {
302    fn from(course: CourseResponse) -> Self {
303        Self::Course(course)
304    }
305}
306
307impl Asset {
308    /// get asset type
309    pub fn asset_type(&self) -> AssetType {
310        (&self.id()).into()
311    }
312
313    /// get jig value as ref
314    pub fn unwrap_jig(&self) -> &JigResponse {
315        match self {
316            Self::Jig(jig) => jig,
317            _ => panic!(),
318        }
319    }
320
321    /// get resource value as ref
322    pub fn unwrap_resource(&self) -> &ResourceResponse {
323        match self {
324            Self::Resource(resource) => resource,
325            _ => panic!(),
326        }
327    }
328
329    /// get playlist value as ref
330    pub fn unwrap_playlist(&self) -> &PlaylistResponse {
331        match self {
332            Self::Playlist(playlist) => playlist,
333            _ => panic!(),
334        }
335    }
336
337    /// get course value as ref
338    pub fn unwrap_course(&self) -> &CourseResponse {
339        match self {
340            Self::Course(course) => course,
341            _ => panic!(),
342        }
343    }
344
345    /// check if is jig
346    pub fn is_jig(&self) -> bool {
347        matches!(self, Self::Jig(_))
348    }
349
350    /// check if is playlist
351    pub fn is_playlist(&self) -> bool {
352        matches!(self, Self::Playlist(_))
353    }
354
355    /// check if is resource
356    pub fn is_resource(&self) -> bool {
357        matches!(self, Self::Resource(_))
358    }
359
360    /// check if is course
361    pub fn is_course(&self) -> bool {
362        matches!(self, Self::Course(_))
363    }
364
365    /// get id
366    pub fn id(&self) -> AssetId {
367        match self {
368            Self::Jig(jig) => jig.id.into(),
369            Self::Playlist(playlist) => playlist.id.into(),
370            Self::Resource(resource) => resource.id.into(),
371            Self::Course(course) => course.id.into(),
372        }
373    }
374
375    /// get id
376    pub fn published_at(&self) -> Option<DateTime<Utc>> {
377        match self {
378            Self::Jig(jig) => jig.published_at,
379            Self::Playlist(playlist) => playlist.published_at,
380            Self::Resource(resource) => resource.published_at,
381            Self::Course(course) => course.published_at,
382        }
383    }
384
385    /// get display_name
386    pub fn display_name(&self) -> &String {
387        match self {
388            Self::Jig(jig) => &jig.jig_data.display_name,
389            Self::Playlist(playlist) => &playlist.playlist_data.display_name,
390            Self::Resource(resource) => &resource.resource_data.display_name,
391            Self::Course(course) => &course.course_data.display_name,
392        }
393    }
394
395    /// get language
396    pub fn language(&self) -> &String {
397        match self {
398            Self::Jig(jig) => &jig.jig_data.language,
399            Self::Playlist(playlist) => &playlist.playlist_data.language,
400            Self::Resource(resource) => &resource.resource_data.language,
401            Self::Course(course) => &course.course_data.language,
402        }
403    }
404
405    /// get description
406    pub fn description(&self) -> &String {
407        match self {
408            Self::Jig(jig) => &jig.jig_data.description,
409            Self::Playlist(playlist) => &playlist.playlist_data.description,
410            Self::Resource(resource) => &resource.resource_data.description,
411            Self::Course(course) => &course.course_data.description,
412        }
413    }
414
415    /// get cover
416    pub fn cover(&self) -> Option<&LiteModule> {
417        match self {
418            Self::Jig(jig) => jig.jig_data.modules.first(),
419            Self::Playlist(playlist) => playlist.playlist_data.cover.as_ref(),
420            Self::Resource(resource) => resource.resource_data.cover.as_ref(),
421            Self::Course(course) => course.course_data.cover.as_ref(),
422        }
423    }
424
425    /// get privacy_level
426    pub fn privacy_level(&self) -> &PrivacyLevel {
427        match self {
428            Self::Jig(jig) => &jig.jig_data.privacy_level,
429            Self::Playlist(playlist) => &playlist.playlist_data.privacy_level,
430            Self::Resource(resource) => &resource.resource_data.privacy_level,
431            Self::Course(course) => &course.course_data.privacy_level,
432        }
433    }
434
435    /// get other_keywords
436    pub fn other_keywords(&self) -> &String {
437        match self {
438            Self::Jig(jig) => &jig.jig_data.other_keywords,
439            Self::Playlist(playlist) => &playlist.playlist_data.other_keywords,
440            Self::Resource(resource) => &resource.resource_data.other_keywords,
441            Self::Course(course) => &course.course_data.other_keywords,
442        }
443    }
444
445    /// get translated_keywords
446    pub fn translated_keywords(&self) -> &String {
447        match self {
448            Self::Jig(jig) => &jig.jig_data.translated_keywords,
449            Self::Playlist(playlist) => &playlist.playlist_data.translated_keywords,
450            Self::Resource(resource) => &resource.resource_data.translated_keywords,
451            Self::Course(course) => &course.course_data.translated_keywords,
452        }
453    }
454
455    /// get age_ranges
456    pub fn age_ranges(&self) -> &Vec<AgeRangeId> {
457        match self {
458            Self::Jig(jig) => &jig.jig_data.age_ranges,
459            Self::Playlist(playlist) => &playlist.playlist_data.age_ranges,
460            Self::Resource(resource) => &resource.resource_data.age_ranges,
461            Self::Course(_) => unimplemented!(),
462        }
463    }
464
465    /// get affiliations
466    pub fn affiliations(&self) -> &Vec<AffiliationId> {
467        match self {
468            Self::Jig(jig) => &jig.jig_data.affiliations,
469            Self::Playlist(playlist) => &playlist.playlist_data.affiliations,
470            Self::Resource(resource) => &resource.resource_data.affiliations,
471            Self::Course(_) => panic!(),
472        }
473    }
474
475    /// get categories
476    pub fn categories(&self) -> &Vec<CategoryId> {
477        match self {
478            Self::Jig(jig) => &jig.jig_data.categories,
479            Self::Playlist(playlist) => &playlist.playlist_data.categories,
480            Self::Resource(resource) => &resource.resource_data.categories,
481            Self::Course(course) => &course.course_data.categories,
482        }
483    }
484
485    /// get likes
486    pub fn likes(&self) -> i64 {
487        match self {
488            Self::Jig(jig) => jig.likes,
489            Self::Playlist(playlist) => playlist.likes,
490            Self::Resource(resource) => resource.likes,
491            Self::Course(course) => course.likes,
492        }
493    }
494
495    /// is likes by current user
496    pub fn is_liked(&self) -> bool {
497        match self {
498            Self::Jig(jig) => jig.is_liked,
499            Self::Playlist(playlist) => playlist.is_liked,
500            Self::Resource(resource) => resource.is_liked,
501            Self::Course(_course) => todo!(),
502        }
503    }
504
505    /// get plays
506    pub fn plays(&self) -> i64 {
507        match self {
508            Self::Jig(jig) => jig.plays,
509            Self::Playlist(playlist) => playlist.plays,
510            Self::Resource(resource) => resource.views,
511            Self::Course(course) => course.plays,
512        }
513    }
514
515    /// get author_id
516    pub fn author_id(&self) -> &Option<UserId> {
517        match self {
518            Self::Jig(jig) => &jig.author_id,
519            Self::Playlist(playlist) => &playlist.author_id,
520            Self::Resource(resource) => &resource.author_id,
521            Self::Course(course) => &course.author_id,
522        }
523    }
524
525    /// get author_name
526    pub fn author_name(&self) -> &Option<String> {
527        match self {
528            Self::Jig(jig) => &jig.author_name,
529            Self::Playlist(playlist) => &playlist.author_name,
530            Self::Resource(resource) => &resource.author_name,
531            Self::Course(course) => &course.author_name,
532        }
533    }
534
535    /// get additional_resources
536    pub fn additional_resources(&self) -> &Vec<AdditionalResource> {
537        match self {
538            Self::Jig(jig) => &jig.jig_data.additional_resources,
539            Self::Playlist(playlist) => &playlist.playlist_data.additional_resources,
540            Self::Resource(resource) => &resource.resource_data.additional_resources,
541            Self::Course(course) => &course.course_data.additional_resources,
542        }
543    }
544
545    /// get translated_description
546    pub fn translated_description(&self) -> &HashMap<String, String> {
547        match self {
548            Self::Jig(jig) => &jig.jig_data.translated_description,
549            Self::Playlist(playlist) => &playlist.playlist_data.translated_description,
550            Self::Resource(resource) => &resource.resource_data.translated_description,
551            Self::Course(course) => &course.course_data.translated_description,
552        }
553    }
554
555    /// get theme
556    pub fn theme(&self) -> ThemeId {
557        match self {
558            Self::Jig(jig) => jig.jig_data.theme,
559            Self::Playlist(_) => ThemeId::default(),
560            Self::Resource(_) => ThemeId::default(),
561            Self::Course(_) => ThemeId::default(),
562        }
563    }
564
565    /// get live_up_to_date
566    pub fn live_up_to_date(&self) -> bool {
567        match self {
568            Self::Jig(jig) => jig.live_up_to_date,
569            Self::Playlist(playlist) => playlist.live_up_to_date,
570            Self::Resource(resource) => resource.live_up_to_date,
571            Self::Course(course) => course.live_up_to_date,
572        }
573    }
574
575    /// whether the current asset is a premium asset
576    pub fn premium(&self) -> bool {
577        match self {
578            Self::Jig(asset) => asset.admin_data.premium,
579            Self::Playlist(asset) => asset.admin_data.premium,
580            Self::Resource(asset) => asset.admin_data.premium,
581            Self::Course(asset) => asset.admin_data.premium,
582        }
583    }
584}
585
586// dyn_clone::clone_trait_object!(Asset);
587
588/// Special parameter for allowing implicit `me` as a user.
589#[derive(Clone, Eq, PartialEq, Debug)]
590pub enum UserOrMe {
591    /// We should use the user found in the session auth.
592    Me,
593
594    /// we should use the provided user.
595    User(UserId),
596}
597
598impl From<UserId> for UserOrMe {
599    fn from(user_id: UserId) -> Self {
600        UserOrMe::User(user_id)
601    }
602}
603
604impl serde::Serialize for UserOrMe {
605    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
606    where
607        S: serde::Serializer,
608    {
609        match self {
610            UserOrMe::Me => serializer.serialize_str("me"),
611            UserOrMe::User(id) => serializer.collect_str(&id),
612        }
613    }
614}
615
616impl<'de> serde::Deserialize<'de> for UserOrMe {
617    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
618    where
619        D: serde::Deserializer<'de>,
620    {
621        struct Visitor;
622
623        impl<'de> serde::de::Visitor<'de> for Visitor {
624            type Value = UserOrMe;
625
626            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
627                formatter.write_str("`me` or `<uuid>`")
628            }
629
630            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
631            where
632                E: serde::de::Error,
633            {
634                if value == "me" {
635                    Ok(UserOrMe::Me)
636                } else {
637                    Uuid::from_str(value)
638                        .map(|id| UserOrMe::User(UserId(id)))
639                        .map_err(|e| E::custom(format!("failed to parse id: {}", e)))
640                }
641            }
642        }
643
644        deserializer.deserialize_str(Visitor)
645    }
646}
647
648/// Sort browse results by timestamp
649#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug, Display)]
650#[cfg_attr(feature = "backend", derive(sqlx::Type))]
651#[serde(rename_all = "camelCase")]
652#[repr(i16)]
653pub enum OrderBy {
654    /// Order Asset results by timestamp created_at
655    #[strum(serialize = "Created")]
656    CreatedAt = 0,
657
658    /// Order Asset results by timestamp published_at
659    #[strum(serialize = "Published")]
660    PublishedAt = 1,
661}
662
663/// Access level for the jig.
664#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
665#[cfg_attr(feature = "backend", derive(sqlx::Type))]
666#[serde(rename_all = "camelCase")]
667#[repr(i16)]
668pub enum PrivacyLevel {
669    /// Publicly available and indexed. Can be shared with others.
670    Public = 0,
671
672    /// Not indexed, but can be accessed by non-owners if the id is known. "Private" in the front-end
673    Unlisted = 1,
674
675    /// NOT IMPLEMENTED. Only available to the author.
676    Private = 2,
677}
678
679impl PrivacyLevel {
680    /// Represents the privacy level as a `str`. Relevant for Algolia tag filtering.
681    pub fn as_str(&self) -> &'static str {
682        match self {
683            Self::Public => "public",
684            Self::Unlisted => "unlisted",
685            Self::Private => "private",
686        }
687    }
688}
689
690impl FromStr for PrivacyLevel {
691    type Err = anyhow::Error;
692
693    fn from_str(s: &str) -> Result<Self, Self::Err> {
694        Ok(match s {
695            "public" => Self::Public,
696            "unlisted" => Self::Unlisted,
697            "private" => Self::Private,
698            _ => return Err(anyhow!("invalid")),
699        })
700    }
701}
702
703impl Default for PrivacyLevel {
704    fn default() -> Self {
705        Self::Public
706    }
707}
708
709/// Whether the data is draft or live.
710#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
711#[cfg_attr(feature = "backend", derive(sqlx::Type))]
712#[serde(rename_all = "camelCase")]
713#[repr(i16)]
714pub enum DraftOrLive {
715    /// Represents a draft copy
716    Draft = 0,
717    /// Represents a live copy
718    Live = 1,
719}
720
721impl Default for DraftOrLive {
722    fn default() -> Self {
723        Self::Live
724    }
725}
726
727impl DraftOrLive {
728    /// create draft variant
729    pub fn draft() -> Self {
730        Self::Draft
731    }
732
733    /// create live variant
734    pub fn live() -> Self {
735        Self::Live
736    }
737
738    /// Returns `true` for a [`Self::Live`] value.
739    ///
740    /// ```
741    /// let x = DraftOrLive::Live;
742    /// assert_eq!(x.is_live(), true);
743    ///
744    /// let x = DraftOrLive::Draft;
745    /// assert_eq!(x.is_live(), false);
746    /// ```
747    pub fn is_live(&self) -> bool {
748        matches!(*self, DraftOrLive::Live)
749    }
750
751    /// Returns `true` for a [`Draft`] value.
752    ///
753    /// ```
754    /// let x = DraftOrLive::Live;
755    /// assert_eq!(x.is_draft(), false);
756    ///
757    /// let x = DraftOrLive::Draft;
758    /// assert_eq!(x.is_draft(), true);
759    /// ```
760    pub fn is_draft(&self) -> bool {
761        !self.is_live()
762    }
763
764    /// get str `draft` of `live`
765    pub fn as_str(&self) -> &'static str {
766        match self {
767            DraftOrLive::Draft => "draft",
768            DraftOrLive::Live => "live",
769        }
770    }
771}
772
773impl FromStr for DraftOrLive {
774    type Err = String;
775    fn from_str(s: &str) -> Result<Self, Self::Err> {
776        match s {
777            "draft" => Ok(Self::Draft),
778            "live" => Ok(Self::Live),
779            s => Err(format!("Can't create DraftFroLive from {:?}", s)),
780        }
781    }
782}
783
784impl From<DraftOrLive> for bool {
785    fn from(draft_or_live: DraftOrLive) -> Self {
786        match draft_or_live {
787            DraftOrLive::Draft => false,
788            DraftOrLive::Live => true,
789        }
790    }
791}
792
793impl From<bool> for DraftOrLive {
794    fn from(draft_or_live: bool) -> Self {
795        match draft_or_live {
796            false => DraftOrLive::Draft,
797            true => DraftOrLive::Live,
798        }
799    }
800}