Documentation
¶
Overview ¶
Package deep provides high-performance, type-safe deep diff, copy, equality, and patch-apply operations for Go values.
Architecture ¶
Deep operates on Patch values — flat, serializable lists of Operation records describing changes between two values of the same type. The four core operations are:
- Diff computes the patch from a to b.
- Apply applies a patch to a target pointer.
- Equal reports whether two values are deeply equal.
- Clone returns a deep copy of a value.
Code Generation ¶
For production use, run deep-gen to generate reflection-free implementations of all four operations for your types:
//go:generate go run github.com/brunoga/deep/v5/cmd/deep-gen -type=MyType .
Generated code is 4–14x faster than the reflection fallback and is used automatically — no API changes required. The reflection engine remains as a transparent fallback for types without generated code.
Patch Construction ¶
Patches can be computed via Diff or built manually with Edit. Typed operation constructors (Set, Add, Remove, Move, Copy) return an Op value that can be passed to Builder.With for a fluent, type-safe chain:
patch := deep.Edit(&user).
With(
deep.Set(nameField, "Alice"),
deep.Set(ageField, 30).If(deep.Gt(ageField, 0)),
).
Guard(deep.Gt(ageField, 18)).
Build()
Field creates type-safe path selectors from struct field accessors. At and MapKey extend paths into slices and maps with full type safety.
Conditions ¶
Per-operation guards are attached to Op values via Op.If and Op.Unless. A global patch guard is set via Builder.Guard or Patch.WithGuard. Conditions are serializable and survive JSON round-trips.
Causality and CRDTs ¶
The [crdt] sub-package provides [crdt.LWW], a generic Last-Write-Wins register; [crdt.CRDT], a concurrency-safe wrapper for any type; and [crdt.Text], a convergent collaborative text type.
Serialization ¶
Patch marshals to/from JSON natively. Patch.ToJSONPatch and ParseJSONPatch interoperate with RFC 6902 JSON Patch (with deep extensions for conditions and log operations).
Index ¶
- Constants
- func And(conds ...*condition.Condition) *condition.Condition
- func Apply[T any](target *T, p Patch[T], opts ...ApplyOption) error
- func ApplyOpReflection[T any](target *T, op Operation, logger *slog.Logger) error
- func Clone[T any](v T) T
- func CloneShared[T any](v T, memo *CloneMemo) T
- func Eq[T, V any](p Path[T, V], val V) *condition.Condition
- func Equal[T any](a, b T) bool
- func EscapePathKey(key string) string
- func Exists[T, V any](p Path[T, V]) *condition.Condition
- func Ge[T, V any](p Path[T, V], val V) *condition.Condition
- func Gt[T, V any](p Path[T, V], val V) *condition.Condition
- func In[T, V any](p Path[T, V], vals []V) *condition.Condition
- func Le[T, V any](p Path[T, V], val V) *condition.Condition
- func Lt[T, V any](p Path[T, V], val V) *condition.Condition
- func Matches[T, V any](p Path[T, V], regex string) *condition.Condition
- func Ne[T, V any](p Path[T, V], val V) *condition.Condition
- func Not(c *condition.Condition) *condition.Condition
- func Or(conds ...*condition.Condition) *condition.Condition
- func Type[T, V any](p Path[T, V], typeName string) *condition.Condition
- func UnescapePathKey(token string) string
- type ApplyError
- type ApplyOption
- type Builder
- type CloneMemo
- type ConflictResolver
- type DiffMemo
- type Op
- type OpKind
- type Operation
- type Patch
- type Path
- type VisitSet
Constants ¶
const ( OpAdd = engine.OpAdd OpRemove = engine.OpRemove OpReplace = engine.OpReplace OpMove = engine.OpMove OpCopy = engine.OpCopy OpLog = engine.OpLog // OpAlias makes Path hold the same object From resolves to — sharing, // where OpCopy makes an independent deep copy. Diff emits it for the second // and later routes to a value referenced more than once. OpAlias = engine.OpAlias )
Variables ¶
This section is empty.
Functions ¶
func Apply ¶
func Apply[T any](target *T, p Patch[T], opts ...ApplyOption) error
Apply applies a Patch to a target pointer. v5 prioritizes the generated Patch method but falls back to reflection if needed.
Note: when a Patch has been serialized to JSON and decoded, numeric values in Operation.Old and Operation.New will be float64 regardless of the original type. This affects strict-mode Old-value checks.
func ApplyOpReflection ¶ added in v5.2.0
ApplyOpReflection applies a single Operation to target using reflection.
This is intended for use by code generated by deep-gen as a fallback for operations the generated fast-path does not handle (e.g. slice index or map key paths). It is not part of the stable user-facing API; prefer Apply for normal use.
func CloneShared ¶ added in v5.11.0
CloneShared deep-copies v through the reflection engine, recording into (and honouring) memo. Generated Clone methods use it for the fields they cannot copy themselves — types from other packages, interfaces, generics — so that a value shared between such a field and the rest of the struct is still copied once, wherever it was reached first.
With a nil memo it behaves like Clone.
func EscapePathKey ¶ added in v5.4.0
EscapePathKey escapes a map key or keyed-slice key for use as a JSON Pointer token (RFC 6901): "~" becomes "~0" and "/" becomes "~1". Generated code uses it when building operation paths; it is also useful when constructing paths by hand.
func UnescapePathKey ¶ added in v5.4.0
UnescapePathKey reverses EscapePathKey, turning a JSON Pointer token back into the original key.
Types ¶
type ApplyError ¶
type ApplyError struct {
Errors []error
}
ApplyError represents one or more errors that occurred during patch application.
func (*ApplyError) Error ¶
func (e *ApplyError) Error() string
func (*ApplyError) Unwrap ¶
func (e *ApplyError) Unwrap() []error
Unwrap implements the errors.Join interface, allowing errors.Is and errors.As to inspect individual errors within the ApplyError.
type ApplyOption ¶
type ApplyOption func(*applyConfig)
ApplyOption configures the behaviour of Apply.
func WithLogger ¶
func WithLogger(l *slog.Logger) ApplyOption
WithLogger sets the slog.Logger used for OpLog operations within a single Apply call. If not provided, slog.Default is used.
type Builder ¶
type Builder[T any] struct { // contains filtered or unexported fields }
Builder constructs a Patch via a fluent chain.
func Edit ¶
Edit returns a Builder for constructing a Patch[T]. The target argument is used only for type inference and is not stored; the builder produces a standalone Patch, not a live view of the target.
func (*Builder[T]) Guard ¶
Guard sets the global guard condition on the patch. If Guard has already been called, the new condition is ANDed with the existing one rather than replacing it — calling Guard twice is equivalent to Guard(And(c1, c2)).
type CloneMemo ¶ added in v5.11.0
type CloneMemo struct {
// contains filtered or unexported fields
}
CloneMemo records the copy made for each value during a single deep copy, so that a value reached more than once is copied once and every reference to it in the result points at that one copy.
Generated Clone methods create and thread a memo when values of the type can hold two references to the same value — through a cycle, or simply through two routes to one pointer. Types where that cannot happen never allocate one.
The memo shares its identity space with the reflection engine: fields that generated code hands to CloneShared record their copies in the same map, so a value referenced from both sides is still copied exactly once.
A memo belongs to one copy: it is not safe for concurrent use.
func NewCloneMemo ¶ added in v5.11.0
func NewCloneMemo() *CloneMemo
NewCloneMemo returns an empty memo. Pass it to Release when the copy is done so it can be reused.
func (*CloneMemo) Load ¶ added in v5.11.0
Load returns the copy already made for the pointer src, if there is one.
Identity is the pointer's address together with its type: pointers of different types that share an address — a struct and its first field — do not collide.
func (*CloneMemo) Release ¶ added in v5.11.0
func (c *CloneMemo) Release()
Release returns c for reuse. The copies it recorded stay valid — only the bookkeeping is discarded — but it must not be called while a copy using c is still running.
type ConflictResolver ¶
ConflictResolver defines how to resolve merge conflicts.
type DiffMemo ¶ added in v5.11.0
type DiffMemo struct {
// contains filtered or unexported fields
}
DiffMemo tracks the pairs of values a generated Diff has visited, and turns repeat visits into alias operations.
A pair is diffed once, at the first path that reaches it — shared structure can be reachable by exponentially many paths, so diffing every route is not an option. Each later route that reaches an already-diffed, changed pair records one OpAlias operation instead: "make this path point at the same object as the first path". Applied in order, the aliases rebuild the sharing the new value has, whatever the target looked like before.
A memo belongs to one Diff call: it is not safe for concurrent use.
func NewDiffMemo ¶ added in v5.11.0
func NewDiffMemo() *DiffMemo
NewDiffMemo returns an empty memo. Pass it to Release when the diff is done so it can be reused.
func (*DiffMemo) AliasOperations ¶ added in v5.11.0
AliasOperations returns the alias operations recorded so far, in the order the routes were reached. Generated Diff appends them after its own operations; an alias shares the object its From path holds, so it lands correctly whether the operations before it mutated that object in place or replaced it.
func (*DiffMemo) Enter ¶ added in v5.11.0
Enter reports whether the pair (a, b), reached at path, is new.
True means the caller should diff the pair, then call Leave. False means the pair is already handled: if a completed visit found changes, Enter has recorded an alias operation for this path — reporting the changes again here would repeat them once per route, and there can be exponentially many routes. A pair still in progress is a cycle, and is left to the comparison already under way.
func (*DiffMemo) Leave ¶ added in v5.11.0
Leave completes the visit Enter opened for (a, b). ops is the number of operations the pair's diff produced; the pair also counts as changed when aliases were recorded below it, since those are changes too — just ones that live in this memo rather than in the caller's patch.
type Op ¶
type Op struct {
// contains filtered or unexported fields
}
Op is a pending patch operation. Obtain one from Set, Add, Remove, Move, or Copy; attach per-operation conditions with Op.If or Op.Unless before passing to Builder.With.
func Copy ¶
Copy returns a type-safe copy operation that duplicates the value at from to to. Both paths must share the same value type V.
func Move ¶
Move returns a type-safe move operation that relocates the value at from to to. Both paths must share the same value type V.
type Operation ¶
Operation is an alias for the internal engine operation type.
Note: after JSON round-trip, numeric Old/New values become float64.
type Patch ¶
type Patch[T any] struct { // Guard is a global Condition that must be satisfied before any operation // in this patch is applied. Set via WithGuard or Builder.Guard. Guard *condition.Condition `json:"cond,omitempty"` // Operations is a flat list of changes. Operations []Operation `json:"ops"` // Strict mode enables Old value verification. Strict bool `json:"strict,omitempty"` // contains filtered or unexported fields }
Patch is a pure data structure representing a set of changes to type T. It is designed to be easily serializable and manipulatable.
func Diff ¶
Diff compares two values and returns a Patch describing the changes from a to b. Generated types (produced by deep-gen) dispatch to a reflection-free implementation. For other types, Diff falls back to the reflection engine. Changed chan and func values diff to a whole-value replace that shares the reference; the error return covers values the engine cannot process.
func Merge ¶
func Merge[T any](base, other Patch[T], r ConflictResolver) Patch[T]
Merge combines two patches into a single patch, resolving conflicts. Operations are deduplicated by path. When both patches modify the same path, r.Resolve is called if r is non-nil; otherwise other's operation wins over base. The output operations are sorted by path for deterministic ordering.
func ParseJSONPatch ¶
ParseJSONPatch parses a JSON Patch document (RFC 6902 plus deep extensions) back into a Patch[T]. This is the inverse of Patch.ToJSONPatch().
Wire conventions:
- A leading {"op":"test","path":"/","if":<predicate>} entry is interpreted as the global Patch.Guard rather than a regular test op, mirroring what ToJSONPatch emits. To round-trip a regular test op at "/", attach it via Builder rather than serialising it as the document's first entry.
- A {"op":"test","path":<p>,"value":<v>} entry immediately followed by a replace or remove at the same path becomes that operation's Old value and marks the patch strict, mirroring what ToJSONPatch emits for a strict patch.
- Any other test op — and any unknown op — is dropped: the operation model has no general test kind.
func (Patch[T]) AsStrict ¶
AsStrict returns a new patch with strict mode enabled. When strict mode is on, every Replace and Remove operation verifies the current value matches Op.Old before applying; mismatches return an error.
func (Patch[T]) ToJSONPatch ¶
ToJSONPatch returns a JSON Patch representation compatible with RFC 6902 and the github.com/brunoga/jsonpatch extensions.
A strict patch (see Patch.AsStrict) emits a test operation carrying the expected Old value before each replace and remove — RFC 6902's native way of expressing precondition checks — so strictness survives the round-trip through ParseJSONPatch.
type Path ¶
type Path[T, V any] struct { // contains filtered or unexported fields }
Path represents a type-safe path to a field of type V within type T.
type VisitSet ¶ added in v5.11.0
type VisitSet struct {
// contains filtered or unexported fields
}
VisitSet records the pairs of values a recursive comparison has already started on, so that following a cycle stops when it repeats rather than running forever, and a value reachable by many routes is compared once instead of once per route.
Generated Equal methods create and thread a set when values of the type can reach the same value twice. Types where that cannot happen never allocate one.
A set belongs to one comparison: it is not safe for concurrent use.
func NewVisitSet ¶ added in v5.11.0
func NewVisitSet() *VisitSet
NewVisitSet returns an empty set. Pass it to Release when the comparison is done so it can be reused.
func (*VisitSet) Enter ¶ added in v5.11.0
Enter records the pair (a, b) and reports whether it is new. A false result means the pair has been reached before and the caller should treat the two as matching rather than descend again: once a pair has been compared the answer is settled — had it differed, the comparison would already have stopped — and for a pair still being compared further up a cycle, the two differ only if something else on the cycle differs, which that comparison will find.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
deep-gen
command
Command deep-gen generates reflection-free Patch, Diff, Equal and Clone methods for the types named by -type.
|
Command deep-gen generates reflection-free Patch, Diff, Equal and Clone methods for the types named by -type. |
|
Package crdt provides Conflict-free Replicated Data Types (CRDTs) built on top of the deep patch engine.
|
Package crdt provides Conflict-free Replicated Data Types (CRDTs) built on top of the deep patch engine. |
|
hlc
Package hlc implements a Hybrid Logical Clock (HLC) for distributed causality tracking.
|
Package hlc implements a Hybrid Logical Clock (HLC) for distributed causality tracking. |
|
examples
|
|
|
atomic_config
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
audit_logging
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
concurrent_updates
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
conditional_ops
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
config_manager
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
crdt_compaction
command
|
|
|
crdt_containers
command
|
|
|
crdt_custom_type
command
|
|
|
crdt_document
command
|
|
|
crdt_list
command
|
|
|
crdt_observers
command
|
|
|
crdt_sync
command
|
|
|
crdt_sync_incremental
command
|
|
|
crdt_undo_redo
command
|
|
|
cyclic_graph
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
http_patch_api
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
ignored_fields
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
json_interop
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
keyed_inventory
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
lww_fields
command
|
|
|
move_copy_ops
command
|
|
|
multi_error
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
nested_structs
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
policy_engine
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
reflection_fallback
command
|
|
|
slice_paths
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
state_management
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
struct_map_keys
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
text_sync
command
|
|
|
three_way_merge
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
websocket_sync
command
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
internal
|
|
|
testmodels
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
testmodels/external
Code generated by deep-gen.
|
Code generated by deep-gen. |
|
testmodels/graph
Package graph holds models whose types can reach themselves: directly, and through every reference kind the generator emits code for — pointers, slices of pointers, slices of values, maps of pointers and maps of values.
|
Package graph holds models whose types can reach themselves: directly, and through every reference kind the generator emits code for — pointers, slices of pointers, slices of values, maps of pointers and maps of values. |
|
testmodels/multirun
Package multirun holds two types generated by two separate deep-gen runs over the same package — the documented workflow for splitting generation.
|
Package multirun holds two types generated by two separate deep-gen runs over the same package — the documented workflow for splitting generation. |
|
testmodels/shapes
Code generated by deep-gen.
|
Code generated by deep-gen. |