diff_logger 0.1.0

Pretty diff logger for JSON values in rust
Documentation
use crate::{
    types::{Change, FieldChange, FieldContentChange, ValueChange},
    PrettyLog,
};
use chrono::{DateTime, FixedOffset};
use serde_json::{Map, Number, Value};
use std::collections::{HashMap, HashSet};

fn lookup_header(key: &str, value: &Value) -> Value {
    match value {
        Value::Object(hm) => hm.get(key).map_or(Value::Null, |x| x.clone()),
        _ => Value::Null,
    }
}

fn get_header(key: &str, old: &Value, cur: &Value, options: &DiffLogger) -> Option<ValueChange> {
    lookup_header(key, old).diff_value(&lookup_header(key, cur), options)
}

fn gen_headers(old_field: &Value, current_field: &Value, options: &DiffLogger) -> Vec<ValueChange> {
    options
        .headers
        .iter()
        .flat_map(|(k, _)| get_header(k, old_field, current_field, options))
        .collect()
}

fn diff_field(
    name: String,
    old_field: &Value,
    current_field: &Value,
    options: &DiffLogger,
) -> Option<FieldChange> {
    let headers = gen_headers(old_field, current_field, options);

    old_field
        .diff_value(current_field, options)
        .map(|value| FieldChange {
            name,
            headers,
            content: FieldContentChange::Diff(value),
        })
}

fn all_keys(old_map: &Map<String, Value>, current_map: &Map<String, Value>) -> Vec<String> {
    let keys: HashSet<_> = old_map
        .keys()
        .chain(current_map.keys())
        .into_iter()
        .collect();

    let mut vec: Vec<String> = keys.iter().map(|x| x.to_string()).collect();
    vec.sort();
    return vec;
}

fn diff_fields(
    old_map: &Map<String, Value>,
    current_map: &Map<String, Value>,
    options: &DiffLogger,
) -> Vec<FieldChange> {
    all_keys(old_map, current_map)
        .iter()
        .flat_map(|key| {
            let old_field = old_map.get(key);
            let current_field = current_map.get(key);
            let name = key.to_string();

            match (old_field, current_field) {
                (None, None) => None,
                (None, Some(v)) => Some(FieldChange {
                    name,
                    headers: Vec::new(),
                    content: FieldContentChange::New(v.clone()),
                }),
                (Some(v), None) => Some(FieldChange {
                    name,
                    headers: Vec::new(),
                    content: FieldContentChange::Deleted(v.clone()),
                }),
                (Some(o), Some(c)) => diff_field(name, o, c, options),
            }
        })
        .collect()
}

type Headers = HashMap<String, bool>;

#[derive(Debug, Clone)]
pub struct DiffLogger {
    headers: Headers,
}

impl DiffLogger {
    pub fn new() -> DiffLogger {
        DiffLogger {
            headers: HashMap::new(),
        }
    }

    fn field_visibility(&self, name: &str) -> bool {
        self.headers.get(name).map_or(true, |x| *x)
    }

    pub fn set_header<T: ToString>(&self, header: T, show_in_fields: bool) -> DiffLogger {
        let mut headers = self.headers.clone();
        headers.insert(header.to_string(), show_in_fields);
        DiffLogger { headers }
    }

    pub fn diff<T: Diff>(&self, prev: &T, next: &T) -> String {
        prev.diff_value(next, self).pretty()
    }

    pub fn log_diff<T: Diff>(&self, prev: &T, next: &T) {
        println!("{}", self.diff(prev, next))
    }
}

pub trait Diff {
    fn diff_value(&self, changed: &Self, logger: &DiffLogger) -> Option<ValueChange>;
}

fn to_float(x: &Number) -> f64 {
    x.as_f64().unwrap_or_default()
}

fn to_timestamps(x: &String, y: &String) -> Option<(DateTime<FixedOffset>, DateTime<FixedOffset>)> {
    if let (Ok(o), Ok(c)) = (
        DateTime::parse_from_rfc3339(x),
        DateTime::parse_from_rfc3339(y),
    ) {
        return Some((o, c));
    }
    return None;
}

fn drop_headers(fields: Vec<FieldChange>, options: &DiffLogger) -> Vec<FieldChange> {
    fields
        .iter()
        .filter(|v| options.field_visibility(&v.name))
        .map(|v| v.clone())
        .collect()
}

fn to_field_map(ls: &Vec<Value>) -> Map<String, Value> {
    ls.iter()
        .enumerate()
        .map(|(k, v)| (k.to_string(), v.clone()))
        .collect()
}

impl Diff for Value {
    fn diff_value(&self, changed: &Self, logger: &DiffLogger) -> Option<ValueChange> {
        if self == changed {
            return None;
        }

        match (self, changed) {
            (Value::Object(map1), Value::Object(map2)) => {
                let fields = diff_fields(map1, map2, logger);

                if fields.is_empty() {
                    None
                } else {
                    Some(ValueChange::Entries(drop_headers(fields, logger)))
                }
            }
            (Value::Array(ls1), Value::Array(ls2)) => {
                let fields = diff_fields(&to_field_map(ls1), &to_field_map(ls2), logger);

                if fields.is_empty() {
                    None
                } else {
                    Some(ValueChange::Entries(drop_headers(fields, logger)))
                }
            }
            (Value::Number(x), Value::Number(y)) => Some(ValueChange::Number(Change {
                before: to_float(x),
                after: to_float(y),
            })),
            (Value::String(b), Value::String(a)) => {
                if let Some((before, after)) = to_timestamps(b, a) {
                    Some(ValueChange::Date(Change { before, after }))
                } else {
                    Some(ValueChange::String(Change {
                        before: b.clone(),
                        after: a.clone(),
                    }))
                }
            }
            (Value::Bool(before), Value::Bool(after)) => Some(ValueChange::Bool(Change {
                before: *before,
                after: *after,
            })),
            (before, after) => Some(ValueChange::Value(Change {
                before: before.clone(),
                after: after.clone(),
            })),
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::{
        types::{
            test_utils::{field, object},
            Change, ValueChange,
        },
        Diff, DiffLogger,
    };

    #[test]
    fn no_changes() {
        let logger = DiffLogger::new();

        let value = json!({
            "a": "David",
            "b": 43,
        });

        assert_eq!(value.diff_value(&value.clone(), &logger), None);
    }

    #[test]
    fn basic_changes() {
        let logger = DiffLogger::new();

        let prev = json!({
            "a": "David",
            "b": 43,
        });

        let next = json!({
            "a": "John",
            "b": 43,
        });

        assert_eq!(
            prev.diff_value(&next, &logger),
            Some(object(&[field(
                "a",
                ValueChange::String(Change {
                    before: "David".to_string(),
                    after: "John".to_string()
                })
            )]))
        );
    }
}