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());
169        assert_eq!(patch.operation_count(), 0);
170        assert!(patch.timestamp_ms > 0);
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);
188        assert_eq!(patch.edges_add.len(), 1);
189        assert_eq!(patch.nodes_delete.len(), 1);
190        assert_eq!(patch.edges_delete.len(), 1);
191        assert_eq!(patch.operation_count(), 5);
192        assert!(!patch.is_empty());
193    }
194
195    #[test]
196    fn test_patch_builder_batch_operations() {
197        let nodes = vec![
198            create_test_node("func1"),
199            create_test_node("func2"),
200            create_test_node("func3"),
201        ];
202        let edges = vec![
203            create_test_edge(&nodes[0], &nodes[1]),
204            create_test_edge(&nodes[1], &nodes[2]),
205        ];
206
207        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
208            .add_nodes(nodes.clone())
209            .add_edges(edges.clone())
210            .delete_nodes(vec!["id1".to_string(), "id2".to_string()])
211            .delete_edges(vec!["edge1".to_string(), "edge2".to_string()])
212            .build();
213
214        assert_eq!(patch.nodes_add.len(), 3);
215        assert_eq!(patch.edges_add.len(), 2);
216        assert_eq!(patch.nodes_delete.len(), 2);
217        assert_eq!(patch.edges_delete.len(), 2);
218        assert_eq!(patch.operation_count(), 9);
219    }
220
221    #[test]
222    fn test_patch_serialization() {
223        let node = create_test_node("test_func");
224        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
225            .add_node(node)
226            .with_timestamp(1234567890)
227            .build();
228
229        // Test JSON serialization
230        let json = serde_json::to_string(&patch).unwrap();
231        let deserialized: AstPatch = serde_json::from_str(&json).unwrap();
232
233        assert_eq!(deserialized.repo, patch.repo);
234        assert_eq!(deserialized.commit, patch.commit);
235        assert_eq!(deserialized.nodes_add.len(), patch.nodes_add.len());
236        assert_eq!(deserialized.timestamp_ms, 1234567890);
237    }
238
239    #[test]
240    fn test_patch_merge() {
241        let node1 = create_test_node("func1");
242        let node2 = create_test_node("func2");
243        let edge = create_test_edge(&node1, &node2);
244
245        let mut patch1 = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
246            .add_node(node1.clone())
247            .delete_node("old1".to_string())
248            .with_timestamp(1000)
249            .build();
250
251        let patch2 = PatchBuilder::new("test_repo".to_string(), "def456".to_string())
252            .add_node(node2.clone())
253            .add_edge(edge.clone())
254            .delete_edge("old_edge".to_string())
255            .with_timestamp(2000)
256            .build();
257
258        patch1.merge(patch2);
259
260        assert_eq!(patch1.nodes_add.len(), 2);
261        assert_eq!(patch1.edges_add.len(), 1);
262        assert_eq!(patch1.nodes_delete.len(), 1);
263        assert_eq!(patch1.edges_delete.len(), 1);
264        assert_eq!(patch1.timestamp_ms, 2000); // Should use the latest timestamp
265        assert_eq!(patch1.operation_count(), 5);
266    }
267
268    #[test]
269    fn test_empty_patch() {
270        let patch = AstPatch::new("test_repo".to_string(), "abc123".to_string());
271        assert!(patch.is_empty());
272        assert_eq!(patch.operation_count(), 0);
273
274        // Empty patch should serialize/deserialize correctly
275        let json = serde_json::to_string(&patch).unwrap();
276        let deserialized: AstPatch = serde_json::from_str(&json).unwrap();
277        assert!(deserialized.is_empty());
278    }
279
280    #[test]
281    fn test_patch_with_custom_timestamp() {
282        let custom_timestamp = 9876543210;
283        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
284            .with_timestamp(custom_timestamp)
285            .build();
286
287        assert_eq!(patch.timestamp_ms, custom_timestamp);
288    }
289
290    #[test]
291    fn test_patch_validation() {
292        // Test that patch can handle nodes with same IDs (deduplication would be done at apply time)
293        let node = create_test_node("func");
294        let patch = PatchBuilder::new("test_repo".to_string(), "abc123".to_string())
295            .add_node(node.clone())
296            .add_node(node.clone()) // Same node added twice
297            .build();
298
299        assert_eq!(patch.nodes_add.len(), 2); // Both are kept in the patch
300    }
301
302    #[test]
303    fn test_large_patch() {
304        let mut builder = PatchBuilder::new("test_repo".to_string(), "abc123".to_string());
305
306        // Add many nodes
307        for i in 0..100 {
308            let node = create_test_node(&format!("func{}", i));
309            builder = builder.add_node(node);
310        }
311
312        // Add many deletions
313        for i in 0..50 {
314            builder = builder.delete_node(format!("old_node_{}", i));
315            builder = builder.delete_edge(format!("old_edge_{}", i));
316        }
317
318        let patch = builder.build();
319        assert_eq!(patch.nodes_add.len(), 100);
320        assert_eq!(patch.nodes_delete.len(), 50);
321        assert_eq!(patch.edges_delete.len(), 50);
322        assert_eq!(patch.operation_count(), 200);
323    }
324}