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/v6/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 NewPatch. 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.NewPatch[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
- Variables
- func And(conds ...*condition.Condition) *condition.Condition
- func Apply[T any](target *T, p Patch[T], opts ...ApplyOption) error
- func Clone[T any](v T) T
- func Eq[T, V any](p Path[T, V], val V) *condition.Condition
- func Equal[T any](a, b T) bool
- func EqualCoerced(current, expected any) 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 RegisterCustomClone[T any](fn func(src T) (T, error))
- func RegisterCustomDiff[T any](fn func(a, b T) (Patch[T], error))
- func RegisterCustomEqual[T any](fn func(a, b T) bool)
- func RegisterTypeFamily(f TypeFamily)
- func Type[T, V any](p Path[T, V], typeName string) *condition.Condition
- func UnescapePathKey(token string) string
- func ValueAs[T any](v any) (T, bool)
- type ApplyError
- type ApplyOption
- type ApplyResult
- type Builder
- type ConflictResolver
- type Op
- type OpKind
- type OpOutcome
- type OpStatus
- type Operation
- type Patch
- type Path
- type RawValue
- type ResolverFunc
- type TypeFamily
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 ¶
var ErrGuardNotMet = errors.New("patch guard not met")
ErrGuardNotMet is returned when a patch's Patch.Guard evaluates false. It is a sentinel so that a caller can tell a rejected patch — the state moved, try again — apart from a patch that could not be applied at all.
var ErrPathNotAllowed = errors.New("path not allowed")
ErrPathNotAllowed is returned when an operation addresses a path outside the set given to WithAllowedPaths. See that option for why the whole patch is rejected rather than the offending operation dropped.
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 Clone ¶
func Clone[T any](v T) T
Clone returns a deep copy of v. Non-nil chan, func and unsafe.Pointer values, which have no meaningful deep copy, come back nil; everything else is copied.
Before v6 a value containing a non-nil chan came back as the zero value of T — the copy error was swallowed and the whole result discarded with it, which is the worst possible reading of "cannot copy this field".
func EqualCoerced ¶
EqualCoerced reports whether current equals expected, where expected may be of a different but losslessly convertible type.
[Operation.Old] and a condition's value are declared `any`, so a patch that travelled as JSON carries whatever the decoder produced — every number arrives as float64, a nested struct as map[string]any — regardless of the types the patch was built from. Equal requires identical types and reports those as mismatches, which is why a strict check against a decoded patch can fail on state it actually matches.
The conversion is verified rather than trusted: converting back has to reproduce the value given, so float64(5.7) does not match an int field holding 5.
This is what generated code and the reflection engine use for strict-mode checks. It is exported so a server applying patches from the wire can make the same comparison itself.
func EscapePathKey ¶
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 RegisterCustomClone ¶
RegisterCustomClone installs fn as the deep copy for T.
fn must return a value that shares no mutable state with its argument; returning the argument makes Clone a shallow copy for that type, with no warning.
func RegisterCustomDiff ¶
RegisterCustomDiff installs fn as the diff for T.
The patch fn returns is spliced into the enclosing patch at the path where the value was reached, so its operation paths are relative to the value rather than to the root. Returning an empty patch means no difference.
func RegisterCustomEqual ¶
RegisterCustomEqual installs fn as the equality test for T.
func RegisterTypeFamily ¶ added in v6.1.0
func RegisterTypeFamily(f TypeFamily)
RegisterTypeFamily installs a family. Families are consulted in registration order; the first whose Match accepts a type owns it, and per-concrete-type registrations (RegisterCustomEqual, RegisterCustomClone, RegisterCustomDiff) take precedence over any family.
Registration is global and should happen during initialisation.
func UnescapePathKey ¶
UnescapePathKey reverses EscapePathKey, turning a JSON Pointer token back into the original key.
func ValueAs ¶
ValueAs extracts an operation's Old or New as a T.
The value may be exactly a T — a patch built in this process — or a still-encoded RawValue from a patch that arrived over the wire, which is decoded directly into T. A value of a different but losslessly convertible type is converted; a lossy conversion is refused, so float64(5.7) does not pass for an int.
Generated code applies operations through this, which is what keeps a wire patch on the fast path; it is exported for callers that inspect operations themselves.
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 WithAllowedPaths ¶
func WithAllowedPaths(prefixes ...string) ApplyOption
WithAllowedPaths restricts a patch to the given path prefixes. An operation addressing anything else — including through the From path of a move or a copy, which reads from wherever it points — makes the whole call fail with ErrPathNotAllowed, leaving the target untouched.
A patch that arrived from somewhere else is a program someone else wrote, running against your data. Struct tags already keep fields out of reach: `deep:"-"` hides a field entirely and `deep:"readonly"` makes writing it an error. This is the complement for cases where the type is shared and the restriction belongs to one caller rather than to the type — a service that should only ever touch /status, say.
A prefix matches itself and everything under it: "/a" allows "/a" and "/a/b", but not "/ab". Passing "/" allows everything, which is the same as not passing the option at all.
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 ApplyResult ¶
type ApplyResult struct {
// Outcomes has one entry per operation, in patch order. An operation the
// patch never reached, because the guard rejected it, has no entry.
Outcomes []OpOutcome
}
ApplyResult reports what ApplyWithResult did, operation by operation.
It exists because a skipped operation and an applied one are otherwise indistinguishable: Apply returns nil for both, so a caller applying a conditional patch cannot tell whether the condition held. For a patch used as a conditional write — the condition standing in for a compare-and-swap — that difference is the entire result of the call.
func ApplyWithResult ¶
func ApplyWithResult[T any](target *T, p Patch[T], opts ...ApplyOption) (*ApplyResult, error)
ApplyWithResult applies a patch and reports what became of every operation.
It behaves as Apply does — operations run in order, each condition sees the state the operations before it left, and a failure does not stop the ones after it — but it returns an ApplyResult saying which operations applied, which their conditions skipped, and which failed.
The returned error covers the patch as a whole: ErrGuardNotMet when the guard rejected it, ErrPathNotAllowed when an operation addressed a path outside WithAllowedPaths, or an *ApplyError collecting the individual failures. A patch whose operations were all skipped returns a nil error — nothing failed — which is why the result, not the error, is what a conditional write should be judged on.
func (*ApplyResult) AllApplied ¶
func (r *ApplyResult) AllApplied() bool
AllApplied reports whether every operation ran. It is false when any operation was skipped by its condition or failed, which for a conditional write is the signal to re-read and retry.
func (*ApplyResult) Counts ¶
func (r *ApplyResult) Counts() (applied, skipped, failed int)
Counts returns how many operations landed in each status.
func (*ApplyResult) String ¶
func (r *ApplyResult) String() string
String summarises the result, listing the paths that did not apply.
func (*ApplyResult) WithStatus ¶
func (r *ApplyResult) WithStatus(s OpStatus) []OpOutcome
WithStatus returns the outcomes in the given status, in patch order.
type Builder ¶
type Builder[T any] struct { // contains filtered or unexported fields }
Builder constructs a Patch via a fluent chain.
func NewPatch ¶
NewPatch returns a Builder for constructing a Patch[T].
p := deep.NewPatch[Listing](). With(deep.Set(pricePath, 2499).If(deep.Eq(pricePath, 1999))). Build()
The builder produces a standalone patch, not a live view of anything.
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 ConflictResolver ¶
ConflictResolver defines how to resolve merge conflicts.
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 OpOutcome ¶
type OpOutcome struct {
// Index is the operation's position in the patch it came from.
Index int
Path string
Kind OpKind
// Status is what became of the operation.
Status OpStatus
// Err is set when Status is StatusFailed.
Err error
}
OpOutcome records what became of a single operation.
type OpStatus ¶
type OpStatus int
OpStatus is what became of one operation.
const ( // StatusApplied means the operation ran and changed the target. StatusApplied OpStatus = iota // StatusSkipped means the operation's If or Unless condition did not hold, // so it was deliberately not applied. This is not an error: it is the // answer to the question the condition asked. StatusSkipped // StatusFailed means the operation could not be applied. StatusFailed )
type Operation ¶
Operation is an alias for the internal engine operation type.
An operation decoded from the wire carries its Old and New still encoded, as RawValue; they are decoded at apply time against the type of the field the operation addresses, so an int field receives an int and a struct field a struct — not the float64 and map[string]any a blind decode would produce.
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 matched by path. When both patches write the same path, r.Resolve decides the value if r is non-nil; otherwise other's operation wins.
Paths that are not equal can still collide: an operation on /user and one on /user/name are not independent, and keeping both produced a patch that could not be applied — removing /user and then writing /user/name fails on the path that is no longer there. When the two sides disagree that way, the operation from other wins and the one it encloses (or is enclosed by) is dropped. r is not consulted for these, because there is no single path at which to ask.
An ancestor and a descendant from the *same* patch are left alone: writing /user and then /user/name within one patch is a legitimate sequence, and the path ordering below applies them in that order.
The output is sorted by path, which makes the result deterministic and puts an ancestor before its descendants.
func MustDiff ¶
MustDiff is Diff that panics on error. The error covers values the reflection engine cannot process at all; for ordinary types it does not happen, which makes this the comfortable form in tests and examples.
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.
func MapKey ¶
func MapKey[T any, M ~map[K]V, K comparable, V any](p Path[T, M], k K) Path[T, V]
MapKey returns a type-safe path to the value at key k within a map field. Keys are RFC 6901-escaped so values containing '/' or '~' navigate correctly.
func PathString ¶
PathString creates a Path from a raw JSON Pointer string, for paths chosen at run time — a field name arriving in a request, say — where no selector function can be written.
Nothing checks that the path exists on T or leads to a V; that is the compile-time guarantee Field, At and MapKey give and this deliberately trades away. An operation built on a wrong path fails at apply time, the way any hand-written path does.
type RawValue ¶
RawValue is a value that arrived over the wire and has not been decoded yet, because the right type to decode it into is not known until the operation carrying it reaches its target field. ValueAs decodes one; Apply does so automatically.
type ResolverFunc ¶
ResolverFunc adapts a function to the ConflictResolver interface, so a one-off resolver does not need a named type:
deep.Merge(a, b, deep.ResolverFunc(func(path string, local, remote any) any {
return local // ours wins
}))
type TypeFamily ¶ added in v6.1.0
type TypeFamily struct {
// Name identifies the family in errors and diagnostics, and joins the
// family's handlers together. It must be unique among registered families.
Name string
// Match reports whether t belongs to the family. It is consulted once per
// type and the verdict is cached, so it may be arbitrarily selective but
// should not depend on anything that changes at run time.
Match func(t reflect.Type) bool
// Equal reports whether two matched values are equal, by the family's own
// definition. Both arguments have the matched type.
Equal func(a, b any) bool
// Clone returns a deep copy of a matched value.
Clone func(v any) any
// Diff returns the operations turning a into b, with paths relative to the
// value — "/" or "" for the whole value — using add, remove and replace
// only. Returning no operations means the values do not differ.
Diff func(a, b any) ([]Operation, error)
// Apply applies one operation to target, which is a pointer to a matched
// value. The operation's path is relative to the value. Apply honours
// op.Strict by verifying op.Old before writing, the way the generic
// applier does.
Apply func(target any, op Operation) error
// Marshal renders a matched value into its wire (JSON) form, and Unmarshal
// reverses it for the given matched type. They exist because a family's
// values may have a wire form encoding/json cannot produce: a protobuf
// Timestamp is an RFC 3339 string under protojson, not a struct of
// seconds and nanos.
Marshal func(v any) ([]byte, error)
Unmarshal func(data []byte, t reflect.Type) (any, error)
}
TypeFamily is custom behaviour for every type matching a predicate, where RegisterCustomEqual and friends are custom behaviour for one concrete type.
The distinction matters for types produced by another runtime. A protobuf application holds hundreds of generated message types; registering each one is not a usable interface, and every one needs the same treatment — the proto runtime's own equality, its own cloning, its own wire form. A family says that once: any type the predicate accepts is handled by these functions, however many such types exist.
A family is all-or-nothing about its boundary. Once a value is matched, nothing generic looks inside it: Equal and Clone are the family's, Diff produces the family's operations, and an operation whose path crosses into a matched value is handed to the family's Apply with the remainder of the path. That is the point — the inside of a protobuf message is the proto runtime's territory, and walking its Go struct directly reads internal bookkeeping and, for Clone, corrupts it.
Source Files
¶
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. |
|
conditional_writes
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_presence
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. |
|
Package gen holds the bookkeeping that code generated by deep-gen threads through its methods: a memo of copies already made, and the visited sets that keep a comparison or a diff from following a cycle forever.
|
Package gen holds the bookkeeping that code generated by deep-gen threads through its methods: a memo of copies already made, and the visited sets that keep a comparison or a diff from following a cycle forever. |
|
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/record
Package record holds a document shaped like one a handful of independent services would share: a row in a store, held as a serialized blob, whose fields are each owned by a different writer.
|
Package record holds a document shaped like one a handful of independent services would share: a row in a store, held as a serialized blob, whose fields are each owned by a different writer. |
|
testmodels/shapes
Code generated by deep-gen.
|
Code generated by deep-gen. |