1pub mod blocks;
5pub mod course_settings;
6pub mod course_structure;
7pub mod error;
8pub mod find_replace;
9pub mod glossary_references;
10pub(crate) mod labels;
11pub mod op;
12pub mod page_content;
13pub(crate) mod path;
14pub mod read;
15pub mod structural_replace;
16
17use headless_lms_utils::document_schema_processor::GutenbergBlock;
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value};
20use uuid::Uuid;
21
22use crate::ids::IdGenerator;
23use crate::instruction_lists::{InstructionsList, InstructionsLocation};
24
25pub use error::{EditError, EditErrorCode, Warning, WarningCode};
26pub use op::{PatchTarget, parse_ops};
27
28#[derive(
31 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, utoipa::ToSchema,
32)]
33#[serde(rename_all = "kebab-case")]
34pub enum PageType {
35 CoursePage,
37 TopLevelPage,
39 CourseFrontPage,
41}
42
43impl PageType {
44 pub fn of(in_chapter: bool, url_path: &str) -> Self {
46 match (in_chapter, url_path) {
47 (true, _) => PageType::CoursePage,
48 (false, "/") => PageType::CourseFrontPage,
49 (false, _) => PageType::TopLevelPage,
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct PageDocument {
57 pub page_id: Uuid,
58 pub page_type: PageType,
59 pub blocks: Vec<GutenbergBlock>,
60}
61
62impl PageDocument {
63 pub fn as_page_ref(&self) -> PageRef<'_> {
65 PageRef {
66 page_id: self.page_id,
67 page_type: self.page_type,
68 blocks: &self.blocks,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy)]
76pub struct PageRef<'a> {
77 pub page_id: Uuid,
78 pub page_type: PageType,
79 pub blocks: &'a [GutenbergBlock],
80}
81
82impl PageRef<'_> {
83 pub fn to_document(self) -> PageDocument {
85 PageDocument {
86 page_id: self.page_id,
87 page_type: self.page_type,
88 blocks: self.blocks.to_vec(),
89 }
90 }
91}
92
93impl<'a> From<PageRef<'a>> for DocumentRef<'a> {
94 fn from(page: PageRef<'a>) -> Self {
95 DocumentRef {
96 key: DocumentKey::Page(page.page_id),
97 page_type: page.page_type,
98 blocks: page.blocks,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum DocumentKey {
107 Page(Uuid),
108 Instructions {
110 list: InstructionsList,
111 page_id: Option<Uuid>,
112 },
113 ExerciseName {
115 exercise_id: Uuid,
116 page_id: Uuid,
117 },
118}
119
120impl DocumentKey {
121 pub fn page_id(self) -> Option<Uuid> {
123 match self {
124 DocumentKey::Page(page_id) | DocumentKey::ExerciseName { page_id, .. } => Some(page_id),
125 DocumentKey::Instructions { page_id, .. } => page_id,
126 }
127 }
128
129 pub(crate) fn block_match(
132 self,
133 block: &GutenbergBlock,
134 path: String,
135 preview: String,
136 ) -> BlockMatch {
137 let (block_id, path, block_name) = match self {
138 DocumentKey::Page(_) => (Some(block.client_id), path, block.name.clone()),
139 DocumentKey::Instructions { list, .. } => (
140 Some(block.client_id),
141 format!("{}{path}", list.path_prefix()),
142 block.name.clone(),
143 ),
144 DocumentKey::ExerciseName { .. } => {
145 (None, "/name".to_string(), "exercise name".to_string())
146 }
147 };
148 BlockMatch {
149 document: self,
150 block_id,
151 path,
152 block_name,
153 preview,
154 }
155 }
156}
157
158impl std::fmt::Display for DocumentKey {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 DocumentKey::Page(page_id) => write!(f, "page {page_id}"),
162 DocumentKey::Instructions { list, .. } => match list {
163 InstructionsList::Task {
164 exercise_id,
165 task_id,
166 } => write!(f, "exercise {exercise_id} task {task_id} instructions"),
167 InstructionsList::Review { exercise_id } => {
168 write!(f, "exercise {exercise_id} review instructions")
169 }
170 InstructionsList::CourseDefaultReview { .. } => {
171 write!(f, "the course default review instructions")
172 }
173 },
174 DocumentKey::ExerciseName { exercise_id, .. } => {
175 write!(f, "exercise {exercise_id} name")
176 }
177 }
178 }
179}
180
181impl Serialize for DocumentKey {
182 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
183 #[derive(Serialize)]
184 struct Location {
185 #[serde(skip_serializing_if = "Option::is_none")]
186 page_id: Option<Uuid>,
187 #[serde(flatten)]
188 instructions: Option<InstructionsLocation>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 exercise: Option<Uuid>,
191 }
192 let (instructions, exercise) = match self {
193 DocumentKey::Page(_) => (None, None),
194 DocumentKey::Instructions { list, .. } => (Some(list.location()), None),
195 DocumentKey::ExerciseName { exercise_id, .. } => (None, Some(*exercise_id)),
196 };
197 Location {
198 page_id: self.page_id(),
199 instructions,
200 exercise,
201 }
202 .serialize(serializer)
203 }
204}
205
206#[derive(Debug, Clone, Copy)]
208pub struct DocumentRef<'a> {
209 pub key: DocumentKey,
210 pub page_type: PageType,
211 pub blocks: &'a [GutenbergBlock],
212}
213
214impl DocumentRef<'_> {
215 pub fn to_document(self) -> Document {
217 Document {
218 key: self.key,
219 page_type: self.page_type,
220 blocks: self.blocks.to_vec(),
221 }
222 }
223}
224
225#[derive(Debug, Clone, PartialEq)]
227pub struct Document {
228 pub key: DocumentKey,
229 pub page_type: PageType,
230 pub blocks: Vec<GutenbergBlock>,
231}
232
233impl Document {
234 pub fn as_document_ref(&self) -> DocumentRef<'_> {
236 DocumentRef {
237 key: self.key,
238 page_type: self.page_type,
239 blocks: &self.blocks,
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize)]
246pub struct BlockMatch {
247 #[serde(flatten)]
248 pub document: DocumentKey,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub block_id: Option<Uuid>,
252 pub path: String,
255 pub block_name: String,
256 pub preview: String,
257}
258
259impl BlockMatch {
260 pub(crate) fn location(&self, count: Option<usize>) -> Value {
262 #[derive(Serialize)]
263 struct Location<'a> {
264 #[serde(flatten)]
265 document: DocumentKey,
266 path: &'a str,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 count: Option<usize>,
269 }
270 serde_json::to_value(Location {
271 document: self.document,
272 path: &self.path,
273 count,
274 })
275 .unwrap_or_default()
276 }
277}
278
279#[derive(Debug, Clone, Copy)]
281pub(crate) enum Anchor<L> {
282 Before(Uuid),
283 After(Uuid),
284 Start(L),
285 End(L),
286}
287
288impl<L: Copy> Anchor<L> {
289 pub(crate) fn target_id(self, owner: impl FnOnce(L) -> Option<Uuid>) -> Option<Uuid> {
291 match self {
292 Anchor::Before(id) | Anchor::After(id) => Some(id),
293 Anchor::Start(list) | Anchor::End(list) => owner(list),
294 }
295 }
296
297 pub(crate) fn insertion_point<P>(
300 self,
301 sibling: impl FnOnce(Uuid) -> Result<(P, usize), EditError>,
302 list_len: impl FnOnce(L) -> Result<(P, usize), EditError>,
303 ) -> Result<(P, usize), EditError> {
304 match self {
305 Anchor::Before(id) => sibling(id),
306 Anchor::After(id) => sibling(id).map(|(list, index)| (list, index + 1)),
307 Anchor::Start(list) => list_len(list).map(|(list, _)| (list, 0)),
308 Anchor::End(list) => list_len(list),
309 }
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Serialize)]
315pub struct CreatedIds {
316 pub op: usize,
317 pub ids: Vec<Uuid>,
318}
319
320pub(crate) struct CreatedIdLog<'a> {
322 generator: &'a dyn IdGenerator,
323 op: usize,
324 created: Vec<CreatedIds>,
325}
326
327impl<'a> CreatedIdLog<'a> {
328 pub(crate) fn new(generator: &'a dyn IdGenerator) -> Self {
330 Self {
331 generator,
332 op: 0,
333 created: Vec::new(),
334 }
335 }
336
337 pub(crate) fn set_op(&mut self, index: usize) {
339 self.op = index;
340 }
341
342 pub(crate) fn op(&self) -> usize {
344 self.op
345 }
346
347 pub(crate) fn new_id(&mut self) -> Uuid {
349 let id = self.generator.new_id();
350 self.record(id);
351 id
352 }
353
354 pub(crate) fn record(&mut self, id: Uuid) {
356 match self.created.last_mut() {
357 Some(last) if last.op == self.op => last.ids.push(id),
358 _ => self.created.push(CreatedIds {
359 op: self.op,
360 ids: vec![id],
361 }),
362 }
363 }
364
365 pub(crate) fn generator(&self) -> &'a dyn IdGenerator {
367 self.generator
368 }
369
370 pub(crate) fn into_created(self) -> Vec<CreatedIds> {
372 self.created
373 }
374}
375
376pub(crate) fn non_empty_string(value: Option<&Value>, what: &str) -> Result<String, EditError> {
379 match value {
380 Some(Value::String(s)) if !s.trim().is_empty() => Ok(s.clone()),
381 _ => Err(EditError::new(
382 EditErrorCode::InvalidValue,
383 format!("{what} is a non-empty string"),
384 )),
385 }
386}
387
388pub(crate) fn value_items<'v>(value: &'v Value, noun: &str) -> Result<Vec<&'v Value>, EditError> {
391 match value {
392 Value::Array(items) if !items.is_empty() => Ok(items.iter().collect()),
393 Value::Object(_) => Ok(vec![value]),
394 Value::Array(_) => Err(EditError::new(
395 EditErrorCode::InvalidValue,
396 "add got an empty list",
397 )),
398 _ => Err(EditError::new(
399 EditErrorCode::InvalidValue,
400 format!("add takes {noun} object or a list of them"),
401 )),
402 }
403}
404
405pub(crate) fn new_item_objects<'v>(
408 value: &'v Value,
409 noun: &str,
410 shape: &str,
411 fields: &[&str],
412) -> Result<Vec<&'v Map<String, Value>>, EditError> {
413 value_items(value, noun)?
414 .into_iter()
415 .map(|item| {
416 let object = item
417 .as_object()
418 .ok_or_else(|| EditError::new(EditErrorCode::InvalidValue, shape))?;
419 reject_unknown_keys(object, fields, noun)?;
420 Ok(object)
421 })
422 .collect()
423}
424
425pub(crate) fn reject_unknown_keys(
428 object: &Map<String, Value>,
429 allowed: &[&str],
430 noun: &str,
431) -> Result<(), EditError> {
432 let unknown: Vec<&String> = object
433 .keys()
434 .filter(|key| !allowed.contains(&key.as_str()))
435 .collect();
436 if unknown.is_empty() {
437 return Ok(());
438 }
439 Err(EditError::new(
440 EditErrorCode::UnknownField,
441 format!("{noun} has no field {}", op::quote_list(&unknown)),
442 )
443 .with_fix(format!("fields: {}", allowed.join(", "))))
444}