Skip to main content

headless_lms_mcp/engine/page_content/
pointer.rs

1//! Writes into a block's attributes at a JSON Pointer.
2
3use serde_json::{Map, Value};
4
5use crate::engine::error::{EditError, EditErrorCode};
6use crate::engine::path::unescape_segment;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub(super) enum PointerWrite {
10    Add,
11    Replace,
12}
13
14/// Runs `edit` on the value holding the last segment of `pointer` (non-empty) inside
15/// `attributes`, with that segment unescaped.
16fn with_parent<T>(
17    attributes: &mut Map<String, Value>,
18    pointer: &str,
19    edit: impl FnOnce(&mut Value, &str) -> Result<T, EditError>,
20) -> Result<T, EditError> {
21    let (parent, last) = pointer.rsplit_once('/').ok_or_else(|| {
22        EditError::new(
23            EditErrorCode::InvalidPath,
24            format!("attribute path {pointer:?} doesn't start with /"),
25        )
26    })?;
27    let mut holder = Value::Object(std::mem::take(attributes));
28    let result = match holder.pointer_mut(parent) {
29        Some(value) => edit(value, &unescape_segment(last)),
30        None => Err(EditError::new(
31            EditErrorCode::NotFound,
32            format!("attributes{parent} isn't set"),
33        )
34        .with_fix("replace the enclosing attribute with a whole object instead")),
35    };
36    if let Value::Object(object) = holder {
37        *attributes = object;
38    }
39    result
40}
41
42pub(super) fn pointer_write(
43    attributes: &mut Map<String, Value>,
44    pointer: &str,
45    value: Value,
46    mode: PointerWrite,
47) -> Result<(), EditError> {
48    with_parent(attributes, pointer, |parent, last| match parent {
49        Value::Object(object) => {
50            object.insert(last.to_string(), value);
51            Ok(())
52        }
53        Value::Array(array) => {
54            if last == "-" {
55                if mode == PointerWrite::Add {
56                    array.push(value);
57                    return Ok(());
58                }
59                return Err(EditError::new(
60                    EditErrorCode::InvalidPath,
61                    "\"-\" appends, so it only works with add",
62                ));
63            }
64            let index: usize = last.parse().map_err(|_| {
65                EditError::new(
66                    EditErrorCode::InvalidPath,
67                    format!("\"{last}\" isn't an array index"),
68                )
69            })?;
70            match mode {
71                PointerWrite::Replace if index < array.len() => {
72                    array[index] = value;
73                    Ok(())
74                }
75                PointerWrite::Add if index <= array.len() => {
76                    array.insert(index, value);
77                    Ok(())
78                }
79                _ => Err(EditError::new(
80                    EditErrorCode::NotFound,
81                    format!(
82                        "index {index} is past the end of an array of {}",
83                        array.len()
84                    ),
85                )),
86            }
87        }
88        _ => Err(EditError::new(
89            EditErrorCode::InvalidPath,
90            format!("attributes{pointer} goes into a value that isn't an object or array"),
91        )),
92    })
93}
94
95pub(super) fn pointer_remove(
96    attributes: &mut Map<String, Value>,
97    pointer: &str,
98) -> Result<(), EditError> {
99    with_parent(attributes, pointer, |parent, last| {
100        let removed = match parent {
101            Value::Object(object) => object.remove(last).is_some(),
102            Value::Array(array) => match last.parse::<usize>() {
103                Ok(index) if index < array.len() => {
104                    array.remove(index);
105                    true
106                }
107                _ => false,
108            },
109            _ => false,
110        };
111        if removed {
112            Ok(())
113        } else {
114            Err(EditError::new(
115                EditErrorCode::NotFound,
116                format!("attributes{pointer} isn't set"),
117            ))
118        }
119    })
120}