1use crate::{detect_format_from_path, parse_content_by_format, DiffOptions, DiffResult};
4use anyhow::{anyhow, Result};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use super::diff_recursive;
11use crate::io::get_all_files_recursive;
12
13pub fn diff_paths(
20 old_path: &str,
21 new_path: &str,
22 options: Option<&DiffOptions>,
23) -> Result<Vec<DiffResult>> {
24 let path1 = Path::new(old_path);
25 let path2 = Path::new(new_path);
26
27 let recursive = options.and_then(|o| o.recursive).unwrap_or(false);
28
29 match (path1.is_dir(), path2.is_dir()) {
30 (true, true) => {
31 if recursive {
32 diff_directories(path1, path2, options)
33 } else {
34 Err(anyhow!(
35 "Both paths are directories. Use --recursive (-r) to compare directories."
36 ))
37 }
38 }
39 (false, false) => diff_files(path1, path2, options),
40 (true, false) => Err(anyhow!(
41 "Cannot compare directory '{old_path}' with file '{new_path}'"
42 )),
43 (false, true) => Err(anyhow!(
44 "Cannot compare file '{old_path}' with directory '{new_path}'"
45 )),
46 }
47}
48
49pub fn diff(old: &Value, new: &Value, options: Option<&DiffOptions>) -> Result<Vec<DiffResult>> {
54 let default_options = DiffOptions::default();
55 let opts = options.unwrap_or(&default_options);
56
57 let mut results = Vec::new();
58 diff_recursive(old, new, "", &mut results, opts);
59 Ok(results)
60}
61
62fn diff_files(
63 path1: &Path,
64 path2: &Path,
65 options: Option<&DiffOptions>,
66) -> Result<Vec<DiffResult>> {
67 let content1 = fs::read_to_string(path1)?;
69 let content2 = fs::read_to_string(path2)?;
70
71 let format1 = detect_format_from_path(path1);
73 let format2 = detect_format_from_path(path2);
74
75 let value1 = parse_content_by_format(&content1, format1)?;
77 let value2 = parse_content_by_format(&content2, format2)?;
78
79 diff(&value1, &value2, options)
81}
82
83fn diff_directories(
84 dir1: &Path,
85 dir2: &Path,
86 options: Option<&DiffOptions>,
87) -> Result<Vec<DiffResult>> {
88 let mut results = Vec::new();
89
90 let files1 = get_all_files_recursive(dir1)?;
92 let files2 = get_all_files_recursive(dir2)?;
93
94 let files1_map: HashMap<String, &Path> = files1
96 .iter()
97 .filter_map(|path| {
98 path.strip_prefix(dir1)
99 .ok()
100 .map(|rel| (rel.to_string_lossy().to_string(), path.as_path()))
101 })
102 .collect();
103
104 let files2_map: HashMap<String, &Path> = files2
105 .iter()
106 .filter_map(|path| {
107 path.strip_prefix(dir2)
108 .ok()
109 .map(|rel| (rel.to_string_lossy().to_string(), path.as_path()))
110 })
111 .collect();
112
113 for (rel_path, abs_path1) in &files1_map {
115 if !files2_map.contains_key(rel_path) {
116 let content = fs::read_to_string(abs_path1).unwrap_or_default();
117 if let Ok(value) = parse_content_by_format(&content, detect_format_from_path(abs_path1))
118 {
119 results.push(DiffResult::Removed(rel_path.clone(), value));
120 }
121 }
122 }
123
124 for (rel_path, abs_path2) in &files2_map {
126 if !files1_map.contains_key(rel_path) {
127 let content = fs::read_to_string(abs_path2).unwrap_or_default();
128 if let Ok(value) = parse_content_by_format(&content, detect_format_from_path(abs_path2))
129 {
130 results.push(DiffResult::Added(rel_path.clone(), value));
131 }
132 }
133 }
134
135 for (rel_path, abs_path1) in &files1_map {
137 if let Some(abs_path2) = files2_map.get(rel_path) {
138 match diff_files(abs_path1, abs_path2, options) {
139 Ok(mut file_results) => {
140 for result in &mut file_results {
142 match result {
143 DiffResult::Added(path, _) => *path = format!("{rel_path}/{path}"),
144 DiffResult::Removed(path, _) => *path = format!("{rel_path}/{path}"),
145 DiffResult::Modified(path, _, _) => {
146 *path = format!("{rel_path}/{path}")
147 }
148 DiffResult::TypeChanged(path, _, _) => {
149 *path = format!("{rel_path}/{path}")
150 }
151 }
152 }
153 results.extend(file_results);
154 }
155 Err(_) => {
156 continue;
158 }
159 }
160 }
161 }
162
163 Ok(results)
164}
165
166#[cfg(test)]
171mod tests {
172 use super::*;
173 use std::fs;
174
175 #[test]
176 fn test_diff_paths_files() {
177 let temp_dir = std::env::temp_dir();
179 let file1_path = temp_dir.join("diffx_test1.json");
180 let file2_path = temp_dir.join("diffx_test2.json");
181
182 fs::write(&file1_path, r#"{"name": "test", "value": 1}"#).unwrap();
183 fs::write(&file2_path, r#"{"name": "test", "value": 2}"#).unwrap();
184
185 let results = diff_paths(
186 &file1_path.to_string_lossy(),
187 &file2_path.to_string_lossy(),
188 None,
189 )
190 .unwrap();
191
192 assert_eq!(results.len(), 1);
193
194 let _ = fs::remove_file(file1_path);
196 let _ = fs::remove_file(file2_path);
197 }
198
199 #[test]
200 fn test_diff_paths_file_vs_directory_error() {
201 let temp_dir = std::env::temp_dir();
202 let file_path = temp_dir.join("diffx_test_file.json");
203 let dir_path = temp_dir.join("diffx_test_dir");
204
205 fs::write(&file_path, r#"{"test": true}"#).unwrap();
206 fs::create_dir_all(&dir_path).unwrap();
207
208 let result = diff_paths(
209 &file_path.to_string_lossy(),
210 &dir_path.to_string_lossy(),
211 None,
212 );
213
214 assert!(result.is_err());
215 assert!(result
216 .unwrap_err()
217 .to_string()
218 .contains("Cannot compare file"));
219
220 let _ = fs::remove_file(file_path);
222 let _ = fs::remove_dir_all(dir_path);
223 }
224}