shared/domain/
module.rs

1//! Types for jig Modules.
2
3use super::asset::{AssetId, AssetType};
4use crate::api::endpoints::PathPart;
5use chrono::{DateTime, Utc};
6use macros::make_path_parts;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9
10/// Module bodies
11pub mod body;
12
13pub use body::Body as ModuleBody;
14
15wrap_uuid! {
16    /// Wrapper type around [`Uuid`](Uuid), represents the **unique ID** of a module.
17    ///
18    /// This uniquely identifies a module. There is no other module that shares this ID.
19    #[serde(rename_all = "camelCase")]
20    pub struct ModuleId
21}
22
23wrap_uuid! {
24    /// Wrapper type around [`Uuid`](Uuid), represents the **unique ID** of a module.
25    ///
26    /// This uniquely identifies a module. There is no other module that shares this ID.
27    #[serde(rename_all = "camelCase")]
28    pub struct StableModuleId
29}
30
31/// Represents the various kinds of data a module can represent.
32#[repr(i16)]
33#[cfg_attr(feature = "backend", derive(sqlx::Type))]
34#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
35pub enum ModuleKind {
36    /// This is a sort of special module, every jig has one and it can't be deleted TODO: is that so?
37    Cover = 0,
38
39    /// Flashcards
40    Flashcards = 1,
41
42    /// Matching
43    Matching = 2,
44
45    /// Memory Game
46    Memory = 3,
47
48    /// Talking Poster
49    Poster = 4,
50
51    /// Listen & Learn
52    TappingBoard = 5,
53
54    /// Tracing
55    Tracing = 6,
56
57    /// Video
58    Video = 7,
59
60    /// Deprecated, next new module should use this slot
61    //VisualQuiz = 8,
62
63    /// Quiz Game
64    CardQuiz = 9,
65
66    /// Drag & Drop
67    DragDrop = 10,
68
69    /// Legacy
70    Legacy = 11,
71
72    /// ResourceCover user for resources and playlist cover
73    ResourceCover = 12,
74
75    /// Answer This (Previously "Find the Answer")
76    FindAnswer = 13,
77
78    /// Embed
79    Embed = 14,
80}
81
82impl ModuleKind {
83    /// casts `self` to a string
84    #[must_use]
85    pub const fn as_str(self) -> &'static str {
86        match self {
87            Self::Cover => "cover",
88            Self::ResourceCover => "resource-cover",
89            Self::Flashcards => "flashcards",
90            Self::Matching => "matching",
91            Self::Memory => "memory",
92            Self::Poster => "poster",
93            Self::TappingBoard => "tapping-board",
94            Self::DragDrop => "drag-drop",
95            Self::Tracing => "tracing",
96            Self::Video => "video",
97            Self::Embed => "embed",
98            Self::CardQuiz => "card-quiz",
99            Self::Legacy => "legacy",
100            Self::FindAnswer => "find-answer",
101        }
102    }
103
104    /// display name for each module kind
105    pub fn display_name(self) -> &'static str {
106        match self {
107            Self::Cover => "Cover",
108            Self::ResourceCover => "Cover",
109            Self::Flashcards => "Flashcards",
110            Self::Matching => "Matching",
111            Self::Memory => "Memory Game",
112            Self::Poster => "Talking Poster",
113            Self::TappingBoard => "Listen & Learn",
114            Self::Tracing => "Tracing",
115            Self::Video => "Video Player",
116            Self::Embed => "Embed It",
117            Self::CardQuiz => "Multiple Choice",
118            Self::DragDrop => "Drag & Drop",
119            Self::FindAnswer => "Answer This",
120            Self::Legacy => "Legacy",
121        }
122    }
123
124    /// Whether this ModuleKind has auto-generated content
125    pub fn autogenerated(&self) -> bool {
126        match self {
127            Self::Flashcards | Self::Matching | Self::Memory | Self::CardQuiz => true,
128            _ => false,
129        }
130    }
131
132    /// Whether this ModuleKind has scoring
133    pub fn has_scoring(&self) -> bool {
134        match self {
135            Self::Matching | Self::CardQuiz | Self::DragDrop | Self::FindAnswer => true,
136            _ => false,
137        }
138    }
139}
140
141impl FromStr for ModuleKind {
142    type Err = anyhow::Error;
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        let res = match s {
145            "cover" => Self::Cover,
146            "resource-cover" => Self::ResourceCover,
147            "flashcards" => Self::Flashcards,
148            "matching" => Self::Matching,
149            "memory" => Self::Memory,
150            "poster" => Self::Poster,
151            "tapping-board" => Self::TappingBoard,
152            "drag-drop" => Self::DragDrop,
153            "tracing" => Self::Tracing,
154            "video" => Self::Video,
155            "embed" => Self::Embed,
156            "card-quiz" => Self::CardQuiz,
157            "legacy" => Self::Legacy,
158            "find-answer" => Self::FindAnswer,
159            _ => anyhow::bail!("Invalid ModuleKind: {}", s),
160        };
161
162        Ok(res)
163    }
164}
165impl ModuleBody {
166    /// Maps module content from request body
167    pub fn map_module_contents(body: &Self) -> anyhow::Result<(ModuleKind, serde_json::Value)> {
168        let kind = body.kind();
169
170        let body = match body {
171            Self::CardQuiz(body) => serde_json::to_value(body)?,
172            Self::Cover(body) => serde_json::to_value(body)?,
173            Self::ResourceCover(body) => serde_json::to_value(body)?,
174            Self::DragDrop(body) => serde_json::to_value(body)?,
175            Self::Flashcards(body) => serde_json::to_value(body)?,
176            Self::Matching(body) => serde_json::to_value(body)?,
177            Self::MemoryGame(body) => serde_json::to_value(body)?,
178            Self::Poster(body) => serde_json::to_value(body)?,
179            Self::TappingBoard(body) => serde_json::to_value(body)?,
180            Self::Video(body) => serde_json::to_value(body)?,
181            Self::Embed(body) => serde_json::to_value(body)?,
182            Self::FindAnswer(body) => serde_json::to_value(body)?,
183            Self::Legacy(body) => serde_json::to_value(body)?,
184        };
185
186        Ok((kind, body))
187    }
188
189    /// Transforms module content from database
190    pub fn transform_response_kind(
191        contents: serde_json::Value,
192        kind: ModuleKind,
193    ) -> anyhow::Result<Self> {
194        match kind {
195            ModuleKind::CardQuiz => Ok(Self::CardQuiz(serde_json::from_value(contents)?)),
196            ModuleKind::Cover => Ok(Self::Cover(serde_json::from_value(contents)?)),
197            ModuleKind::ResourceCover => Ok(Self::ResourceCover(serde_json::from_value(contents)?)),
198            ModuleKind::DragDrop => Ok(Self::DragDrop(serde_json::from_value(contents)?)),
199            ModuleKind::Flashcards => Ok(Self::Flashcards(serde_json::from_value(contents)?)),
200            ModuleKind::Matching => Ok(Self::Matching(serde_json::from_value(contents)?)),
201            ModuleKind::Memory => Ok(Self::MemoryGame(serde_json::from_value(contents)?)),
202            ModuleKind::Poster => Ok(Self::Poster(serde_json::from_value(contents)?)),
203            ModuleKind::TappingBoard => Ok(Self::TappingBoard(serde_json::from_value(contents)?)),
204            ModuleKind::Video => Ok(Self::Video(serde_json::from_value(contents)?)),
205            ModuleKind::Embed => Ok(Self::Embed(serde_json::from_value(contents)?)),
206            ModuleKind::FindAnswer => Ok(Self::FindAnswer(serde_json::from_value(contents)?)),
207            ModuleKind::Legacy => Ok(Self::Legacy(serde_json::from_value(contents)?)),
208
209            _ => anyhow::bail!("Unimplemented response kind"),
210        }
211    }
212}
213
214/// Minimal information about a module.
215#[derive(Clone, Serialize, Deserialize, Debug)]
216pub struct LiteModule {
217    /// The module's unique ID.
218    pub id: ModuleId,
219
220    /// ID that doesn't change when publishing.
221    pub stable_id: StableModuleId,
222
223    /// Which kind of module this is.
224    pub kind: ModuleKind,
225
226    /// Whether this module is completed.
227    #[serde(default)]
228    pub is_complete: bool,
229}
230
231/// Over the wire representation of a module.
232#[derive(Serialize, Deserialize, Debug, Clone)]
233pub struct Module {
234    /// The module's unique ID.
235    pub id: ModuleId,
236
237    /// ID that doesn't change when publishing.
238    pub stable_id: StableModuleId,
239
240    /// The module's body.
241    pub body: ModuleBody,
242
243    /// Whether the module is complete or not.
244    pub is_complete: bool,
245
246    /// Whether a jig has been updated.
247    pub is_updated: bool,
248
249    /// When the module was originally created.
250    pub created_at: DateTime<Utc>,
251
252    /// When the module was last updated.
253    pub updated_at: DateTime<Utc>,
254}
255
256make_path_parts!(ModuleCreatePath => "/v1/module/draft");
257
258/// Request to create a new `Module`.
259#[derive(Serialize, Deserialize, Debug)]
260pub struct ModuleCreateRequest {
261    /// ID for Playlist or JIG
262    #[serde(flatten)]
263    pub parent_id: AssetId,
264
265    /// The module's body.
266    pub body: ModuleBody,
267}
268
269make_path_parts!(ModuleGetLivePath => "/v1/{}/module/live/{}" => AssetType, ModuleId);
270
271make_path_parts!(ModuleGetDraftPath => "/v1/{}/module/draft/{}" => AssetType, ModuleId);
272
273/// Response for successfully finding a module
274#[derive(Serialize, Deserialize, Debug, Clone)]
275pub struct ModuleResponse {
276    /// The module we found
277    pub module: Module,
278}
279
280make_path_parts!(ModuleUploadPath => "/v1/module/draft/{}" => ModuleId);
281
282/// Request to update a `Module`.
283/// note: fields here cannot be nulled out (`None` means "don't change").
284#[derive(Serialize, Deserialize, Debug)]
285#[serde(rename_all = "camelCase")]
286pub struct ModuleUpdateRequest {
287    /// ID for Playlist or JIG
288    #[serde(flatten)]
289    pub parent_id: AssetId,
290
291    /// The module's body.
292    #[serde(default)]
293    pub body: Option<ModuleBody>,
294
295    /// Where to move this module to in the parent. Relevant for the order that the modules
296    /// are returned when fetching JIG data.
297    ///
298    /// Numbers larger than the parent JIG's module count will move it to the *end*.
299    #[serde(default)]
300    pub index: Option<u16>,
301
302    /// check if module is complete
303    #[serde(default)]
304    pub is_complete: Option<bool>,
305}
306
307make_path_parts!(ModuleDeletePath => "/v1/module/draft/{}" => ModuleId);
308
309/// Request to delete a `Module`.
310#[derive(Serialize, Deserialize, Debug)]
311#[serde(rename_all = "camelCase")]
312pub struct ModuleDeleteRequest {
313    /// ID for Playlist or JIG
314    #[serde(flatten)]
315    pub parent_id: AssetId,
316}