Skip to main content

ucm_engine/
operation.rs

1//! Operations that can be applied to documents.
2
3use serde::{Deserialize, Serialize};
4use ucm_core::{BlockId, Content, EdgeType};
5
6/// Target for move operations
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum MoveTarget {
9    /// Move to a parent at optional index
10    ToParent {
11        parent_id: BlockId,
12        index: Option<usize>,
13    },
14    /// Move before a sibling
15    Before { sibling_id: BlockId },
16    /// Move after a sibling
17    After { sibling_id: BlockId },
18}
19
20/// Operations that can be applied to a document
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum Operation {
23    /// Edit content at a path
24    Edit {
25        block_id: BlockId,
26        path: String,
27        value: serde_json::Value,
28        operator: EditOperator,
29    },
30
31    /// Move a block to a new parent (legacy)
32    Move {
33        block_id: BlockId,
34        new_parent: BlockId,
35        index: Option<usize>,
36    },
37
38    /// Move a block with flexible target
39    MoveToTarget {
40        block_id: BlockId,
41        target: MoveTarget,
42    },
43
44    /// Append a new block
45    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 a block
55    Delete {
56        block_id: BlockId,
57        cascade: bool,
58        preserve_children: bool,
59    },
60
61    /// Prune unreachable blocks
62    Prune { condition: Option<PruneCondition> },
63
64    /// Add an edge
65    Link {
66        source: BlockId,
67        edge_type: EdgeType,
68        target: BlockId,
69        metadata: Option<serde_json::Value>,
70    },
71
72    /// Remove an edge
73    Unlink {
74        source: BlockId,
75        edge_type: EdgeType,
76        target: BlockId,
77    },
78
79    /// Create a snapshot
80    CreateSnapshot {
81        name: String,
82        description: Option<String>,
83    },
84
85    /// Restore a snapshot
86    RestoreSnapshot { name: String },
87
88    /// Write markdown content to a section, replacing all children
89    WriteSection {
90        /// Target section (heading block) to write to
91        section_id: BlockId,
92        /// New markdown content to parse and insert
93        markdown: String,
94        /// Adjust heading levels relative to this base (e.g., 2 means top-level becomes H2)
95        base_heading_level: Option<usize>,
96    },
97}
98
99/// Edit operators
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101pub enum EditOperator {
102    /// Set value (=)
103    Set,
104    /// Append to string/array (+=)
105    Append,
106    /// Remove from string/array (-=)
107    Remove,
108    /// Increment number (++)
109    Increment,
110    /// Decrement number (--)
111    Decrement,
112}
113
114/// Prune condition
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub enum PruneCondition {
117    Unreachable,
118    TagContains(String),
119    Custom(String),
120}
121
122/// Result of an operation
123#[derive(Debug, Clone)]
124pub struct OperationResult {
125    /// Whether the operation succeeded
126    pub success: bool,
127    /// Affected block IDs
128    pub affected_blocks: Vec<BlockId>,
129    /// Any warnings generated
130    pub warnings: Vec<String>,
131    /// Error message if failed
132    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    /// Get a description of the operation for logging
162    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}