serde_hkx_features/
diff.rs

1//! Show diff between two files.
2
3use crate::error::{Error, Result};
4use crate::fs::ReadExt as _;
5use serde_hkx::bytes::hexdump;
6use std::path::Path;
7use tokio::fs;
8
9/// Output diff between two files string to stdout/file.
10/// - `output`: If not provided, then stdout.
11/// - `use_color`: ANSI color diff. (red & green)
12///
13/// # Errors
14/// - Not found extension.
15/// - Fail to read.
16/// - Fail to write.
17///
18/// # Note
19/// extension
20/// - `hkx` -> Hexdump string
21/// - else -> Any encode string
22pub async fn write_diff<I1, I2, O>(
23    old: I1,
24    new: I2,
25    output: Option<O>,
26    use_color: bool,
27) -> Result<()>
28where
29    I1: AsRef<Path>,
30    I2: AsRef<Path>,
31    O: AsRef<Path>,
32{
33    let old_str = read_any_to_string(old).await?;
34    let new_str = read_any_to_string(new).await?;
35
36    let diff_str = diff(old_str, new_str, use_color);
37    match output {
38        Some(output) => fs::write(output, &diff_str).await?,
39        None => print!("{diff_str}"),
40    };
41    Ok(())
42}
43
44/// extension
45/// - `hkx` -> Hexdump string
46/// - else -> Any encode string
47///
48/// # Errors
49/// Not found extension.
50async fn read_any_to_string<I>(path: I) -> Result<String>
51where
52    I: AsRef<Path>,
53{
54    let path = path.as_ref();
55    let ext = path.extension();
56
57    if let Some(ext) = ext {
58        if ext.eq_ignore_ascii_case("hkx") {
59            Ok(hexdump::to_string(path.read_bytes().await?))
60        } else {
61            path.read_any_string().await
62        }
63    } else {
64        Err(Error::MissingExtension {
65            path: path.to_path_buf(),
66        })
67    }
68}
69
70/// Show diff between two files.
71///
72/// - `color`: ANSI color diff. (red & green)
73pub fn diff(old: impl AsRef<str>, new: impl AsRef<str>, color: bool) -> String {
74    let mut output_diff = String::new();
75
76    let diff = similar::TextDiff::from_lines(old.as_ref(), new.as_ref());
77    for change in diff.iter_all_changes() {
78        let (sign, end) = if color {
79            const DELETE: &str = "\u{1b}[31m-"; // 31 is red
80            const INSERT: &str = "\u{1b}[32m+"; // 32 is green
81            const RESET_COLOR: &str = "\u{1b}[39m";
82
83            output_diff.push_str(RESET_COLOR);
84
85            let sign = match change.tag() {
86                similar::ChangeTag::Delete => DELETE,
87                similar::ChangeTag::Insert => INSERT,
88                similar::ChangeTag::Equal => " ",
89            };
90            (sign, RESET_COLOR)
91        } else {
92            let sign = match change.tag() {
93                similar::ChangeTag::Delete => "-",
94                similar::ChangeTag::Insert => "+",
95                similar::ChangeTag::Equal => " ",
96            };
97            (sign, "")
98        };
99        output_diff += &format!("{sign}{change}{end}");
100    }
101    output_diff
102}