Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 59 additions & 34 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,84 @@
# Development Guidelines
# CLAUDE.md

This document contains the critical information about working with the project codebase.
Follows these guidelines precisely to ensure consistency and maintainability of the code.
This file provides guidance to Claude Code (claude.ai/code) and other AI
assistants when working with code in this repository. Follow these guidelines
precisely to ensure consistency and maintainability.

## Stack

- Language: Go (Go 1.26)
- Framework: Go standard library
- Language: Go (Go 1.26+)
- Framework: Go standard library **only** — this library is intentionally
dependency-free
- Testing: Go's built-in testing package
- Dependency Management: Go modules
- Version Control: Git
- Documentation: go doc
- Documentation: `go doc` / pkg.go.dev, and Markdown files under `docs/`
- Code Review: Pull requests on GitHub
- CI/CD: GitHub Actions

## Project Structure

Since this is a library built in native Go, the files are organized following the
standard Go single-package layout in the repository root.

- Library files are located in the root directory.
- `*_examples_test.go` files contain executable Go examples rendered by pkg.go.dev.
- .github/ contains GitHub-specific files such as workflows for CI/CD.
- .gitignore specifies files and directories to be ignored by Git.
- .vscode/ contains Visual Studio Code configuration files.
- LICENSE is the license file for the project.
- README.md provides an overview of the project, installation instructions, usage examples, and other relevant information.
- go.mod declares the module and Go toolchain version.
- go.sum should not exist unless external dependencies are intentionally introduced.
- \*.go files contain the main source code of the library.
- \*\_test.go files contain the test cases for the library.
## Key Conventions

## Code Style

- Follow Go's idiomatic style defined in
- **Style:** Follow the Google Go Style Guide and Effective Go:
- <https://google.github.io/styleguide/go/guide>
- <https://google.github.io/styleguide/go/decisions>
- <https://google.github.io/styleguide/go/best-practices>
- <https://golang.org/doc/effective_go.html>
- <https://go.dev/doc/effective_go>
- Keep functions small and focused on a single task; use dependency injection
for testability.
- Use meaningful names for variables, functions, and packages.
- Keep functions small and focused on a single task.
- Use comments to explain complex logic or decisions.
- Use `any`, never `interface{}`.
- Prefer `for b.Loop()` over `for i := 0; i < b.N; i++` in benchmarks (Go 1.24+).
- don't use `interface{}` instead use `any` for better readability.
- **Tests:** Table-driven tests where practical. Test files are co-located with
source (`*_test.go`). Executable examples live in `*_examples_test.go` and are
verified by `go test`.
- **No external dependencies:** Do not add third-party modules without explicit
approval. `go.sum` should not exist unless a dependency is intentionally
introduced.

## Project Structure

This is a single-package Go library; source files live in the repository root.

- `*.go` — library source code.
- `*_test.go` — unit tests and benchmarks.
- `*_examples_test.go` — executable examples rendered by pkg.go.dev.
- `doc.go` — package-level documentation.
- `docs/` — extensive usage documentation in Markdown.
- `.github/` — GitHub Actions workflows, CodeQL, Dependabot, release metadata.
- `.golangci.yaml` — optional local golangci-lint configuration.
- `.vscode/` — editor settings.
- `LICENSE` — Apache License 2.0.
- `README.md` — project overview and usage guide.
- `SECURITY.md` — vulnerability reporting policy.
- `go.mod` — module definition with no external requirements.

## Post-Change Checklist

Use standard Go commands after making changes:
Always run these after making changes, before committing:

```bash
go fix ./...
go fmt ./...
go vet ./...
go fmt ./... # Format code
go fix ./... # Apply Go 1.26+ modernizations
betteralign -apply ./... # Align struct fields for optimal memory layout (if installed)
go vet ./... # Vet
golangci-lint run ./... # Lint (if installed)
go test -race -coverprofile=/tmp/comparator-coverage.txt -covermode=atomic ./...
go build ./...
go build ./... # Verify build
```

Do not add external module dependencies without explicit approval; this project is intentionally standard-library-only.
Do not add external module dependencies without explicit approval; this project
is intentionally standard-library-only.

## Commit Message & Pull Request Guidelines

- Always work on a new branch unless explicitly told to use the current one.
- Use semantic (Conventional Commits) messages, e.g. `feat:`, `fix:`, `docs:`,
`test:`, `chore:`, `refactor:`.
- Keep the commit subject under 72 characters and the whole message concise.
- Group related changes into focused commits rather than one large commit.
- Open pull requests against `main` with a semantic title (e.g.
`feat: add JSON-tag-aware paths`) and a description that summarizes the
changes and references any related issues.
- Keep changes small, idiomatic, tested, documented, and dependency-free unless
there is a clear reason to expand the project scope.
1 change: 1 addition & 0 deletions AGENTS.md
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ Only the Go standard library is used. There are no third-party dependencies.
- 🔍 **Difference detection** — detailed, path-addressed reports of what changed
- 🎨 **Multiple output formats** — text (with ANSI color), JSON, Markdown, HTML
- 📐 **Unified diff** — Unix `diff`-style output
- 🩹 **JSON Patch** — RFC 6902 patch documents
- 🩹 **JSON Patch** — generate *and apply* RFC 6902 patch documents
- 🌳 **Visual tree diff** — hierarchical representation for viewers
- 🧠 **Custom comparators** — register domain-specific equality per type
- 📊 **Statistics & suggestions** — metrics and actionable hints
- 🧠 **Custom equality** — per-type comparators plus auto-detected
`Equatable`/`Comparable` interfaces
- 🔤 **Type-safe generics** — `EqualT[T]`, `DiffT[T]`, and friends
- ⏱️ **Context-aware** — cancel or bound long comparisons with `context.Context`
- 📡 **Streaming reporter** — observe each difference as it is found
- 🎨 **Pluggable formatters** — register custom output formats (CSV, JUnit, …)
- 🧪 **Testing helpers** — `AssertEqual` / `RequireEqual` for test suites
- ⚙️ **Configurable behavior** — float precision, slice-order insensitivity,
field ignoring, depth limits, NaN handling, and more
field/path/map-key ignoring, depth limits, NaN handling, and more
- 🚫 **Zero third-party dependencies** — standard library only
- 📄 **Apache-2.0 licensed**

Expand Down Expand Up @@ -131,8 +136,10 @@ on [pkg.go.dev](https://pkg.go.dev/github.com/slashdevops/comparator).
| ⚙️ [Configuration & Options](docs/configuration.md) | Every option, its default, and when to use it. |
| 🔍 [Diffing](docs/diffing.md) | `DiffResult`, `Difference`, diff modes. |
| 🎨 [Output Formats](docs/output-formats.md) | Text/color, JSON, Markdown, HTML, unified, visual. |
| 🩹 [JSON Patch](docs/json-patch.md) | Generating RFC 6902 patch documents. |
| 🧠 [Custom Comparators](docs/custom-comparators.md) | Domain-specific equality logic. |
| 🩹 [JSON Patch](docs/json-patch.md) | Generating and applying RFC 6902 patches. |
| 🧠 [Custom Comparators](docs/custom-comparators.md) | Custom equality + `Equatable`/`Comparable`. |
| 🧩 [Extensibility](docs/extensibility.md) | Generics, reporters, context, pluggable formatters. |
| 🧪 [Testing](docs/testing.md) | `AssertEqual` and friends for test suites. |
| ⚡ [Performance](docs/performance.md) | Cost model, benchmarks, tuning. |
| ❓ [FAQ](docs/faq.md) | Gotchas, thread-safety, common questions. |

Expand All @@ -144,17 +151,26 @@ on [pkg.go.dev](https://pkg.go.dev/github.com/slashdevops/comparator).
| `IgnoreSliceOrder()` | `false` | Treat slices as sets (ignore order). |
| `WithMaxDepth(int)` | `0` (unlimited) | Limit recursion depth. |
| `IgnoreUnexported()` | `false` | Skip unexported struct fields. |
| `EquateEmpty()` | `true` | Treat nil and empty containers as equal. |
| `WithEquateEmpty(bool)` | `true` | Toggle nil/empty-container equivalence (both directions). |
| `EquateNaNs()` | `false` | Treat NaN values as equal. |
| `IgnoreStructFields(...string)` | none | Skip specific struct fields by name. |
| `WithIgnorePaths(...string)` | none | Skip struct fields at exact canonical paths. |
| `WithIgnorePathPatterns(...string)` | none | Skip struct fields whose path matches a regexp. |
| `WithIgnoreMapKeys(...string)` | none | Skip map entries by key. |
| `WithFieldNaming(FieldNaming)` | `GoFieldNaming` | Use Go field names or `json` tag names in paths/pointers. |
| `WithTimeLayout(string)` | `time.RFC3339Nano` | Layout for rendering `time.Time`. |
| `WithCustomComparator[T](func(T, T) bool)` | none | Register custom equality for a type. |
| `WithReporter(func(Difference))` | none | Stream each difference to a callback. |
| `WithDiffMode(DiffMode)` | `DiffModeSimple` | Diff detail level. |
| `WithMaxDiffs(int)` | `1000` | Cap the number of differences collected. |
| `WithOutputFormat(string)` | `"text"` | `text`, `json`, `markdown`, or `html`. |
| `WithOutputFormat(string)` | `"text"` | `text`, `json`, `markdown`, `html`, or a registered format. |
| `WithColorize(bool)` | `false` | ANSI colors in text output. |
| `WithIncludeEqual(bool)` | `false` | Include equal values in reports. |

Struct fields tagged `comparator:"-"` are always skipped. See
[Configuration & Options](docs/configuration.md) and
[Extensibility](docs/extensibility.md) for the full set.

See [Configuration & Options](docs/configuration.md) for details.

## 🧵 Thread Safety
Expand Down Expand Up @@ -199,12 +215,19 @@ go build ./...
|-- .github/ GitHub Actions, CodeQL, Dependabot, release metadata
|-- .golangci.yaml Optional local golangci-lint configuration
|-- docs/ Extensive usage documentation
|-- AGENTS.md AI assistant guidance (symlink to copilot-instructions)
|-- doc.go Package documentation rendered by pkg.go.dev
|-- comparator.go Public comparison and diffing API
|-- comparator_test.go Unit tests and benchmarks
|-- comparator_examples_test.go Executable examples
|-- comparator_api_examples_test.go Executable examples
|-- comparator_options_examples_test.go Executable examples
|-- comparator.go Core comparison and diffing engine
|-- errors.go Sentinel errors
|-- generics.go Type-safe generic API (EqualT, DiffT, ...)
|-- context.go Context-aware comparison
|-- patch.go ApplyJSONPatch (RFC 6902 applier)
|-- formatters.go Pluggable output formatter registry
|-- options_ext.go Additional functional options
|-- stringer.go String() helpers for diff types
|-- assert.go Testing helpers (AssertEqual, ...)
|-- *_test.go Unit tests and benchmarks
|-- *_examples_test.go Executable examples
|-- go.mod Module definition with no external requirements
|-- LICENSE Apache License 2.0
|-- README.md Project overview and usage guide
Expand Down
84 changes: 84 additions & 0 deletions assert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package comparator

import (
"fmt"
"strings"
)

// TestingT is the subset of *testing.T (and *testing.B) used by the assertion
// helpers. Accepting an interface avoids importing the testing package into
// production builds and lets callers supply their own compatible type.
type TestingT interface {
Helper()
Errorf(format string, args ...any)
}

// tbFatal is optionally implemented by *testing.T/*testing.B via Fatalf. When
// available, the Require* helpers use it to stop the test immediately.
type tbFatal interface {
Fatalf(format string, args ...any)
}

// AssertEqual fails the test (via t.Errorf) when want and got are not equal
// under the given options, printing a readable diff of the differences. It
// returns true when the values are equal. Use it as a drop-in equality check in
// tests:
//
// comparator.AssertEqual(t, wantUser, gotUser, comparator.IgnoreStructFields("ID"))
func AssertEqual(t TestingT, want, got any, opts ...Option) bool {
t.Helper()
if ok, msg := diffMessage(want, got, opts...); !ok {
t.Errorf("%s", msg)
return false
}
return true
}

// AssertNotEqual fails the test when want and got are equal under the given
// options. It returns true when the values differ.
func AssertNotEqual(t TestingT, want, got any, opts ...Option) bool {
t.Helper()
if EqualT(want, got, opts...) {
t.Errorf("expected values to differ, but they are equal")
return false
}
return true
}

// RequireEqual behaves like AssertEqual but stops the test immediately (via
// Fatalf when t supports it) instead of merely marking it failed.
func RequireEqual(t TestingT, want, got any, opts ...Option) {
t.Helper()
if ok, msg := diffMessage(want, got, opts...); !ok {
fail(t, msg)
}
}

// diffMessage reports whether want and got are equal and, when not, a
// human-readable multi-line message describing the differences.
func diffMessage(want, got any, opts ...Option) (bool, string) {
comp := NewDiffComparer(opts...)
result := comp.CompareWithDiff(want, got)
if result.Equal {
return true, ""
}

var b strings.Builder
b.WriteString("values are not equal:\n")
b.WriteString(result.Summary)
for _, d := range result.Differences {
if d.Detail.Type == "equal" {
continue
}
fmt.Fprintf(&b, "\n %s: %s", d.Path, d.Message)
}
return false, b.String()
}

func fail(t TestingT, msg string) {
if ft, ok := t.(tbFatal); ok {
ft.Fatalf("%s", msg)
return
}
t.Errorf("%s", msg)
}
Loading