diffo 0.2.0

Semantic diffing for Rust structs via serde
Documentation
use diffo::*;
use serde::Serialize;
use std::collections::HashMap;

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
    active: bool,
}

#[derive(Serialize)]
struct Config {
    version: String,
    database: DatabaseConfig,
    api: ApiConfig,
    features: Vec<String>,
}

#[derive(Serialize)]
struct DatabaseConfig {
    host: String,
    port: u16,
    max_connections: u32,
}

#[derive(Serialize)]
struct ApiConfig {
    timeout: u64,
    retries: u32,
    endpoints: HashMap<String, String>,
}

#[test]
fn test_simple_struct_diff() {
    let old = User {
        id: 1,
        name: "Alice".into(),
        email: "alice@old.com".into(),
        active: true,
    };

    let new = User {
        id: 1,
        name: "Alice Smith".into(),
        email: "alice@new.com".into(),
        active: true,
    };

    let diff = diff(&old, &new).unwrap();

    assert!(!diff.is_empty());
    assert!(diff.get("name").is_some());
    assert!(diff.get("email").is_some());
    assert!(diff.get("id").is_none());
    assert!(diff.get("active").is_none());
}

#[test]
fn test_nested_struct_diff() {
    let old = Config {
        version: "1.0.0".into(),
        database: DatabaseConfig {
            host: "localhost".into(),
            port: 5432,
            max_connections: 100,
        },
        api: ApiConfig {
            timeout: 30,
            retries: 3,
            endpoints: HashMap::from([("users".into(), "/api/users".into())]),
        },
        features: vec!["auth".into(), "logging".into()],
    };

    let new = Config {
        version: "1.1.0".into(),
        database: DatabaseConfig {
            host: "db.prod.com".into(),
            port: 5432,
            max_connections: 200,
        },
        api: ApiConfig {
            timeout: 60,
            retries: 5,
            endpoints: HashMap::from([
                ("users".into(), "/api/v2/users".into()),
                ("posts".into(), "/api/posts".into()),
            ]),
        },
        features: vec!["auth".into(), "logging".into(), "metrics".into()],
    };

    let diff = diff(&old, &new).unwrap();

    // Version changed
    assert!(diff.get("version").is_some());

    // Database host changed
    assert!(diff.get("database.host").is_some());

    // Database port unchanged
    assert!(diff.get("database.port").is_none());

    // Database max_connections changed
    assert!(diff.get("database.max_connections").is_some());

    // API timeout changed
    assert!(diff.get("api.timeout").is_some());

    // API retries changed
    assert!(diff.get("api.retries").is_some());

    // Endpoint modified
    assert!(diff.get("api.endpoints.users").is_some());

    // Endpoint added
    assert!(diff.get("api.endpoints.posts").is_some());

    // Feature added
    assert!(diff.get("features[2]").is_some());
}

#[test]
fn test_identical_values() {
    let v1 = User {
        id: 1,
        name: "Alice".into(),
        email: "alice@example.com".into(),
        active: true,
    };

    let v2 = User {
        id: 1,
        name: "Alice".into(),
        email: "alice@example.com".into(),
        active: true,
    };

    let diff = diff(&v1, &v2).unwrap();
    assert!(diff.is_empty());
}

#[test]
fn test_vector_changes() {
    let old = vec![1, 2, 3, 4];
    let new = vec![1, 2, 5, 4, 6];

    let diff = diff(&old, &new).unwrap();

    // Index 2 changed
    assert!(diff.get("[2]").is_some());

    // Index 4 added
    assert!(diff.get("[4]").is_some());

    // Index 0, 1, 3 unchanged
    assert!(diff.get("[0]").is_none());
    assert!(diff.get("[1]").is_none());
    assert!(diff.get("[3]").is_none());
}

#[test]
fn test_hashmap_changes() {
    let mut old = HashMap::new();
    old.insert("a", 1);
    old.insert("b", 2);

    let mut new = HashMap::new();
    new.insert("a", 1);
    new.insert("b", 3);
    new.insert("c", 4);

    let diff = diff(&old, &new).unwrap();

    // 'a' unchanged
    assert!(diff.get("a").is_none());

    // 'b' modified
    assert!(diff.get("b").is_some());

    // 'c' added
    assert!(diff.get("c").is_some());
}

#[test]
fn test_with_masking() {
    #[derive(Serialize)]
    struct Credentials {
        username: String,
        password: String,
        api_key: String,
    }

    let old = Credentials {
        username: "admin".into(),
        password: "old_secret".into(),
        api_key: "key123".into(),
    };

    let new = Credentials {
        username: "admin".into(),
        password: "new_secret".into(),
        api_key: "key456".into(),
    };

    let config = DiffConfig::new().mask("password").mask("api_key");

    let diff = diff_with(&old, &new, &config).unwrap();

    // Password changed (will be in diff, but formatters can mask it)
    assert!(diff.get("password").is_some());

    // API key changed
    assert!(diff.get("api_key").is_some());

    // Username unchanged
    assert!(diff.get("username").is_none());
}

#[test]
fn test_float_tolerance() {
    #[derive(Serialize)]
    struct Metrics {
        value: f64,
        threshold: f64,
    }

    let old = Metrics {
        value: 1.0,
        threshold: 0.5,
    };

    let new = Metrics {
        value: 1.0000001,
        threshold: 0.6,
    };

    // With tolerance
    let config = DiffConfig::new().default_float_tolerance(0.001);
    let diff = diff_with(&old, &new, &config).unwrap();

    // value within tolerance
    assert!(diff.get("value").is_none());

    // threshold outside tolerance
    assert!(diff.get("threshold").is_some());
}

#[test]
fn test_collection_limit() {
    let old: Vec<i32> = (0..2000).collect();
    let new: Vec<i32> = (0..2000).collect();

    let config = DiffConfig::new().collection_limit(100);
    let diff = diff_with(&old, &new, &config).unwrap();

    // Should be elided
    assert!(!diff.is_empty());
    assert_eq!(diff.len(), 1);

    // Check that it's an elided change
    let change = diff.changes().values().next().unwrap();
    assert!(change.is_elided());
}

#[test]
fn test_formatters() {
    let old = vec![1, 2, 3];
    let new = vec![1, 2, 4];

    let diff = diff(&old, &new).unwrap();

    // Pretty format
    let pretty = diff.to_pretty();
    assert!(!pretty.is_empty());

    // JSON format
    let json = diff.to_json().unwrap();
    assert!(json.contains("\"type\""));

    // JSON Patch format
    let patch = diff.to_json_patch().unwrap();
    assert!(patch.contains("\"op\""));

    // Markdown format
    let markdown = diff.to_markdown().unwrap();
    assert!(markdown.contains("| Path |"));
}

#[test]
fn test_deep_nesting() {
    #[derive(Serialize)]
    struct Level3 {
        value: i32,
    }

    #[derive(Serialize)]
    struct Level2 {
        inner: Level3,
    }

    #[derive(Serialize)]
    struct Level1 {
        inner: Level2,
    }

    let old = Level1 {
        inner: Level2 {
            inner: Level3 { value: 1 },
        },
    };

    let new = Level1 {
        inner: Level2 {
            inner: Level3 { value: 2 },
        },
    };

    let diff = diff(&old, &new).unwrap();
    assert!(diff.get("inner.inner.value").is_some());
}