| Crates.io | diffo |
| lib.rs | diffo |
| version | 0.2.0 |
| created_at | 2025-10-08 15:02:06.649572+00 |
| updated_at | 2025-10-12 12:29:57.907399+00 |
| description | Semantic diffing for Rust structs via serde |
| homepage | https://github.com/PiotrGrzybowski/diffo |
| repository | https://github.com/PiotrGrzybowski/diffo |
| max_upload_size | |
| id | 1874077 |
| size | 1,392,425 |
Semantic diffing for Rust structs via serde.
Compare any two Rust values that implement Serialize and get a detailed, human-readable diff showing exactly what changed. Perfect for audit logs, API testing, config validation, and debugging.
[dependencies]
diffo = "0.2"
use diffo::diff;
use serde::Serialize;
#[derive(Serialize)]
struct User {
name: String,
email: String,
}
let old = User {
name: "Alice".into(),
email: "alice@old.com".into()
};
let new = User {
name: "Alice Smith".into(),
email: "alice@new.com".into()
};
let d = diff(&old, &new).unwrap();
println!("{}", d.to_pretty());
Output:
name
- "Alice"
+ "Alice Smith"
email
- "alice@old.com"
+ "alice@new.com"
Serialize typeuser.roles[2].nameuse diffo::diff;
use serde::Serialize;
#[derive(Serialize)]
struct User {
name: String,
email: String,
}
let old = User {
name: "Alice".into(),
email: "alice@old.com".into()
};
let new = User {
name: "Alice Smith".into(),
email: "alice@new.com".into()
};
let d = diff(&old, &new).unwrap();
// Check what changed
if !d.is_empty() {
println!("Found {} changes", d.len());
// Check specific paths
if let Some(change) = d.get("email") {
println!("Email changed: {:?}", change);
}
}
use diffo::{diff, apply};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, PartialEq)]
struct Config {
version: String,
enabled: bool,
}
let v1 = Config { version: "1.0".into(), enabled: true };
let v2 = Config { version: "2.0".into(), enabled: false };
// Create diff from v1 to v2
let forward = diff(&v1, &v2).unwrap();
// Apply forward diff: v1 + diff = v2
let result: Config = apply(&v1, &forward).unwrap();
assert_eq!(result, v2);
// Create reverse diff for undo
let backward = diff(&v2, &v1).unwrap();
let undone: Config = apply(&v2, &backward).unwrap();
assert_eq!(undone, v1);
use diffo::{diff_with, DiffConfig};
let config = DiffConfig::new()
.mask("*.password")
.mask("api.secret");
let diff = diff_with(&old, &new, &config).unwrap();
use diffo::DiffConfig;
let config = DiffConfig::new()
.float_tolerance("metrics.*", 1e-6)
.default_float_tolerance(1e-9);
let diff = diff_with(&old, &new, &config).unwrap();
Choose the right algorithm for your use case:
use diffo::{DiffConfig, SequenceDiffAlgorithm};
let config = DiffConfig::new()
// IndexBased: O(n) simple comparison (default, backward compatible)
.default_sequence_algorithm(SequenceDiffAlgorithm::IndexBased)
// Myers: O(ND) optimal edit distance
.sequence_algorithm("commits", SequenceDiffAlgorithm::Myers)
// Patience: O(N log N) human-intuitive, great for code/reorderings
.sequence_algorithm("users", SequenceDiffAlgorithm::Patience);
let diff = diff_with(&old, &new, &config).unwrap();
Algorithm comparison:
Define custom comparison logic for specific paths:
use diffo::{DiffConfig, ValueExt};
use std::rc::Rc;
let config = DiffConfig::new()
// Ignore timestamp fields
.comparator("", Rc::new(|old, new| {
old.get_field("id") == new.get_field("id") &&
old.get_field("name") == new.get_field("name")
// timestamp field ignored
}))
// Case-insensitive URL comparison
.comparator("url", Rc::new(|old, new| {
if let (Some(a), Some(b)) = (old.as_string(), new.as_string()) {
a.to_lowercase() == b.to_lowercase()
} else {
old == new
}
}));
let diff = diff_with(&old, &new, &config).unwrap();
Use cases:
Array element limitation:
Due to glob pattern syntax, array indices like [0], [1] require workaround patterns:
// For small, known-size arrays
.comparator("?0?", comparator) // Matches [0]
.comparator("?1?", comparator) // Matches [1]
// Note: ?0? = 3 chars, so won't match [10] (4 chars)
For dynamic arrays, consider comparing at the parent level or using .ignore() for specific indices.
let diff = diff(&old, &new).unwrap();
// Pretty format (human-readable)
println!("{}", diff.to_pretty());
// JSON format
let json = diff.to_json()?;
// JSON Patch (RFC 6902)
let patch = diff.to_json_patch()?;
// Markdown table (great for PRs)
let markdown = diff.to_markdown()?;
JSON Patch Output:
[
{
"op": "replace",
"path": "/name",
"value": "Alice Smith"
},
{
"op": "replace",
"path": "/email",
"value": "alice@new.com"
}
]
Markdown Output:
| Path | Change | Old Value | New Value |
|---|---|---|---|
| name | Modified | "Alice" | "Alice Smith" |
| Modified | "alice@old.com" | "alice@new.com" |
use diffo::{DiffConfig, SequenceDiffAlgorithm};
let config = DiffConfig::new()
// Ignore specific paths
.ignore("*.internal")
.ignore("metadata.timestamp")
// Mask sensitive data
.mask("*.password")
.mask("*.secret")
// Float comparison tolerance
.float_tolerance("metrics.*.value", 1e-6)
.default_float_tolerance(1e-9)
// Limit depth (prevent stack overflow)
.max_depth(32)
// Limit collection size
.collection_limit(500)
// Sequence diff algorithms (per-path or default)
.sequence_algorithm("logs", SequenceDiffAlgorithm::IndexBased)
.sequence_algorithm("commits", SequenceDiffAlgorithm::Myers)
.default_sequence_algorithm(SequenceDiffAlgorithm::Patience);
let diff = diff_with(&old, &new, &config)?;
Diffo is designed for production use with configurable algorithms:
Algorithm selection guide:
| Feature | diffo | serde-diff | json-diff |
|---|---|---|---|
| Path notation | ✅ | ❌ | ✅ |
| Secret masking | ✅ | ❌ | ❌ |
| JSON Patch (RFC 6902) | ✅ | ❌ | ✅ |
| Float tolerance | ✅ | ❌ | ❌ |
| Works with any Serialize | ✅ | ✅ | JSON only |
| Multiple formatters | ✅ | ❌ | ❌ |
| Collection limits | ✅ | ❌ | ❌ |
| Multiple diff algorithms | ✅ | ❌ | ❌ |
| Custom comparators | ✅ | ❌ | ❌ |
Contributions are welcome! Please open an issue or PR on GitHub.
This project is licensed under the MIT License - see the LICENSE file for details.
Built with: