# diffo
**Semantic diffing for Rust structs via serde.**
[](https://crates.io/crates/diffo)
[](https://docs.rs/diffo)
[](LICENSE)
## Features
- ð **Zero boilerplate** - works with any `Serialize` type
- ðŊ **Path-based changes** - `user.roles[2].name`
- ðĻ **Multiple formatters** - pretty, JSON, JSON Patch (RFC 6902), Markdown
- ð **Secret masking** - hide sensitive fields
- ð§ **Configurable** - float tolerance, depth limits, collection caps
- ⥠**Fast** - optimized for production use
## Quick Start
```rust
use diffo::diff;
use serde::Serialize;
#[derive(Serialize)]
struct Config {
version: String,
port: u16,
}
let old = Config { version: "1.0".into(), port: 8080 };
let new = Config { version: "1.1".into(), port: 8080 };
let d = diff(&old, &new).unwrap();
println!("{}", d.to_pretty());
```
**Output:**
```
version
- String("1.0")
+ String("1.1")
```
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
diffo = "0.1"
```
## Examples
### Basic Usage
```rust
use diffo::diff;
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
let old = User {
id: 1,
name: "Alice".into(),
email: "alice@old.com".into()
};
let new = User {
id: 1,
name: "Alice Smith".into(),
email: "alice@new.com".into()
};
let diff = diff(&old, &new).unwrap();
// Check specific changes
assert!(diff.get("name").is_some());
assert!(diff.get("email").is_some());
assert!(diff.get("id").is_none());
```
### Masking Secrets
```rust
use diffo::{diff_with, DiffConfig};
let config = DiffConfig::new()
.mask("*.password")
.mask("api.secret");
let diff = diff_with(&old, &new, &config).unwrap();
```
### Float Tolerance
```rust
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();
```
### Output Formats
```rust
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:**
```json
[
{
"op": "replace",
"path": "/name",
"value": "Alice Smith"
},
{
"op": "replace",
"path": "/email",
"value": "alice@new.com"
}
]
```
**Markdown Output:**
| name | Modified | "Alice" | "Alice Smith" |
| email | Modified | "alice@old.com" | "alice@new.com" |
## Use Cases
- **Config changes** - Track configuration drift
- **Audit logs** - Record what changed in entities
- **API testing** - Compare expected vs actual responses
- **Migration tools** - Validate data transformations
- **Database migrations** - Verify schema changes
- **CI/CD** - Detect unintended changes
## Advanced Configuration
```rust
use diffo::DiffConfig;
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);
let diff = diff_with(&old, &new, &config)?;
```
## Edge Cases Handled
- **Floats**: NaN == NaN, -0.0 == +0.0
- **Large collections**: Automatic elision with configurable limits
- **Large byte arrays**: Hex previews instead of full dumps
- **Deep nesting**: Configurable depth limits
- **Type mismatches**: Clear reporting when types differ
## Performance
Diffo is designed for production use:
- **O(n)** complexity for most operations (n = number of fields)
- **Index-based** sequence diffing (not LCS, which is O(nÂē))
- **Efficient** path representation with BTreeMap
- **Zero-copy** where possible
Typical performance (on modest hardware):
- Small struct (5 fields): ~1-2 Ξs
- Medium struct (50 fields): ~10-20 Ξs
- Large struct (500 fields): ~100-200 Ξs
## Comparison with Alternatives
| Path notation | â
| â | â
|
| Secret masking | â
| â | â |
| JSON Patch (RFC 6902) | â
| â | â
|
| Float tolerance | â
| â | â |
| Works with any Serialize | â
| â
| JSON only |
| Multiple formatters | â
| â | â |
| Collection limits | â
| â | â |
## Roadmap
### v0.2
- [ ] LCS-based sequence diffing (patience algorithm)
- [ ] Configurable sequence diff strategies
- [ ] Custom comparator functions per path
### v0.3
- [ ] Path interning for performance
- [ ] Streaming diff for very large structures
- [ ] Diff application (apply changes to produce new value)
### v1.0
- [ ] Stable API guarantee
- [ ] Comprehensive benchmarks vs alternatives
- [ ] Performance optimization guide
## Contributing
Contributions are welcome! Please open an issue or PR on GitHub.
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Acknowledgments
Built with:
- [serde](https://serde.rs/) - Serialization framework
- [serde-value](https://github.com/arcnmx/serde-value) - Generic value representation