clnrm_core/cli/commands/
diff.rs1use crate::error::{CleanroomError, Result};
6use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct DiffResult {
11 pub added_count: usize,
13 pub removed_count: usize,
15 pub modified_count: usize,
17 pub added: Vec<String>,
19 pub removed: Vec<String>,
21 pub modified: Vec<String>,
23}
24
25pub fn diff_traces(
27 baseline: &Path,
28 current: &Path,
29 format: &str,
30 only_changes: bool,
31) -> Result<DiffResult> {
32 let baseline_content = std::fs::read_to_string(baseline)
34 .map_err(|e| CleanroomError::io_error(format!("Failed to read baseline: {}", e)))?;
35
36 let current_content = std::fs::read_to_string(current)
37 .map_err(|e| CleanroomError::io_error(format!("Failed to read current: {}", e)))?;
38
39 let baseline_json: serde_json::Value =
41 serde_json::from_str(&baseline_content).map_err(|e| {
42 CleanroomError::serialization_error(format!("Failed to parse baseline JSON: {}", e))
43 })?;
44
45 let current_json: serde_json::Value = serde_json::from_str(¤t_content).map_err(|e| {
46 CleanroomError::serialization_error(format!("Failed to parse current JSON: {}", e))
47 })?;
48
49 let baseline_spans = extract_span_names(&baseline_json);
51 let current_spans = extract_span_names(¤t_json);
52
53 let added: Vec<String> = current_spans
55 .iter()
56 .filter(|s| !baseline_spans.contains(s))
57 .cloned()
58 .collect();
59
60 let removed: Vec<String> = baseline_spans
61 .iter()
62 .filter(|s| !current_spans.contains(s))
63 .cloned()
64 .collect();
65
66 let modified = Vec::new();
68
69 let result = DiffResult {
70 added_count: added.len(),
71 removed_count: removed.len(),
72 modified_count: modified.len(),
73 added,
74 removed,
75 modified,
76 };
77
78 match format {
80 "json" => {
81 let json = serde_json::json!({
82 "added_count": result.added_count,
83 "removed_count": result.removed_count,
84 "modified_count": result.modified_count,
85 "added": result.added,
86 "removed": result.removed,
87 "modified": result.modified,
88 });
89 println!(
90 "{}",
91 serde_json::to_string_pretty(&json).map_err(|e| {
92 CleanroomError::serialization_error(format!("Failed to serialize JSON: {}", e))
93 })?
94 );
95 }
96 _ => {
97 if result.added_count > 0 {
99 println!("Added spans ({}):", result.added_count);
100 for span in &result.added {
101 println!(" + {}", span);
102 }
103 }
104
105 if result.removed_count > 0 {
106 println!("Removed spans ({}):", result.removed_count);
107 for span in &result.removed {
108 println!(" - {}", span);
109 }
110 }
111
112 if result.modified_count > 0 {
113 println!("Modified spans ({}):", result.modified_count);
114 for span in &result.modified {
115 println!(" ~ {}", span);
116 }
117 }
118
119 if !only_changes
120 || result.added_count + result.removed_count + result.modified_count == 0
121 {
122 println!(
123 "\nSummary: {} added, {} removed, {} modified",
124 result.added_count, result.removed_count, result.modified_count
125 );
126 }
127 }
128 }
129
130 Ok(result)
131}
132
133fn extract_span_names(json: &serde_json::Value) -> Vec<String> {
135 let mut spans = Vec::new();
136
137 if let Some(array) = json.as_array() {
138 for item in array {
139 if let Some(name) = item.get("name").and_then(|n| n.as_str()) {
140 spans.push(name.to_string());
141 }
142 }
143 } else if let Some(obj) = json.as_object() {
144 if let Some(name) = obj.get("name").and_then(|n| n.as_str()) {
145 spans.push(name.to_string());
146 }
147
148 for (_, value) in obj {
150 spans.extend(extract_span_names(value));
151 }
152 }
153
154 spans
155}