From 2b2e012dc47b88d43d87a41e73b0bf34b95a510a Mon Sep 17 00:00:00 2001 From: Paul Frederiksen Date: Fri, 16 Jan 2026 15:03:50 -0800 Subject: [PATCH] Add comprehensive documentation and examples - Remove WIP status, mark project as production ready - Add detailed Quick Start with expected output - Add Examples section with 5 real-world scenarios: - Ignore specific paths with wildcards - Array-as-set comparison by key field - Type coercions for semantic equivalence - Cross-format YAML/JSON comparison - Kubernetes deployment diff example - Add complete API Reference section: - Main functions (DiffBytes, DiffYAML, DiffJSON, DiffTrees) - Options and Coercions types with documentation - Result and Change type definitions - Report generation functions - Add Testing section with coverage details - Update Project Status to show all features complete (84.8% coverage) - Add Future Enhancements section Co-Authored-By: Claude Sonnet 4.5 --- README.md | 392 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 375 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2750dfb..d2966b1 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,6 @@ Semantic, human-grade diffs for YAML/JSON/HCL configuration files. Perfect for GitOps reviews, CI checks, configuration drift detection, and any scenario where you need to understand what actually changed in your config files. -## Status - -🚧 **Work in Progress** - Initial development in progress. - ## Installation ```bash @@ -38,19 +34,36 @@ func main() { oldYAML := []byte(` name: myapp replicas: 3 +image: nginx:1.19 `) newYAML := []byte(` name: myapp replicas: 5 +image: nginx:1.20 +env: production `) - result, err := configdiff.DiffBytes(oldYAML, "yaml", newYAML, "yaml", configdiff.Options{}) + result, err := configdiff.DiffYAML(oldYAML, newYAML, configdiff.Options{}) if err != nil { panic(err) } + // Human-friendly report fmt.Println(result.Report) + // Output: + // Summary: +1 added, ~2 modified (3 total) + // + // Changes: + // + /env = "production" + // + // ~ /image: "nginx:1.19" → "nginx:1.20" + // + // ~ /replicas: 3 → 5 + + // Machine-readable patch + patchJSON, _ := result.Patch.ToJSONIndent() + fmt.Println(string(patchJSON)) } ``` @@ -94,8 +107,321 @@ opts := configdiff.Options{ ### Multiple Output Formats -- **Machine-readable patches**: JSON Patch-like operations for programmatic consumption -- **Pretty reports**: Human-friendly, concise output with context +**Machine-readable patches** (JSON Patch-like): +```json +{ + "operations": [ + { + "op": "add", + "path": "/env", + "value": "production" + }, + { + "op": "replace", + "path": "/image", + "value": "nginx:1.20" + }, + { + "op": "replace", + "path": "/replicas", + "value": 5 + } + ] +} +``` + +**Pretty reports** with configurable verbosity: +```go +// Detailed report (default) +report.GenerateDetailed(changes) +// Summary: +1 added, ~2 modified (3 total) +// +// Changes: +// + /env = "production" +// ~ /image: "nginx:1.19" → "nginx:1.20" +// ~ /replicas: 3 → 5 + +// Compact report (paths only) +report.GenerateCompact(changes) +// Summary: +1 added, ~2 modified (3 total) +// Changes: +// + /env +// ~ /image +// ~ /replicas + +// Custom formatting +report.Generate(changes, report.Options{ + Compact: false, + ShowValues: true, + MaxValueLength: 50, // Truncate long values +}) +``` + +## Examples + +### Ignore Specific Paths + +Useful for ignoring timestamps, auto-generated fields, or status information: + +```go +opts := configdiff.Options{ + IgnorePaths: []string{ + "/metadata/creationTimestamp", + "/metadata/generation", + "/status", // Exact match + "/status/*", // Wildcard: ignores all fields under /status + }, +} + +result, _ := configdiff.DiffYAML(oldK8s, newK8s, opts) +``` + +### Array-as-Set Comparison + +Compare arrays by a key field instead of position: + +```go +oldYAML := []byte(` +spec: + containers: + - name: nginx + image: nginx:1.19 + - name: sidecar + image: busybox:latest +`) + +newYAML := []byte(` +spec: + containers: + - name: sidecar + image: busybox:1.36 # Reordered + changed + - name: nginx + image: nginx:1.20 # Changed +`) + +opts := configdiff.Options{ + ArraySetKeys: map[string]string{ + "/spec/containers": "name", // Match containers by "name" field + }, +} + +result, _ := configdiff.DiffYAML(oldYAML, newYAML, opts) +// Output: +// Summary: ~2 modified (2 total) +// +// Changes: +// ~ /spec/containers[name=nginx]/image: "nginx:1.19" → "nginx:1.20" +// ~ /spec/containers[name=sidecar]/image: "busybox:latest" → "busybox:1.36" +``` + +### Type Coercions + +Handle semantic equivalence across type boundaries: + +```go +jsonConfig := []byte(`{"replicas": 3, "enabled": true}`) +yamlConfig := []byte(` +replicas: "3" # String in YAML +enabled: "true" # String in YAML +`) + +opts := configdiff.Options{ + Coercions: configdiff.Coercions{ + NumericStrings: true, // "3" == 3 + BoolStrings: true, // "true" == true + }, +} + +result, _ := configdiff.DiffBytes(jsonConfig, "json", yamlConfig, "yaml", opts) +// No differences detected due to coercion +``` + +### Cross-Format Comparison + +Compare YAML and JSON representations: + +```go +yamlConfig := []byte(` +database: + host: localhost + port: 5432 +`) + +jsonConfig := []byte(`{ + "database": { + "host": "localhost", + "port": 5432 + } +}`) + +result, _ := configdiff.DiffBytes(yamlConfig, "yaml", jsonConfig, "json", configdiff.Options{}) +// No differences - semantically identical +``` + +### Kubernetes Deployment Diff + +Real-world example comparing Kubernetes deployments: + +```go +package main + +import ( + "fmt" + "github.com/pfrederiksen/configdiff" +) + +func main() { + oldDeploy := []byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + generation: 1 +spec: + replicas: 3 + template: + spec: + containers: + - name: app + image: myapp:v1.0 + resources: + limits: + memory: 512Mi +`) + + newDeploy := []byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + generation: 2 +spec: + replicas: 5 + template: + spec: + containers: + - name: app + image: myapp:v1.1 + resources: + limits: + memory: 1Gi + - name: sidecar + image: envoy:v1.20 +`) + + opts := configdiff.Options{ + IgnorePaths: []string{ + "/metadata/generation", // Auto-incremented + }, + ArraySetKeys: map[string]string{ + "/spec/template/spec/containers": "name", + }, + StableOrder: true, + } + + result, _ := configdiff.DiffYAML(oldDeploy, newDeploy, opts) + fmt.Println(result.Report) + // Output: + // Summary: +1 added, ~3 modified (4 total) + // + // Changes: + // + /spec/template/spec/containers[name=sidecar] = {...} (2 keys) + // + // ~ /spec/replicas: 3 → 5 + // + // ~ /spec/template/spec/containers[name=app]/image: "myapp:v1.0" → "myapp:v1.1" + // + // ~ /spec/template/spec/containers[name=app]/resources/limits/memory: "512Mi" → "1Gi" +} +``` + +## API Reference + +### Main Functions + +```go +// DiffBytes compares two configuration byte slices +func DiffBytes(a []byte, aFormat string, b []byte, bFormat string, opts Options) (*Result, error) + +// DiffYAML is a convenience function for YAML-only comparison +func DiffYAML(a, b []byte, opts Options) (*Result, error) + +// DiffJSON is a convenience function for JSON-only comparison +func DiffJSON(a, b []byte, opts Options) (*Result, error) + +// DiffTrees compares pre-parsed tree nodes +func DiffTrees(a, b *tree.Node, opts Options) (*Result, error) +``` + +### Options + +```go +type Options struct { + // IgnorePaths: List of paths to ignore during comparison + // Supports wildcards: "/status/*" matches all fields under /status + IgnorePaths []string + + // ArraySetKeys: Map of array paths to key fields + // Treats arrays as sets, matching elements by the specified field + // Example: map[string]string{"/spec/containers": "name"} + ArraySetKeys map[string]string + + // Coercions: Type coercion rules for semantic comparison + Coercions Coercions + + // StableOrder: Sort changes deterministically for reproducible output + StableOrder bool +} + +type Coercions struct { + // NumericStrings: Treat numeric strings as numbers ("42" == 42) + NumericStrings bool + + // BoolStrings: Treat bool strings as booleans ("true" == true) + BoolStrings bool +} +``` + +### Result + +```go +type Result struct { + // Changes: List of detected changes + Changes []Change + + // Patch: Machine-readable patch operations + Patch *Patch + + // Report: Human-friendly formatted report + Report string +} + +type Change struct { + Type ChangeType // Add, Remove, Modify, Move + Path string // JSON Pointer-like path + OldValue *tree.Node // Previous value (nil for Add) + NewValue *tree.Node // New value (nil for Remove) +} +``` + +### Report Generation + +```go +// Generate creates a report with custom options +func Generate(changes []Change, opts Options) string + +// GenerateDetailed creates a detailed report with values +func GenerateDetailed(changes []Change) string + +// GenerateCompact creates a compact report with paths only +func GenerateCompact(changes []Change) string + +type Options struct { + Compact bool // If true, only show paths + ShowValues bool // If true, include old/new values + MaxValueLength int // Truncate values longer than this (0 = no limit) +} +``` ## Use Cases @@ -103,19 +429,51 @@ opts := configdiff.Options{ - **CI/CD Checks**: Validate configuration changes before deployment - **Drift Detection**: Compare actual vs desired state in deployed systems - **Configuration Management**: Track changes across environments +- **Multi-Format Comparison**: Compare YAML and JSON representations of the same config + +## Testing + +The project maintains high test coverage (>80%) with comprehensive test suites: + +- **Unit Tests**: Table-driven tests for all core functionality +- **Golden Tests**: Reference output files in `testdata/` for report formatting validation +- **Integration Tests**: End-to-end scenarios covering real-world use cases +- **CI/CD**: Automated testing on multiple Go versions (1.21, 1.22) with coverage enforcement + +Run tests: +```bash +# Run all tests +go test ./... + +# Run with coverage +go test -cover ./... + +# Run with race detector +go test -race ./... + +# Update golden test files +go test ./report -update +``` + +## Project Status + +**Production Ready** - All core features implemented and tested: -## Development Status +- [x] Repository setup with CI/CD +- [x] Tree package with normalized representation +- [x] YAML/JSON parsing with format detection +- [x] Semantic diff engine with customizable rules +- [x] JSON Patch-like operations +- [x] Human-friendly report generation +- [x] Comprehensive test coverage (84.8%) +- [x] Full API documentation and examples -This project is under active development. Current progress: +### Future Enhancements -- [x] Repository setup -- [ ] Tree package implementation -- [ ] YAML/JSON parsing -- [ ] Diff engine -- [ ] Patch format -- [ ] Pretty reporting -- [ ] Comprehensive tests -- [ ] Full documentation +- HCL format support (experimental) +- Additional coercion rules +- Performance optimizations for very large configs +- CLI tool for command-line usage ## Contributing