diffo 0.2.0

Semantic diffing for Rust structs via serde
Documentation
//! Demonstrates custom comparator functions.

use diffo::{diff, diff_with, DiffConfig, ValueExt};
use serde::Serialize;
use std::rc::Rc;

#[derive(Serialize)]
struct Event {
    id: u64,
    name: String,
    timestamp: u64,
}

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

#[derive(Serialize)]
struct Config {
    url: String,
    timeout: u32,
}

fn main() {
    println!("=== Custom Comparators Demo ===\n");

    // 1. Ignoring timestamp fields
    println!("1. Ignoring Timestamps");
    let old_event = Event {
        id: 1,
        name: "login".into(),
        timestamp: 1000,
    };

    let new_event = Event {
        id: 1,
        name: "login".into(),
        timestamp: 2000, // Changed
    };

    // Without comparator - shows timestamp change
    let d = diff(&old_event, &new_event).unwrap();
    println!("   Without comparator: {} changes", d.len());

    // With comparator - ignores timestamp
    let config = DiffConfig::new().comparator(
        "",
        Rc::new(|old, new| {
            // Compare everything except timestamp
            old.get_field("id") == new.get_field("id")
                && old.get_field("name") == new.get_field("name")
        }),
    );

    let d = diff_with(&old_event, &new_event, &config).unwrap();
    println!("   With comparator: {} changes", d.len());
    println!();

    // 2. Comparing by ID only
    println!("2. Comparing Users by ID Only");
    let old_user = User {
        id: 42,
        name: "Alice".into(),
        email: "alice@old.com".into(),
        updated_at: 1000,
    };

    let new_user = User {
        id: 42,
        name: "Alice Smith".into(),
        email: "alice@new.com".into(),
        updated_at: 2000,
    };

    // Compare users by ID only - treat as equal if ID matches
    let config = DiffConfig::new().comparator(
        "",
        Rc::new(|old, new| {
            // Users are "equal" if they have the same ID
            old.get_field("id") == new.get_field("id")
        }),
    );

    let d = diff_with(&old_user, &new_user, &config).unwrap();
    println!("   Changes when comparing by ID only: {}", d.len());
    println!();

    // 3. Case-insensitive URL comparison
    println!("3. Case-Insensitive URL Comparison");
    let old_config = Config {
        url: "https://API.example.com".into(),
        timeout: 30,
    };

    let new_config = Config {
        url: "https://api.example.com".into(), // Different case
        timeout: 30,
    };

    // Without comparator - shows URL change
    let d = diff(&old_config, &new_config).unwrap();
    println!("   Without comparator: {} changes", d.len());

    // With case-insensitive comparator for URL
    let config = DiffConfig::new().comparator(
        "url",
        Rc::new(|old, new| {
            if let (Some(old_str), Some(new_str)) = (old.as_string(), new.as_string()) {
                old_str.to_lowercase() == new_str.to_lowercase()
            } else {
                old == new
            }
        }),
    );

    let d = diff_with(&old_config, &new_config, &config).unwrap();
    println!("   With case-insensitive comparator: {} changes", d.len());
    println!();

    // 4. Ignoring specific fields with patterns
    println!("4. Ignoring Fields with Patterns");
    #[derive(Serialize)]
    struct Data {
        stable: String,
        temp_cache: String,
        temp_buffer: String,
    }

    let old_data = Data {
        stable: "same".into(),
        temp_cache: "old_cache".into(),
        temp_buffer: "old_buffer".into(),
    };

    let new_data = Data {
        stable: "same".into(),
        temp_cache: "new_cache".into(),
        temp_buffer: "new_buffer".into(),
    };

    // Ignore all fields starting with "temp"
    let config = DiffConfig::new()
        .comparator(
            "temp_cache",
            Rc::new(|_old, _new| true), // Always equal
        )
        .comparator(
            "temp_buffer",
            Rc::new(|_old, _new| true), // Always equal
        );

    let d = diff_with(&old_data, &new_data, &config).unwrap();
    println!("   Changes after ignoring temp fields: {}", d.len());
    println!();

    // 5. Using ValueExt helpers
    println!("5. Using ValueExt Helper Methods");
    #[derive(Serialize)]
    struct Product {
        id: u64,
        name: String,
        price: f64,
    }

    let old_product = Product {
        id: 1,
        name: "Widget".into(),
        price: 9.99,
    };

    let new_product = Product {
        id: 1,
        name: "Widget".into(),
        price: 10.99, // Price changed
    };

    // Compare by ID and name only, ignore price
    let config = DiffConfig::new().comparator(
        "",
        Rc::new(|old, new| {
            let id_match = old.get_field("id") == new.get_field("id");
            let name_match = old.get_field("name") == new.get_field("name");
            id_match && name_match
        }),
    );

    let d = diff_with(&old_product, &new_product, &config).unwrap();
    println!("   Changes when ignoring price: {}", d.len());

    println!("\n=== Summary ===");
    println!("Custom comparators allow you to:");
    println!("  - Ignore specific fields (timestamps, caches)");
    println!("  - Compare by key fields only (ID-based equality)");
    println!("  - Apply custom logic (case-insensitive, normalized URLs)");
    println!("  - Use ValueExt helpers for convenient field access");
    println!("\nNote: Array element wildcards (e.g., [*]) have limitations due to glob");
    println!("pattern syntax. Use explicit patterns like ?0?, ?1? for small arrays.");
}