Skip to content

brunoga/deep

Repository files navigation

Deep v5: The High-Performance Type-Safe Synchronization Toolkit

deep is a comprehensive Go library for comparing, cloning, and synchronizing complex data structures. Deep introduces a revolutionary architecture centered on Code Generation and Type-Safe Selectors, delivering up to 15x performance improvements over traditional reflection-based libraries.

Key Features

  • Extreme Performance: Reflection-free operations via deep-gen (10x-20x faster than v4).
  • Compile-Time Safety: Type-safe field selectors replace brittle string paths.
  • Data-Oriented: Patches are pure, flat data structures, natively serializable to JSON.
  • Integrated Causality: Native support for HLC (Hybrid Logical Clocks) and LWW (Last-Write-Wins).
  • First-Class CRDTs: Built-in support for Text and LWW[T] convergent registers.
  • Standard Compliant: Export to RFC 6902 JSON Patch with advanced predicate extensions.
  • Hybrid Architecture: Optimized generated paths with a robust reflection safety net.

Performance Comparison (Deep Generated vs v4 Reflection)

Benchmarks performed on typical struct models (User with IDs, Names, Slices):

Operation v4 (Reflection) Deep (Generated) Speedup
Apply Patch 726 ns/op 50 ns/op 14.5x
Diff + Apply 2,391 ns/op 270 ns/op 8.8x
Clone 1,872 ns/op 290 ns/op 6.4x
Equality 202 ns/op 84 ns/op 2.4x

Run go test -bench=. ./... to reproduce. BenchmarkApplyGenerated uses generated code; BenchmarkApplyReflection uses the fallback path on a type with no generated code.

Quick Start

1. Define your models

type User struct {
    ID    int            `json:"id"`
    Name  string         `json:"name"`
    Roles []string       `json:"roles"`
    Score map[string]int `json:"score"`
}

2. Generate optimized code

Add a go:generate directive to your source file:

//go:generate go run github.com/brunoga/deep/v5/cmd/deep-gen -type=User .

Then run:

go generate ./...

This writes user_deep.go in the same directory. Commit it alongside your source.

3. Use the Type-Safe API

import deep "github.com/brunoga/deep/v5"

u1 := User{ID: 1, Name: "Alice", Roles: []string{"user"}}
u2 := User{ID: 1, Name: "Bob", Roles: []string{"user", "admin"}}

// State-based Diffing
patch, err := deep.Diff(u1, u2)
if err != nil {
    log.Fatal(err)
}

// Operation-based Building (Fluent, Type-Safe API)
namePath  := deep.Field(func(u *User) *string        { return &u.Name  })
scorePath := deep.Field(func(u *User) *map[string]int { return &u.Score })

patch2 := deep.Edit(&u1).
    With(
        deep.Set(namePath, "Alice Smith"),
        deep.Add(deep.MapKey(scorePath, "power"), 100),
    ).
    Build()

// Application
if err := deep.Apply(&u1, patch); err != nil {
    log.Fatal(err)
}

Advanced Features

Integrated CRDTs

Convert any field into a convergent register:

type Document struct {
    Title   crdt.LWW[string] // Native Last-Write-Wins
    Content crdt.Text        // Collaborative Text CRDT
}

Conditional Patching

Conditions travel with the patch and are enforced wherever it is applied — including on remote peers via JSON Patch interop.

Global guard — the entire patch is a no-op if the condition fails. Use this for state-machine transitions where partial application would leave things in a bad state:

statusPath := deep.Field(func(o *Order) *string { return &o.Status })

// Atomically transition to "shipped" only if the order is currently "paid".
patch := deep.Edit(&order).
    With(deep.Set(statusPath, "shipped")).
    Guard(deep.Eq(statusPath, "paid")).
    Build()

Per-operation conditions (Op.If / Op.Unless) — individual operations are skipped independently within a single patch. Use this when some fields are conditional but others should always be updated:

paidAtPath  := deep.Field(func(i *Invoice) *time.Time { return &i.PaidAt  })
feePath     := deep.Field(func(i *Invoice) *float64   { return &i.LateFee })
balancePath := deep.Field(func(i *Invoice) *float64   { return &i.Balance })

// Always record the payment time; only add a late fee if a balance is overdue.
patch := deep.Edit(&invoice).
    With(deep.Set(paidAtPath, time.Now())).
    With(deep.Set(feePath, 25.0).If(deep.Gt(balancePath, 0.0))).
    Build()

Observability

Embed OpLog operations in a patch to emit structured trace messages during Apply. Route them to any *slog.Logger — useful for request-scoped loggers, test capture, or tracing without touching your model types:

namePath := deep.Field(func(u *User) *string { return &u.Name })

patch := deep.Edit(&u).
    Log("starting update").
    With(deep.Set(namePath, "Alice Smith")).
    Log("update complete").
    Build()

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
deep.Apply(&u, patch, deep.WithLogger(logger))
// {"level":"INFO","msg":"deep log","message":"starting update","path":"/"}
// {"level":"INFO","msg":"deep log","message":"update complete","path":"/"}

When no logger is provided, slog.Default() is used — so existing slog.SetDefault configuration is respected without any extra wiring.

Patch Utilities

// Reverse a patch to produce an undo patch.
undo := patch.Reverse()

// Enable strict mode — Apply verifies Old values match before each operation.
strictPatch := patch.AsStrict()

Undo/Redo with CRDTs

CRDT[T].Reverse applies the inverse of a delta to the local node and returns a new Delta[T] with a fresh HLC timestamp, making it causally after the original edit and safe to propagate to peers:

// Apply an edit, then undo it.
delta, _ := node.Edit(func(v *MyDoc) { v.Title = "Draft" })
undo := node.Reverse(delta)   // applies inverse locally, returns undo delta

// Redo: reverse the undo.
redo := node.Reverse(undo)

Standard Interop

Export your Deep patches to standard RFC 6902 JSON Patch format, and parse them back:

jsonData, err := patch.ToJSONPatch()
// Output: [{"op":"replace","path":"/name","value":"Bob"}]

restored, err := deep.ParseJSONPatch[User](jsonData)

JSON deserialization note: When a patch is JSON-encoded and then decoded, numeric values in Operation.Old and Operation.New are unmarshaled as float64 (standard Go JSON behavior). Generated Patch methods handle this automatically with numeric coercion. If you use the reflection fallback, be aware of this when inspecting Old/New directly.

Architecture: Why v5?

v4 used a Recursive Tree Patch model. Every field was a nested patch object. While flexible, this caused high memory allocations and made serialization difficult.

Deep uses a Flat Operation Model. A patch is a simple slice of Operations. This makes patches:

  1. Portable: Trivially serializable to any format.
  2. Fast: Iterating a slice is much faster than traversing a tree.
  3. Composable: Merging two patches is a stateless operation.

License

Apache 2.0

About

Deep: High-Performance Data Manipulation for Go

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages