Skip to main content

headless_lms_mcp/engine/read/
mod.rs

1//! Read selectors shared by `get_page_content`, `get_course_structure` and `read_back`.
2//!
3//! Results carry ids and ready-made paths only; offsets exist in requests, and a paged read
4//! continues from an id-anchored `next` selector.
5
6use std::collections::HashMap;
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use uuid::Uuid;
12
13use super::error::{EditError, EditErrorCode};
14use crate::diff::ChangeKind;
15
16mod page;
17mod structure;
18mod window;
19
20pub(crate) use page::block_preview;
21pub use page::read_page;
22pub use structure::read_structure;
23
24/// What to read. All fields are optional; an empty selector is an outline of the whole page or course.
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct Selector {
28    /// A block or entity (`/p1`), a field (`/p1/attributes/content`, `/ch3/deadline`) or a children list (`/box1/innerBlocks`, `/ch3/pages`); `""` is the page's top level.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub path: Option<String>,
31    /// Last sibling (inclusive) of a range starting at `path`.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub through: Option<String>,
34    /// Siblings to include on each side of `path`.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub around: Option<u32>,
37    /// Start of a window over a children list.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub offset: Option<u32>,
40    /// Most items to return, from `offset` or from the block at `path`.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub limit: Option<u32>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub mode: Option<ReadMode>,
45    /// `staged` (default) includes changes waiting to be published; `published` is what students see.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub state: Option<ReadState>,
48}
49
50/// How much of each item a read returns.
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum ReadMode {
54    /// Ids, types and short previews.
55    #[default]
56    Outline,
57    /// The stored JSON.
58    Raw,
59}
60
61/// Which state of the course a read sees.
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
63#[serde(rename_all = "snake_case")]
64pub enum ReadState {
65    #[default]
66    Staged,
67    Published,
68}
69
70/// Why a read returned fewer items than its selector covers.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "snake_case")]
73pub enum StopReason {
74    Limit,
75    Budget,
76}
77
78/// Read settings the caller derives from the client and the staged changeset.
79#[derive(Debug, Clone, Copy)]
80pub struct ReadOptions<'a> {
81    /// Serialized size the returned units may use; whole units only.
82    pub budget_bytes: usize,
83    /// How staged blocks or entities differ from the published ones, to mark them; `None` marks
84    /// nothing.
85    pub pending: Option<&'a HashMap<Uuid, ChangeKind>>,
86}
87
88/// One selector's result.
89#[derive(Debug, Clone, PartialEq, Serialize)]
90pub struct ReadResult {
91    pub selector: Selector,
92    /// Length of the children list a window or range was taken from.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub total: Option<usize>,
95    /// The children list holding the returned items, when it isn't the top level.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub parent: Option<String>,
98    #[serde(skip_serializing_if = "Vec::is_empty")]
99    pub items: Vec<Value>,
100    /// A single field's value, for field paths.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub value: Option<Value>,
103    pub has_more: bool,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub next: Option<Selector>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub stopped_by: Option<StopReason>,
108}
109
110impl ReadResult {
111    /// The result of reading one item or field at `selector`, with no paging.
112    pub(crate) fn single(selector: &Selector, items: Vec<Value>) -> Self {
113        Self {
114            selector: selector.clone(),
115            total: None,
116            parent: None,
117            items,
118            value: None,
119            has_more: false,
120            next: None,
121            stopped_by: None,
122        }
123    }
124}
125
126/// Rejects selector field combinations that have no meaning.
127pub(crate) fn check_selector(selector: &Selector) -> Result<(), EditError> {
128    let ranged = [
129        selector.through.is_some(),
130        selector.around.is_some(),
131        selector.offset.is_some(),
132    ];
133    if ranged.iter().filter(|b| **b).count() > 1 {
134        return Err(EditError::new(
135            EditErrorCode::InvalidValue,
136            "use one of through, around and offset per selector",
137        ));
138    }
139    if selector.limit == Some(0) {
140        return Err(EditError::new(
141            EditErrorCode::InvalidValue,
142            "limit must be at least 1",
143        ));
144    }
145    Ok(())
146}
147
148/// The result of reading one field.
149pub(crate) fn field_result(selector: &Selector, value: Value) -> ReadResult {
150    ReadResult {
151        value: Some(value),
152        ..ReadResult::single(selector, Vec::new())
153    }
154}