Skip to main content

xml_3dm/merge/
edit_log.rs

1//! Edit operation logging for the merge algorithm.
2//!
3//! This module tracks the edit operations performed during the merge process,
4//! including inserts, updates, copies, moves, and deletes.
5
6use std::io::Write;
7
8use crate::node::NodeRef;
9
10/// Types of edit operations.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum EditType {
13    /// Node was inserted (new content).
14    Insert,
15    /// Node content was updated.
16    Update,
17    /// Node was copied from base.
18    Copy,
19    /// Node was moved from original location.
20    Move,
21    /// Node was deleted.
22    Delete,
23}
24
25impl EditType {
26    /// Returns the XML tag name for this edit type.
27    pub fn tag_name(&self) -> &'static str {
28        match self {
29            EditType::Insert => "insert",
30            EditType::Update => "update",
31            EditType::Copy => "copy",
32            EditType::Move => "move",
33            EditType::Delete => "delete",
34        }
35    }
36}
37
38/// A single edit operation entry.
39#[derive(Debug, Clone)]
40pub struct EditEntry {
41    /// The type of edit operation.
42    pub edit_type: EditType,
43    /// The node being edited (branch node for most ops, base for delete).
44    pub node: Option<NodeRef>,
45    /// Whether the node is from the left tree.
46    pub is_left_tree: bool,
47}
48
49/// Log of edit operations performed during merge.
50///
51/// The edit log supports checkpoints for transactional semantics -
52/// operations can be rolled back if a merge path fails.
53#[derive(Debug, Default)]
54pub struct EditLog {
55    /// List of edit operations.
56    edits: Vec<EditEntry>,
57    /// Stack of checkpoint positions for rollback.
58    checkpoints: Vec<usize>,
59}
60
61impl EditLog {
62    /// Creates a new empty edit log.
63    pub fn new() -> Self {
64        EditLog {
65            edits: Vec::new(),
66            checkpoints: Vec::new(),
67        }
68    }
69
70    /// Records an insert operation.
71    pub fn insert(&mut self, node: Option<NodeRef>) {
72        let is_left = node
73            .as_ref()
74            .map(crate::node::BranchNode::is_left_tree)
75            .unwrap_or(true);
76
77        self.edits.push(EditEntry {
78            edit_type: EditType::Insert,
79            node,
80            is_left_tree: is_left,
81        });
82    }
83
84    /// Records an update operation.
85    pub fn update(&mut self, node: Option<NodeRef>) {
86        let is_left = node
87            .as_ref()
88            .map(crate::node::BranchNode::is_left_tree)
89            .unwrap_or(true);
90
91        self.edits.push(EditEntry {
92            edit_type: EditType::Update,
93            node,
94            is_left_tree: is_left,
95        });
96    }
97
98    /// Records a copy operation.
99    pub fn copy(&mut self, node: Option<NodeRef>) {
100        let is_left = node
101            .as_ref()
102            .map(crate::node::BranchNode::is_left_tree)
103            .unwrap_or(true);
104
105        self.edits.push(EditEntry {
106            edit_type: EditType::Copy,
107            node,
108            is_left_tree: is_left,
109        });
110    }
111
112    /// Records a move operation.
113    pub fn r#move(&mut self, node: Option<NodeRef>) {
114        let is_left = node
115            .as_ref()
116            .map(crate::node::BranchNode::is_left_tree)
117            .unwrap_or(true);
118
119        self.edits.push(EditEntry {
120            edit_type: EditType::Move,
121            node,
122            is_left_tree: is_left,
123        });
124    }
125
126    /// Records a delete operation.
127    pub fn delete(&mut self, node: Option<NodeRef>) {
128        // For delete, node is a base node, so is_left_tree doesn't apply
129        self.edits.push(EditEntry {
130            edit_type: EditType::Delete,
131            node,
132            is_left_tree: true,
133        });
134    }
135
136    /// Creates a checkpoint for potential rollback.
137    pub fn checkpoint(&mut self) {
138        self.checkpoints.push(self.edits.len());
139    }
140
141    /// Rolls back to the last checkpoint, discarding operations since then.
142    pub fn rewind(&mut self) {
143        if let Some(pos) = self.checkpoints.pop() {
144            self.edits.truncate(pos);
145        }
146    }
147
148    /// Commits the operations since the last checkpoint.
149    pub fn commit(&mut self) {
150        self.checkpoints.pop();
151    }
152
153    /// Returns the number of edit operations.
154    pub fn edit_count(&self) -> usize {
155        self.edits.len()
156    }
157
158    /// Returns a reference to the edit operations.
159    pub fn edits(&self) -> &[EditEntry] {
160        &self.edits
161    }
162
163    /// Counts operations by type.
164    pub fn count_by_type(&self, edit_type: EditType) -> usize {
165        self.edits
166            .iter()
167            .filter(|e| e.edit_type == edit_type)
168            .count()
169    }
170
171    /// Writes the edit log as XML.
172    pub fn write_xml<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
173        writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
174        writeln!(writer, "<edits>")?;
175
176        for entry in &self.edits {
177            self.write_entry(writer, entry)?;
178        }
179
180        writeln!(writer, "</edits>")?;
181        Ok(())
182    }
183
184    /// Writes a single edit entry as XML.
185    fn write_entry<W: Write>(&self, writer: &mut W, entry: &EditEntry) -> std::io::Result<()> {
186        let tag = entry.edit_type.tag_name();
187        let tree = if entry.is_left_tree {
188            "branch1"
189        } else {
190            "branch2"
191        };
192
193        write!(writer, "  <{}", tag)?;
194
195        if let Some(node) = &entry.node {
196            let path = get_node_path(node);
197            write!(writer, " path=\"{}\"", escape_xml(&path))?;
198
199            // For update/copy/move, include base source
200            if entry.edit_type != EditType::Insert && entry.edit_type != EditType::Delete {
201                if let Some(base) = crate::node::BranchNode::base_match(node) {
202                    let base_path = get_node_path(&base);
203                    write!(writer, " src=\"{}\"", escape_xml(&base_path))?;
204                }
205            }
206        }
207
208        write!(writer, " originTree=\"{}\"", tree)?;
209        writeln!(writer, " />")?;
210
211        Ok(())
212    }
213}
214
215/// Escapes special characters in XML attribute values.
216fn escape_xml(s: &str) -> String {
217    s.replace('&', "&amp;")
218        .replace('<', "&lt;")
219        .replace('>', "&gt;")
220        .replace('"', "&quot;")
221}
222
223/// Gets a path string for a node.
224fn get_node_path(node: &NodeRef) -> String {
225    use crate::node::XmlContent;
226
227    let mut parts = Vec::new();
228    let mut current = Some(node.clone());
229
230    while let Some(n) = current {
231        let borrowed = n.borrow();
232        let name = match borrowed.content() {
233            Some(XmlContent::Element(e)) => e.qname().to_string(),
234            Some(XmlContent::Text(_)) => "#text".to_string(),
235            Some(XmlContent::Comment(_)) => "#comment".to_string(),
236            Some(XmlContent::ProcessingInstruction(pi)) => format!("#pi-{}", pi.target()),
237            None => "#node".to_string(),
238        };
239
240        let pos = borrowed.child_pos();
241        if pos >= 0 {
242            parts.push(format!("{}[{}]", name, pos));
243        } else {
244            parts.push(name);
245        }
246
247        current = borrowed.parent().upgrade();
248    }
249
250    parts.reverse();
251    if parts.is_empty() {
252        "/".to_string()
253    } else {
254        format!("/{}", parts.join("/"))
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_edit_type_tag_names() {
264        assert_eq!(EditType::Insert.tag_name(), "insert");
265        assert_eq!(EditType::Update.tag_name(), "update");
266        assert_eq!(EditType::Copy.tag_name(), "copy");
267        assert_eq!(EditType::Move.tag_name(), "move");
268        assert_eq!(EditType::Delete.tag_name(), "delete");
269    }
270
271    #[test]
272    fn test_edit_log_empty() {
273        let log = EditLog::new();
274        assert_eq!(log.edit_count(), 0);
275    }
276
277    #[test]
278    fn test_edit_log_operations() {
279        let mut log = EditLog::new();
280
281        log.insert(None);
282        log.update(None);
283        log.copy(None);
284        log.r#move(None);
285        log.delete(None);
286
287        assert_eq!(log.edit_count(), 5);
288        assert_eq!(log.count_by_type(EditType::Insert), 1);
289        assert_eq!(log.count_by_type(EditType::Update), 1);
290        assert_eq!(log.count_by_type(EditType::Copy), 1);
291        assert_eq!(log.count_by_type(EditType::Move), 1);
292        assert_eq!(log.count_by_type(EditType::Delete), 1);
293    }
294
295    #[test]
296    fn test_edit_log_checkpoint_rewind() {
297        let mut log = EditLog::new();
298
299        log.insert(None);
300        log.insert(None);
301        assert_eq!(log.edit_count(), 2);
302
303        log.checkpoint();
304        log.insert(None);
305        log.insert(None);
306        assert_eq!(log.edit_count(), 4);
307
308        log.rewind();
309        assert_eq!(log.edit_count(), 2);
310    }
311
312    #[test]
313    fn test_edit_log_checkpoint_commit() {
314        let mut log = EditLog::new();
315
316        log.insert(None);
317        log.checkpoint();
318        log.insert(None);
319        log.commit();
320        log.insert(None);
321
322        assert_eq!(log.edit_count(), 3);
323    }
324
325    #[test]
326    fn test_nested_checkpoints() {
327        let mut log = EditLog::new();
328
329        log.insert(None); // 1
330        log.checkpoint();
331        log.insert(None); // 2
332        log.checkpoint();
333        log.insert(None); // 3
334        assert_eq!(log.edit_count(), 3);
335
336        log.rewind(); // Back to 2
337        assert_eq!(log.edit_count(), 2);
338
339        log.rewind(); // Back to 1
340        assert_eq!(log.edit_count(), 1);
341    }
342}