headless_lms_mcp/engine/read/
mod.rs1use 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#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct Selector {
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub path: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub through: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub around: Option<u32>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub offset: Option<u32>,
40 #[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 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub state: Option<ReadState>,
48}
49
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum ReadMode {
54 #[default]
56 Outline,
57 Raw,
59}
60
61#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "snake_case")]
73pub enum StopReason {
74 Limit,
75 Budget,
76}
77
78#[derive(Debug, Clone, Copy)]
80pub struct ReadOptions<'a> {
81 pub budget_bytes: usize,
83 pub pending: Option<&'a HashMap<Uuid, ChangeKind>>,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize)]
90pub struct ReadResult {
91 pub selector: Selector,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub total: Option<usize>,
95 #[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 #[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 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
126pub(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
148pub(crate) fn field_result(selector: &Selector, value: Value) -> ReadResult {
150 ReadResult {
151 value: Some(value),
152 ..ReadResult::single(selector, Vec::new())
153 }
154}