use crate::{detect_format_from_path, parse_content_by_format, DiffOptions, DiffResult};
use anyhow::{anyhow, Result};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use super::diff_recursive;
use crate::io::get_all_files_recursive;
pub fn diff_paths(
old_path: &str,
new_path: &str,
options: Option<&DiffOptions>,
) -> Result<Vec<DiffResult>> {
let path1 = Path::new(old_path);
let path2 = Path::new(new_path);
let recursive = options.and_then(|o| o.recursive).unwrap_or(false);
match (path1.is_dir(), path2.is_dir()) {
(true, true) => {
if recursive {
diff_directories(path1, path2, options)
} else {
Err(anyhow!(
"Both paths are directories. Use --recursive (-r) to compare directories."
))
}
}
(false, false) => diff_files(path1, path2, options),
(true, false) => Err(anyhow!(
"Cannot compare directory '{old_path}' with file '{new_path}'"
)),
(false, true) => Err(anyhow!(
"Cannot compare file '{old_path}' with directory '{new_path}'"
)),
}
}
pub fn diff(old: &Value, new: &Value, options: Option<&DiffOptions>) -> Result<Vec<DiffResult>> {
let default_options = DiffOptions::default();
let opts = options.unwrap_or(&default_options);
let mut results = Vec::new();
diff_recursive(old, new, "", &mut results, opts);
Ok(results)
}
fn diff_files(
path1: &Path,
path2: &Path,
options: Option<&DiffOptions>,
) -> Result<Vec<DiffResult>> {
let content1 = fs::read_to_string(path1)?;
let content2 = fs::read_to_string(path2)?;
let format1 = detect_format_from_path(path1);
let format2 = detect_format_from_path(path2);
let value1 = parse_content_by_format(&content1, format1)?;
let value2 = parse_content_by_format(&content2, format2)?;
diff(&value1, &value2, options)
}
fn diff_directories(
dir1: &Path,
dir2: &Path,
options: Option<&DiffOptions>,
) -> Result<Vec<DiffResult>> {
let mut results = Vec::new();
let files1 = get_all_files_recursive(dir1)?;
let files2 = get_all_files_recursive(dir2)?;
let files1_map: HashMap<String, &Path> = files1
.iter()
.filter_map(|path| {
path.strip_prefix(dir1)
.ok()
.map(|rel| (rel.to_string_lossy().to_string(), path.as_path()))
})
.collect();
let files2_map: HashMap<String, &Path> = files2
.iter()
.filter_map(|path| {
path.strip_prefix(dir2)
.ok()
.map(|rel| (rel.to_string_lossy().to_string(), path.as_path()))
})
.collect();
for (rel_path, abs_path1) in &files1_map {
if !files2_map.contains_key(rel_path) {
let content = fs::read_to_string(abs_path1).unwrap_or_default();
if let Ok(value) = parse_content_by_format(&content, detect_format_from_path(abs_path1))
{
results.push(DiffResult::Removed(rel_path.clone(), value));
}
}
}
for (rel_path, abs_path2) in &files2_map {
if !files1_map.contains_key(rel_path) {
let content = fs::read_to_string(abs_path2).unwrap_or_default();
if let Ok(value) = parse_content_by_format(&content, detect_format_from_path(abs_path2))
{
results.push(DiffResult::Added(rel_path.clone(), value));
}
}
}
for (rel_path, abs_path1) in &files1_map {
if let Some(abs_path2) = files2_map.get(rel_path) {
match diff_files(abs_path1, abs_path2, options) {
Ok(mut file_results) => {
for result in &mut file_results {
match result {
DiffResult::Added(path, _) => *path = format!("{rel_path}/{path}"),
DiffResult::Removed(path, _) => *path = format!("{rel_path}/{path}"),
DiffResult::Modified(path, _, _) => {
*path = format!("{rel_path}/{path}")
}
DiffResult::TypeChanged(path, _, _) => {
*path = format!("{rel_path}/{path}")
}
}
}
results.extend(file_results);
}
Err(_) => {
continue;
}
}
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_diff_paths_files() {
let temp_dir = std::env::temp_dir();
let file1_path = temp_dir.join("diffx_test1.json");
let file2_path = temp_dir.join("diffx_test2.json");
fs::write(&file1_path, r#"{"name": "test", "value": 1}"#).unwrap();
fs::write(&file2_path, r#"{"name": "test", "value": 2}"#).unwrap();
let results = diff_paths(
&file1_path.to_string_lossy(),
&file2_path.to_string_lossy(),
None,
)
.unwrap();
assert_eq!(results.len(), 1);
let _ = fs::remove_file(file1_path);
let _ = fs::remove_file(file2_path);
}
#[test]
fn test_diff_paths_file_vs_directory_error() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("diffx_test_file.json");
let dir_path = temp_dir.join("diffx_test_dir");
fs::write(&file_path, r#"{"test": true}"#).unwrap();
fs::create_dir_all(&dir_path).unwrap();
let result = diff_paths(
&file_path.to_string_lossy(),
&dir_path.to_string_lossy(),
None,
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot compare file"));
let _ = fs::remove_file(file_path);
let _ = fs::remove_dir_all(dir_path);
}
}