codeprism_core/patch/
mod.rs

1//! AST patch generation and application
2
3use crate::ast::{Edge, Node};
4use serde::{Deserialize, Serialize};
5
6/// AST patch containing changes to apply
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AstPatch {
9    /// Repository ID
10    pub repo: String,
11    /// Commit SHA
12    pub commit: String,
13    /// Nodes to add
14    pub nodes_add: Vec<Node>,
15    /// Edges to add
16    pub edges_add: Vec<Edge>,
17    /// Node IDs to delete
18    pub nodes_delete: Vec<String>,
19    /// Edge IDs to delete
20    pub edges_delete: Vec<String>,
21    /// Timestamp in milliseconds
22    pub timestamp_ms: i64,
23}
24
25impl AstPatch {
26    /// Create a new empty patch
27    pub fn new(repo: String, commit: String) -> Self {
28        Self {
29            repo,
30            commit,
31            nodes_add: Vec::new(),
32            edges_add: Vec::new(),
33            nodes_delete: Vec::new(),
34            edges_delete: Vec::new(),
35            timestamp_ms: chrono::Utc::now().timestamp_millis(),
36        }
37    }
38
39    /// Check if the patch is empty
40    pub fn is_empty(&self) -> bool {
41        self.nodes_add.is_empty()
42            && self.edges_add.is_empty()
43            && self.nodes_delete.is_empty()
44            && self.edges_delete.is_empty()
45    }
46
47    /// Get the total number of operations in the patch
48    pub fn operation_count(&self) -> usize {
49        self.nodes_add.len()
50            + self.edges_add.len()
51            + self.nodes_delete.len()
52            + self.edges_delete.len()
53    }
54
55    /// Merge another patch into this one
56    pub fn merge(&mut self, other: AstPatch) {
57        self.nodes_add.extend(other.nodes_add);
58        self.edges_add.extend(other.edges_add);
59        self.nodes_delete.extend(other.nodes_delete);
60        self.edges_delete.extend(other.edges_delete);
61        // Update timestamp to the latest
62        if other.timestamp_ms > self.timestamp_ms {
63            self.timestamp_ms = other.timestamp_ms;
64        }
65    }
66}
67
68/// Builder for creating AST patches
69pub struct PatchBuilder {
70    patch: AstPatch,
71}
72
73impl PatchBuilder {
74    /// Create a new patch builder
75    pub fn new(repo: String, commit: String) -> Self {
76        Self {
77            patch: AstPatch::new(repo, commit),
78        }
79    }
80
81    /// Add a node to the patch
82    pub fn add_node(mut self, node: Node) -> Self {
83        self.patch.nodes_add.push(node);
84        self
85    }
86
87    /// Add multiple nodes to the patch
88    pub fn add_nodes(mut self, nodes: Vec<Node>) -> Self {
89        self.patch.nodes_add.extend(nodes);
90        self
91    }
92
93    /// Add an edge to the patch
94    pub fn add_edge(mut self, edge: Edge) -> Self {
95        self.patch.edges_add.push(edge);
96        self
97    }
98
99    /// Add multiple edges to the patch
100    pub fn add_edges(mut self, edges: Vec<Edge>) -> Self {
101        self.patch.edges_add.extend(edges);
102        self
103    }
104
105    /// Delete a node
106    pub fn delete_node(mut self, node_id: String) -> Self {
107        self.patch.nodes_delete.push(node_id);
108        self
109    }
110
111    /// Delete multiple nodes
112    pub fn delete_nodes(mut self, node_ids: Vec<String>) -> Self {
113        self.patch.nodes_delete.extend(node_ids);
114        self
115    }
116
117    /// Delete an edge
118    pub fn delete_edge(mut self, edge_id: String) -> Self {
119        self.patch.edges_delete.push(edge_id);
120        self
121    }
122
123    /// Delete multiple edges
124    pub fn delete_edges(mut self, edge_ids: Vec<String>) -> Self {
125        self.patch.edges_delete.extend(edge_ids);
126        self
127    }
128
129    /// Set custom timestamp
130    pub fn with_timestamp(mut self, timestamp_ms: i64) -> Self {
131        self.patch.timestamp_ms = timestamp_ms;
132        self
133    }
134
135    /// Build the patch
136    pub fn build(self) -> AstPatch {
137        self.patch
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::ast::{EdgeKind, Language, NodeKind, Span};
145    use std::path::PathBuf;
146
147    fn create_test_node(name: &str) -> Node {
148        let span = Span::new(0, 10, 1, 1, 1, 11);
149        Node::new(
150            "test_repo",
151            NodeKind::Function,
152            name.to_string(),
153            Language::JavaScript,
154            PathBuf::from("test.js"),
155            span,
156        )
157    }
158
159    fn create_test_edge(source: &Node, target: &Node) -> Edge {
160        Edge::new(source.id, target.id, EdgeKind::Calls)
161    }
162
163    #[test]
164    fn test_patch_creation() {
165        let patch = AstPatch::new("test_repo".to_string(), "abc123".to_string());
166        assert_eq!(patch.repo, "test_repo");
167        assert_eq!(patch.commit, "abc123");
168        assert!(patch.is_empty(), "New patch should be empty");
169        assert_eq!(patch.operation_count(), 0);
170        assert!(patch.timestamp_ms > 0, "Patch should have valid timestamp");
171    }
172
173    #[test]
174    fn test_patch_builder_basic() {
175        let node1 = create_test_node("func1");
176        let node2 = create_test_node("func2");
177        let edge = create_test_edge(&node1, &node2);
178
179        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
180            .add_node(node1.clone())
181            .add_node(node2.clone())
182            .add_edge(edge.clone())
183            .delete_node("old_node_id".to_string())
184            .delete_edge("old_edge_id".to_string())
185            .build();
186
187        assert_eq!(patch.nodes_add.len(), 2, "Should have 2 nodes to add");
188        assert_eq!(patch.edges_add.len(), 1, "Should have 1 edge to add");
189        assert_eq!(patch.nodes_delete.len(), 1, "Should have 1 node to delete");
190        assert_eq!(patch.edges_delete.len(), 1, "Should have 1 edge to delete");
191        assert_eq!(patch.operation_count(), 5, "Total operations should be 5");
192        assert!(
193            !patch.is_empty(),
194            "Patch with operations should not be empty"
195        );
196
197        // Verify actual content of operations
198        assert!(
199            patch.nodes_add.iter().any(|n| n.kind == NodeKind::Function),
200            "Should add function node"
201        );
202        assert!(
203            patch.nodes_add.iter().all(|n| n.kind == NodeKind::Function),
204            "Should add function nodes"
205        );
206        // Verify operations contain expected types
207        assert!(
208            !patch.nodes_delete.is_empty(),
209            "Should have nodes to delete"
210        );
211    }
212
213    #[test]
214    fn test_patch_builder_batch_operations() {
215        let nodes = vec![
216            create_test_node("func1"),
217            create_test_node("func2"),
218            create_test_node("func3"),
219        ];
220        let edges = vec![
221            create_test_edge(&nodes[0], &nodes[1]),
222            create_test_edge(&nodes[1], &nodes[2]),
223        ];
224
225        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
226            .add_nodes(nodes.clone())
227            .add_edges(edges.clone())
228            .delete_nodes(vec!["id1".to_string(), "id2".to_string()])
229            .delete_edges(vec!["edge1".to_string(), "edge2".to_string()])
230            .build();
231
232        assert_eq!(patch.nodes_add.len(), 3, "Should have 3 items");
233        assert_eq!(patch.edges_add.len(), 2, "Should have 2 items");
234        assert_eq!(patch.nodes_delete.len(), 2, "Should have 2 items");
235        assert_eq!(patch.edges_delete.len(), 2, "Should have 2 items");
236        assert_eq!(patch.operation_count(), 9);
237    }
238
239    #[test]
240    fn test_patch_serialization() {
241        let node = create_test_node("test_func");
242        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
243            .add_node(node)
244            .with_timestamp(1234567890)
245            .build();
246
247        // Test JSON serialization
248        let json = serde_json::to_string(&patch).unwrap();
249        let deserialized: AstPatch = serde_json::from_str(&json).unwrap();
250
251        assert_eq!(deserialized.repo, patch.repo);
252        assert_eq!(deserialized.commit, patch.commit);
253        assert_eq!(deserialized.nodes_add.len(), patch.nodes_add.len());
254        assert_eq!(deserialized.timestamp_ms, 1234567890);
255    }
256
257    #[test]
258    fn test_patch_merge() {
259        let node1 = create_test_node("func1");
260        let node2 = create_test_node("func2");
261        let edge = create_test_edge(&node1, &node2);
262
263        let mut patch1 = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
264            .add_node(node1.clone())
265            .delete_node("old1".to_string())
266            .with_timestamp(1000)
267            .build();
268
269        let patch2 = PatchBuilder::new("test_repo".to_string(), "def456".to_string())
270            .add_node(node2.clone())
271            .add_edge(edge.clone())
272            .delete_edge("old_edge".to_string())
273            .with_timestamp(2000)
274            .build();
275
276        patch1.merge(patch2);
277
278        assert_eq!(patch1.nodes_add.len(), 2, "Should have 2 items");
279        assert_eq!(patch1.edges_add.len(), 1, "Should have 1 items");
280        assert_eq!(patch1.nodes_delete.len(), 1, "Should have 1 items");
281        assert_eq!(patch1.edges_delete.len(), 1, "Should have 1 items");
282        assert_eq!(patch1.timestamp_ms, 2000); // Should use the latest timestamp
283        assert_eq!(patch1.operation_count(), 5);
284    }
285
286    #[test]
287    fn test_empty_patch() {
288        let patch = AstPatch::new("test_repo".to_string(), "abc123".to_string());
289        assert!(patch.is_empty(), "New patch should be empty");
290        assert_eq!(patch.operation_count(), 0);
291
292        // Empty patch should serialize/deserialize correctly
293        let json = serde_json::to_string(&patch).unwrap();
294        let deserialized: AstPatch = serde_json::from_str(&json).unwrap();
295        assert!(
296            deserialized.is_empty(),
297            "Empty patch should remain empty after serialization"
298        );
299    }
300
301    #[test]
302    fn test_patch_with_custom_timestamp() {
303        let custom_timestamp = 9876543210;
304        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
305            .with_timestamp(custom_timestamp)
306            .build();
307
308        assert_eq!(patch.timestamp_ms, custom_timestamp);
309    }
310
311    #[test]
312    fn test_patch_validation() {
313        // Test that patch can handle nodes with same IDs (deduplication would be done at apply time)
314        let node = create_test_node("func");
315        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
316            .add_node(node.clone())
317            .add_node(node.clone()) // Same node added twice
318            .build();
319
320        assert_eq!(patch.nodes_add.len(), 2, "Should have 2 items"); // Both are kept in the patch
321    }
322
323    #[test]
324    fn test_large_patch() {
325        let mut builder = PatchBuilder::new("test_repo".to_string(), "abc123".to_string());
326
327        // Add many nodes
328        for i in 0..100 {
329            let node = create_test_node(&format!("func{i}"));
330            builder = builder.add_node(node);
331        }
332
333        // Add many deletions
334        for i in 0..50 {
335            builder = builder.delete_node(format!("old_node_{i}"));
336            builder = builder.delete_edge(format!("old_edge_{i}"));
337        }
338
339        let patch = builder.build();
340        assert_eq!(patch.nodes_add.len(), 100, "Should have 100 items");
341        assert_eq!(patch.nodes_delete.len(), 50, "Should have 50 items");
342        assert_eq!(patch.edges_delete.len(), 50, "Should have 50 items");
343        assert_eq!(patch.operation_count(), 200);
344    }
345}