1use 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
10pub mod body;
12
13pub use body::Body as ModuleBody;
14
15wrap_uuid! {
16 #[serde(rename_all = "camelCase")]
20 pub struct ModuleId
21}
22
23wrap_uuid! {
24 #[serde(rename_all = "camelCase")]
28 pub struct StableModuleId
29}
30
31#[repr(i16)]
33#[cfg_attr(feature = "backend", derive(sqlx::Type))]
34#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
35pub enum ModuleKind {
36 Cover = 0,
38
39 Flashcards = 1,
41
42 Matching = 2,
44
45 Memory = 3,
47
48 Poster = 4,
50
51 TappingBoard = 5,
53
54 Tracing = 6,
56
57 Video = 7,
59
60 CardQuiz = 9,
65
66 DragDrop = 10,
68
69 Legacy = 11,
71
72 ResourceCover = 12,
74
75 FindAnswer = 13,
77
78 Embed = 14,
80}
81
82impl ModuleKind {
83 #[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 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 pub fn autogenerated(&self) -> bool {
126 match self {
127 Self::Flashcards | Self::Matching | Self::Memory | Self::CardQuiz => true,
128 _ => false,
129 }
130 }
131
132 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 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 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#[derive(Clone, Serialize, Deserialize, Debug)]
216pub struct LiteModule {
217 pub id: ModuleId,
219
220 pub stable_id: StableModuleId,
222
223 pub kind: ModuleKind,
225
226 #[serde(default)]
228 pub is_complete: bool,
229}
230
231#[derive(Serialize, Deserialize, Debug, Clone)]
233pub struct Module {
234 pub id: ModuleId,
236
237 pub stable_id: StableModuleId,
239
240 pub body: ModuleBody,
242
243 pub is_complete: bool,
245
246 pub is_updated: bool,
248
249 pub created_at: DateTime<Utc>,
251
252 pub updated_at: DateTime<Utc>,
254}
255
256make_path_parts!(ModuleCreatePath => "/v1/module/draft");
257
258#[derive(Serialize, Deserialize, Debug)]
260pub struct ModuleCreateRequest {
261 #[serde(flatten)]
263 pub parent_id: AssetId,
264
265 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#[derive(Serialize, Deserialize, Debug, Clone)]
275pub struct ModuleResponse {
276 pub module: Module,
278}
279
280make_path_parts!(ModuleUploadPath => "/v1/module/draft/{}" => ModuleId);
281
282#[derive(Serialize, Deserialize, Debug)]
285#[serde(rename_all = "camelCase")]
286pub struct ModuleUpdateRequest {
287 #[serde(flatten)]
289 pub parent_id: AssetId,
290
291 #[serde(default)]
293 pub body: Option<ModuleBody>,
294
295 #[serde(default)]
300 pub index: Option<u16>,
301
302 #[serde(default)]
304 pub is_complete: Option<bool>,
305}
306
307make_path_parts!(ModuleDeletePath => "/v1/module/draft/{}" => ModuleId);
308
309#[derive(Serialize, Deserialize, Debug)]
311#[serde(rename_all = "camelCase")]
312pub struct ModuleDeleteRequest {
313 #[serde(flatten)]
315 pub parent_id: AssetId,
316}