headless_lms_mcp/engine/course_structure/
model.rs1use chrono::{DateTime, Utc};
4use serde::Serialize;
5use uuid::Uuid;
6
7use crate::engine::path::{is_course_root, split_path};
8
9pub(crate) const RENDERED_KEYS: [&str; 4] = ["path", "kind", "pending", "is_chapter_front_page"];
12
13#[derive(Debug, Clone, PartialEq, Serialize)]
15pub struct CourseStructure {
16 pub id: Uuid,
17 pub pages: Vec<StructurePage>,
19 pub modules: Vec<StructureModule>,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize)]
25pub struct StructureModule {
26 pub id: Uuid,
27 pub default: bool,
28 pub name: Option<String>,
30 pub chapters: Vec<StructureChapter>,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize)]
35pub struct StructureChapter {
36 pub id: Uuid,
37 pub chapter_number: i32,
39 pub name: String,
40 pub color: Option<String>,
41 pub opens_at: Option<DateTime<Utc>>,
42 pub deadline: Option<DateTime<Utc>>,
43 pub pages: Vec<StructurePage>,
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize)]
49pub struct StructurePage {
50 pub id: Uuid,
51 pub title: String,
52 pub url_path: String,
53 pub hidden: bool,
54 pub block_count: u32,
55 pub exercise_count: u32,
56 #[serde(skip_serializing_if = "std::ops::Not::not")]
57 pub has_submissions: bool,
58 pub page_language_group_id: Option<Uuid>,
61}
62
63impl CourseStructure {
64 pub fn modules(&self) -> impl Iterator<Item = &StructureModule> {
66 self.modules.iter()
67 }
68
69 pub fn chapters(&self) -> impl Iterator<Item = &StructureChapter> {
71 self.modules.iter().flat_map(|m| m.chapters.iter())
72 }
73
74 pub(crate) fn chapters_mut(&mut self) -> impl Iterator<Item = &mut StructureChapter> {
76 self.modules.iter_mut().flat_map(|m| m.chapters.iter_mut())
77 }
78
79 pub fn pages(&self) -> impl Iterator<Item = (Option<&StructureChapter>, &StructurePage)> {
81 self.pages.iter().map(|p| (None, p)).chain(
82 self.chapters()
83 .flat_map(|c| c.pages.iter().map(move |p| (Some(c), p))),
84 )
85 }
86
87 pub(crate) fn pages_mut(&mut self) -> impl Iterator<Item = &mut StructurePage> {
89 self.pages.iter_mut().chain(
90 self.modules
91 .iter_mut()
92 .flat_map(|m| m.chapters.iter_mut())
93 .flat_map(|c| c.pages.iter_mut()),
94 )
95 }
96
97 pub fn page(&self, page_id: Uuid) -> Option<&StructurePage> {
99 self.pages().map(|(_, p)| p).find(|p| p.id == page_id)
100 }
101
102 pub(crate) fn page_mut(&mut self, page_id: Uuid) -> Option<&mut StructurePage> {
104 self.pages_mut().find(|p| p.id == page_id)
105 }
106
107 pub fn chapter_of_page(&self, page_id: Uuid) -> Option<&StructureChapter> {
109 self.pages()
110 .find(|(_, p)| p.id == page_id)
111 .and_then(|(c, _)| c)
112 }
113
114 pub fn is_chapter_front_page(&self, page_id: Uuid) -> bool {
116 self.chapters()
117 .any(|c| c.pages.first().is_some_and(|p| p.id == page_id))
118 }
119
120 pub fn module(&self, id: Uuid) -> Option<&StructureModule> {
122 self.modules.iter().find(|m| m.id == id)
123 }
124
125 pub(crate) fn module_mut(&mut self, id: Uuid) -> Option<&mut StructureModule> {
127 self.modules.iter_mut().find(|m| m.id == id)
128 }
129
130 pub fn chapter(&self, id: Uuid) -> Option<&StructureChapter> {
132 self.chapters().find(|c| c.id == id)
133 }
134
135 pub(crate) fn chapter_mut(&mut self, id: Uuid) -> Option<&mut StructureChapter> {
137 self.chapters_mut().find(|c| c.id == id)
138 }
139
140 pub(crate) fn locate(&self, id: Uuid) -> Option<Loc> {
142 let loc = |list, index| Some(Loc { list, index });
143 if let Some(index) = self.pages.iter().position(|p| p.id == id) {
144 return loc(ListRef::CoursePages, index);
145 }
146 for (module_index, module) in self.modules.iter().enumerate() {
147 if module.id == id {
148 return loc(ListRef::CourseModules, module_index);
149 }
150 for (chapter_index, chapter) in module.chapters.iter().enumerate() {
151 if chapter.id == id {
152 return loc(ListRef::ModuleChapters(module.id), chapter_index);
153 }
154 if let Some(index) = chapter.pages.iter().position(|p| p.id == id) {
155 return loc(ListRef::ChapterPages(chapter.id), index);
156 }
157 }
158 }
159 None
160 }
161
162 pub(crate) fn list(&self, list: ListRef) -> Vec<EntityRef<'_>> {
164 match list {
165 ListRef::CoursePages => self.pages.iter().map(EntityRef::Page).collect(),
166 ListRef::CourseModules => self.modules.iter().map(EntityRef::Module).collect(),
167 ListRef::ModuleChapters(id) => self
168 .module(id)
169 .map(|m| m.chapters.iter().map(EntityRef::Chapter).collect())
170 .unwrap_or_default(),
171 ListRef::ChapterPages(id) => self
172 .chapter(id)
173 .map(|c| c.pages.iter().map(EntityRef::Page).collect())
174 .unwrap_or_default(),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy)]
181pub(crate) struct Loc {
182 pub(crate) list: ListRef,
183 pub(crate) index: usize,
184}
185
186impl Loc {
187 pub(crate) fn kind(&self) -> Kind {
188 self.list.kind()
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub(crate) enum Kind {
195 Module,
196 Chapter,
197 Page,
198}
199
200impl Kind {
201 pub(crate) fn noun(self) -> &'static str {
203 match self {
204 Kind::Module => "module",
205 Kind::Chapter => "chapter",
206 Kind::Page => "page",
207 }
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub(crate) enum ListRef {
214 CoursePages,
215 CourseModules,
216 ModuleChapters(Uuid),
217 ChapterPages(Uuid),
218}
219
220impl ListRef {
221 pub(crate) fn of_course(key: &str) -> Option<Self> {
223 match key {
224 "pages" => Some(ListRef::CoursePages),
225 "modules" => Some(ListRef::CourseModules),
226 _ => None,
227 }
228 }
229
230 pub(crate) fn of_entity(kind: Kind, id: Uuid, key: &str) -> Option<Self> {
233 match (kind, key) {
234 (Kind::Module, "chapters") => Some(ListRef::ModuleChapters(id)),
235 (Kind::Chapter, "pages") => Some(ListRef::ChapterPages(id)),
236 _ => None,
237 }
238 }
239
240 pub(crate) fn owner(self) -> Option<Uuid> {
242 match self {
243 ListRef::ModuleChapters(id) | ListRef::ChapterPages(id) => Some(id),
244 ListRef::CoursePages | ListRef::CourseModules => None,
245 }
246 }
247
248 pub(crate) fn kind(self) -> Kind {
249 match self {
250 ListRef::CoursePages | ListRef::ChapterPages(_) => Kind::Page,
251 ListRef::CourseModules => Kind::Module,
252 ListRef::ModuleChapters(_) => Kind::Chapter,
253 }
254 }
255
256 pub(crate) fn path(self) -> String {
258 match self {
259 ListRef::CoursePages => "/course/pages".to_string(),
260 ListRef::CourseModules => "/course/modules".to_string(),
261 ListRef::ModuleChapters(id) => format!("/{id}/chapters"),
262 ListRef::ChapterPages(id) => format!("/{id}/pages"),
263 }
264 }
265}
266
267#[derive(Debug, Clone, Copy, Serialize)]
269#[serde(untagged)]
270pub(crate) enum EntityRef<'a> {
271 Module(&'a StructureModule),
272 Chapter(&'a StructureChapter),
273 Page(&'a StructurePage),
274}
275
276impl EntityRef<'_> {
277 pub(crate) fn id(self) -> Uuid {
278 match self {
279 EntityRef::Module(m) => m.id,
280 EntityRef::Chapter(c) => c.id,
281 EntityRef::Page(p) => p.id,
282 }
283 }
284
285 pub(crate) fn kind(self) -> Kind {
286 match self {
287 EntityRef::Module(_) => Kind::Module,
288 EntityRef::Chapter(_) => Kind::Chapter,
289 EntityRef::Page(_) => Kind::Page,
290 }
291 }
292}
293
294pub(crate) struct StructurePath {
297 pub(crate) root: String,
299 pub(crate) child: Option<String>,
301 pub(crate) is_deeper: bool,
303}
304
305impl StructurePath {
306 pub(crate) fn is_course(&self, course_id: Uuid) -> bool {
308 is_course_root(&self.root, course_id)
309 }
310}
311
312pub(crate) fn parse_structure_path(path: &str) -> Option<StructurePath> {
314 let mut segments = split_path(path)?.into_iter();
315 let root = segments.next().unwrap_or_default();
316 let child = segments.next();
317 Some(StructurePath {
318 root,
319 child,
320 is_deeper: segments.next().is_some(),
321 })
322}
323
324pub(super) enum Entity {
325 Module(StructureModule),
326 Chapter(StructureChapter),
327 Page(StructurePage),
328}
329
330impl Entity {
331 pub(super) fn id(&self) -> Uuid {
332 match self {
333 Entity::Module(m) => m.id,
334 Entity::Chapter(c) => c.id,
335 Entity::Page(p) => p.id,
336 }
337 }
338
339 pub(super) fn kind(&self) -> Kind {
340 match self {
341 Entity::Module(_) => Kind::Module,
342 Entity::Chapter(_) => Kind::Chapter,
343 Entity::Page(_) => Kind::Page,
344 }
345 }
346}
347
348pub(super) fn entity_ids(entity: &Entity) -> Vec<Uuid> {
349 match entity {
350 Entity::Module(m) => std::iter::once(m.id)
351 .chain(
352 m.chapters
353 .iter()
354 .flat_map(|c| std::iter::once(c.id).chain(c.pages.iter().map(|p| p.id))),
355 )
356 .collect(),
357 Entity::Chapter(c) => std::iter::once(c.id)
358 .chain(c.pages.iter().map(|p| p.id))
359 .collect(),
360 Entity::Page(p) => vec![p.id],
361 }
362}