1use serde::{Deserialize, Serialize};
4use ucm_core::{BlockId, Content, EdgeType};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum MoveTarget {
9 ToParent {
11 parent_id: BlockId,
12 index: Option<usize>,
13 },
14 Before { sibling_id: BlockId },
16 After { sibling_id: BlockId },
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum Operation {
23 Edit {
25 block_id: BlockId,
26 path: String,
27 value: serde_json::Value,
28 operator: EditOperator,
29 },
30
31 Move {
33 block_id: BlockId,
34 new_parent: BlockId,
35 index: Option<usize>,
36 },
37
38 MoveToTarget {
40 block_id: BlockId,
41 target: MoveTarget,
42 },
43
44 Append {
46 parent_id: BlockId,
47 content: Content,
48 label: Option<String>,
49 tags: Vec<String>,
50 semantic_role: Option<String>,
51 index: Option<usize>,
52 },
53
54 Delete {
56 block_id: BlockId,
57 cascade: bool,
58 preserve_children: bool,
59 },
60
61 Prune { condition: Option<PruneCondition> },
63
64 Link {
66 source: BlockId,
67 edge_type: EdgeType,
68 target: BlockId,
69 metadata: Option<serde_json::Value>,
70 },
71
72 Unlink {
74 source: BlockId,
75 edge_type: EdgeType,
76 target: BlockId,
77 },
78
79 CreateSnapshot {
81 name: String,
82 description: Option<String>,
83 },
84
85 RestoreSnapshot { name: String },
87
88 WriteSection {
90 section_id: BlockId,
92 markdown: String,
94 base_heading_level: Option<usize>,
96 },
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101pub enum EditOperator {
102 Set,
104 Append,
106 Remove,
108 Increment,
110 Decrement,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub enum PruneCondition {
117 Unreachable,
118 TagContains(String),
119 Custom(String),
120}
121
122#[derive(Debug, Clone)]
124pub struct OperationResult {
125 pub success: bool,
127 pub affected_blocks: Vec<BlockId>,
129 pub warnings: Vec<String>,
131 pub error: Option<String>,
133}
134
135impl OperationResult {
136 pub fn success(affected: Vec<BlockId>) -> Self {
137 Self {
138 success: true,
139 affected_blocks: affected,
140 warnings: Vec::new(),
141 error: None,
142 }
143 }
144
145 pub fn failure(error: impl Into<String>) -> Self {
146 Self {
147 success: false,
148 affected_blocks: Vec::new(),
149 warnings: Vec::new(),
150 error: Some(error.into()),
151 }
152 }
153
154 pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
155 self.warnings.push(warning.into());
156 self
157 }
158}
159
160impl Operation {
161 pub fn description(&self) -> String {
163 match self {
164 Operation::Edit { block_id, path, .. } => {
165 format!("EDIT {} SET {}", block_id, path)
166 }
167 Operation::Move {
168 block_id,
169 new_parent,
170 ..
171 } => {
172 format!("MOVE {} TO {}", block_id, new_parent)
173 }
174 Operation::MoveToTarget { block_id, target } => match target {
175 MoveTarget::ToParent { parent_id, index } => {
176 if let Some(idx) = index {
177 format!("MOVE {} TO {} AT {}", block_id, parent_id, idx)
178 } else {
179 format!("MOVE {} TO {}", block_id, parent_id)
180 }
181 }
182 MoveTarget::Before { sibling_id } => {
183 format!("MOVE {} BEFORE {}", block_id, sibling_id)
184 }
185 MoveTarget::After { sibling_id } => {
186 format!("MOVE {} AFTER {}", block_id, sibling_id)
187 }
188 },
189 Operation::Append { parent_id, .. } => {
190 format!("APPEND to {}", parent_id)
191 }
192 Operation::Delete {
193 block_id, cascade, ..
194 } => {
195 if *cascade {
196 format!("DELETE {} CASCADE", block_id)
197 } else {
198 format!("DELETE {}", block_id)
199 }
200 }
201 Operation::Prune { condition } => match condition {
202 Some(PruneCondition::Unreachable) | None => "PRUNE UNREACHABLE".to_string(),
203 Some(PruneCondition::TagContains(tag)) => format!("PRUNE WHERE tag={}", tag),
204 Some(PruneCondition::Custom(c)) => format!("PRUNE WHERE {}", c),
205 },
206 Operation::Link {
207 source,
208 edge_type,
209 target,
210 ..
211 } => {
212 format!("LINK {} {} {}", source, edge_type.as_str(), target)
213 }
214 Operation::Unlink {
215 source,
216 edge_type,
217 target,
218 } => {
219 format!("UNLINK {} {} {}", source, edge_type.as_str(), target)
220 }
221 Operation::CreateSnapshot { name, .. } => {
222 format!("SNAPSHOT CREATE {}", name)
223 }
224 Operation::RestoreSnapshot { name } => {
225 format!("SNAPSHOT RESTORE {}", name)
226 }
227 Operation::WriteSection {
228 section_id,
229 base_heading_level,
230 ..
231 } => {
232 if let Some(level) = base_heading_level {
233 format!("WRITE_SECTION {} BASE_LEVEL {}", section_id, level)
234 } else {
235 format!("WRITE_SECTION {}", section_id)
236 }
237 }
238 }
239 }
240}