diff_logger 0.1.0

Pretty diff logger for JSON values in rust
Documentation
use crate::types::{Change, FieldChange, FieldContentChange, ValueChange};
use chrono::{DateTime, Duration, FixedOffset, Local};
use colored::Colorize;
use serde_json::Value;

const NEW_LINE: &str = "\n";
const EMPTY: &str = "";

fn indent(x: String) -> String {
    x.replace("\n", "\n  ")
}

fn headers(xs: Vec<String>) -> String {
    format!(
        "{}{}{}",
        "\u{25D6}".bright_black(),
        xs.join(", ").on_bright_black(),
        "\u{25D7}".bright_black()
    )
}

fn lines(xs: Vec<String>) -> String {
    xs.join(NEW_LINE)
}

pub trait PrettyLog {
    fn pretty(&self) -> String;
}

impl PrettyLog for DateTime<FixedOffset> {
    fn pretty(&self) -> String {
        let local: DateTime<Local> = DateTime::from(self.clone());
        format!("{}", local.format("%H:%M:%S"))
    }
}
impl PrettyLog for bool {
    fn pretty(&self) -> String {
        self.to_string()
    }
}

impl PrettyLog for f64 {
    fn pretty(&self) -> String {
        self.to_string()
    }
}

impl PrettyLog for String {
    fn pretty(&self) -> String {
        format!("\"{}\"", self.to_string())
    }
}

impl PrettyLog for Duration {
    fn pretty(&self) -> String {
        let hours = self.num_hours();
        let minutes = self.num_minutes() % 60;

        if hours > 0 {
            return format!("{}:{} hours", hours, minutes);
        }

        let seconds = self.num_seconds() % 60;

        if minutes > 0 {
            return format!("{}:{} minutes", minutes, seconds);
        }

        format!("{} seconds", seconds)
    }
}

impl PrettyLog for Value {
    fn pretty(&self) -> String {
        match self {
            Value::Null => "null".to_owned(),
            Value::Bool(b) => b.pretty(),
            Value::Number(n) => n.to_string(),
            v => v.to_string(),
        }
    }
}

impl<T: PrettyLog> PrettyLog for Change<T> {
    fn pretty(&self) -> String {
        format!("{} -> {}", self.before.pretty(), self.after.pretty())
    }
}

fn pretty_numeric<T: PrettyLog, C: PrettyLog>(
    change: &Change<T>,
    diff: C,
    is_positive: bool,
) -> String {
    format!(
        "{} | {}",
        change.pretty(),
        if is_positive {
            diff.pretty().green()
        } else {
            diff.pretty().red()
        }
    )
}

fn print_headers(vs: &Vec<ValueChange>) -> String {
    if vs.len() == 0 {
        return EMPTY.to_string();
    }

    headers(vs.iter().map(|v| v.pretty()).collect())
}

impl PrettyLog for FieldChange {
    fn pretty(&self) -> String {
        let new_line = match &self.content {
            FieldContentChange::Diff(v) => !v.is_leaf(),
            _ => false,
        };

        let name = match &self.content {
            FieldContentChange::Diff(_) => format!("~ {}", self.name).yellow(),
            FieldContentChange::Deleted(_) => format!("- {}", self.name).red(),
            FieldContentChange::New(_) => format!("+ {}", self.name).green(),
        };

        let value = self.content.pretty();

        format!(
            "{}:{}{}{}",
            &name,
            print_headers(&self.headers),
            if new_line {
                NEW_LINE
            } else if value.is_empty() {
                EMPTY
            } else {
                " "
            },
            value
        )
    }
}

impl PrettyLog for FieldContentChange {
    fn pretty(&self) -> String {
        match &self {
            FieldContentChange::Diff(d) => d.pretty(),
            FieldContentChange::Deleted(x) => x.pretty(),
            FieldContentChange::New(x) => x.pretty(),
        }
    }
}

impl PrettyLog for ValueChange {
    fn pretty(&self) -> String {
        match self {
            ValueChange::Entries(elems) => {
                lines(elems.iter().map(|x| indent(x.pretty())).collect())
            }
            ValueChange::Value(ch) => ch.pretty(),
            ValueChange::Number(ch) => {
                pretty_numeric(ch, ch.after - ch.before, ch.after > ch.before)
            }
            ValueChange::Date(ch) => pretty_numeric(ch, ch.after - ch.before, ch.after > ch.before),
            ValueChange::String(ch) => ch.pretty(),
            ValueChange::Bool(ch) => ch.pretty(),
        }
    }
}

impl<T: PrettyLog> PrettyLog for Option<T> {
    fn pretty(&self) -> String {
        match self {
            Some(v) => v.pretty(),
            None => "".to_owned(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::PrettyLog;
    use crate::types::{
        test_utils::{field, object},
        Change, FieldChange, FieldContentChange, ValueChange,
    };

    fn drop_colors(x: String) -> String {
        x.replace("\u{1b}[32m", "")
            .replace("\u{1b}[0m", "")
            .replace("\u{1b}[31m", "")
            .replace("\u{1b}[90m", "")
            .replace("\u{1b}[33m", "")
            .replace("\u{1b}[100m", "")
    }

    #[test]
    fn empty_object() {
        assert_eq!(object(&[]).pretty(), "");
    }

    #[test]
    fn header_only_positive() {
        let diff = FieldChange {
            content: FieldContentChange::Diff(object(&[])),
            name: "stats".to_owned(),
            headers: [ValueChange::Number(Change {
                before: 1.0,
                after: 2.0,
            })]
            .to_vec(),
        };

        assert_eq!(drop_colors(diff.pretty()), format!("~ stats:◖1 -> 2 | 1◗"));
    }

    #[test]
    fn header_only_negative() {
        let diff = FieldChange {
            content: FieldContentChange::Diff(object(&[])),
            name: "stats".to_owned(),
            headers: [ValueChange::Number(Change {
                before: 423.0,
                after: 2.0,
            })]
            .to_vec(),
        };

        assert_eq!(drop_colors(diff.pretty()), "~ stats:◖423 -> 2 | -421◗");
    }

    #[test]
    fn object_fields() {
        let diff = object(&[field(
            "field",
            object(&[
                field(
                    "text",
                    ValueChange::String(Change {
                        before: "A".to_owned(),
                        after: "B".to_owned(),
                    }),
                ),
                field(
                    "number",
                    ValueChange::Number(Change {
                        before: 1.0,
                        after: 2.0,
                    }),
                ),
            ]),
        )]);

        assert_eq!(
            drop_colors(diff.pretty()),
            format!(
                "~ field:\
                \n  ~ text: \"A\" -> \"B\"\
                \n  ~ number: 1 -> 2 | 1"
            )
        );
    }
}