deep

package module
v5.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

deep: Type-Safe Diff, Patch, Clone and Sync for Go

deep is a comprehensive Go library for comparing, cloning, patching and synchronizing complex data structures. It combines code generation (fast, reflection-free implementations for your types) with a reflection engine (which handles everything else, including unexported fields) behind one uniform API.

Key Features

  • Four core operations: Diff, Apply, Equal, Clone — one call each, for any type.
  • Hybrid architecture: deep-gen generates optimized fast paths; a reflection engine transparently handles types without generated code, unexported fields, and exotic shapes.
  • Recursive data: values that reference themselves, or reach the same value twice, work on both paths — cycles are rebuilt, shared nodes stay shared (foreign pointers included), and diffs stay linear with alias operations rewiring the extra routes.
  • Compile-time safety: type-safe field selectors replace brittle string paths.
  • Data-oriented patches: a patch is a flat, serializable list of operations — portable, mergeable, reversible.
  • Conditional patching: guards and per-operation conditions travel inside the patch.
  • Strict mode: optimistic-concurrency checks that verify expected old values before writing.
  • Conditional writes: apply a patch server-side and get a per-operation report of what applied, what its condition skipped, and what failed — narrowing the unit of conflict from the whole row to the fields a write depends on.
  • Standard interop: RFC 6902 JSON Patch import/export, including strict checks as test ops.
  • First-class CRDTs: CRDT[T] wrapper with hybrid logical clocks, plus LWW, Text, Counter, Set and Map convergent types.

Quick Start

go get github.com/brunoga/deep/v5
1. Define your model
type User struct {
    ID    int            `json:"id"`
    Name  string         `json:"name"`
    Roles []string       `json:"roles"`
    Score map[string]int `json:"score"`
}
//go:generate go run github.com/brunoga/deep/v5/cmd/deep-gen -type=User .
go generate ./...

This writes user_deep.go next to your source. Commit it. Everything works without this step too — the reflection engine picks up any type automatically — generation just makes it much faster (see benchmarks below).

3. Use the 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"}}

// Compare two values; get a patch describing the changes.
patch, err := deep.Diff(u1, u2)

// Apply a patch.
err = deep.Apply(&u1, patch)

// Deep equality and deep copy.
same := deep.Equal(u1, u2)
clone := deep.Clone(u1)

Benchmarks

Generated code vs the reflection engine, on the same five-field struct (nested struct, slice, map). Reproduce with go test -bench 'Generated|Reflection' -benchmem .:

Operation Reflection Generated Speedup
Diff 3,121 ns/op (77 allocs) 546 ns/op (16 allocs) 5.7×
Apply 1,476 ns/op (49 allocs) 80 ns/op (2 allocs) 18.5×
Equal 245 ns/op (5 allocs) 100 ns/op (2 allocs) 2.4×
Clone 986 ns/op (15 allocs) 188 ns/op (5 allocs) 5.3×

Deep copy compared with other clone libraries, same struct:

Library ns/op allocs/op Unexported fields Cyclic structures
deep (generated) 168 5
barkimedes/go-deepcopy 740 16 silently zeroed
deep (reflection) 981 15
mitchellh/copystructure 3,149 91 silently zeroed stack overflow

Numbers from an Intel Core Ultra 9 285K; relative ordering is what matters. The competitor benchmarks live out-of-tree so this module stays dependency-free.

Core Operations

  • deep.Diff(a, b) (Patch[T], error) — computes the operations turning a into b. Changed chan/func values diff to a whole-value replace that shares the reference; the error return covers values the reflection engine cannot process.
  • deep.Apply(&target, patch, opts...) error — applies a patch. Individual operation failures are collected: the returned error is an *ApplyError whose Unwrap() []error yields every failure; the remaining operations still apply. Pass deep.WithLogger(l) to route log operations to a specific *slog.Logger.
  • deep.Equal(a, b) bool — deep equality, including unexported fields and cyclic values.
  • deep.Clone(v) T — deep copy. The reflection path also handles unexported fields; non-nil chan and func values are cloned as nil.

All four automatically dispatch to generated methods when they exist and fall back to reflection when they don't — including per-operation: a generated Patch method hands any operation it does not model (slice indexing, move/copy, strict map entries) to the reflection engine.

Type-Safe Selectors

Selectors turn field accessors into JSON Pointer paths at compile time — no string literals to typo:

namePath  := deep.Field(func(u *User) *string         { return &u.Name })
scorePath := deep.Field(func(u *User) *map[string]int { return &u.Score })
rolesPath := deep.Field(func(u *User) *[]string       { return &u.Roles })

deep.At(rolesPath, 0)            // "/roles/0"    — slice element
deep.MapKey(scorePath, "power")  // "/score/power" — map value
namePath.String()                // "/name"

Selectors work for unexported fields too — expose an accessor method returning the field's address and pass it to Field.

Map keys are RFC 6901-escaped automatically (/~1, ~~0). Building paths by hand? Use deep.EscapePathKey / deep.UnescapePathKey.

Building Patches

Diff derives patches from state; the builder constructs them explicitly:

patch := deep.Edit(&user).
    With(
        deep.Set(namePath, "Alice Smith"),            // replace
        deep.Add(deep.MapKey(scorePath, "power"), 100),
        deep.Remove(deep.MapKey(scorePath, "legacy")),
        deep.Move(oldPath, newPath),
        deep.Copy(srcPath, dstPath),
    ).
    Log("update applied").              // structured log line during Apply
    Guard(deep.Eq(statusPath, "paid")). // whole patch is a no-op unless this holds
    Build()

Edit's argument is used only for type inference; the builder produces a standalone Patch[T], not a live view.

Conditions

Conditions are data — they serialize with the patch and are enforced wherever it is applied, including remote peers.

Comparisons: Eq, Ne, Gt, Ge, Lt, Le • Membership: In • Structure: Exists, Type ("string", "number", "boolean", "object", "array", "null") • Text: Matches (regexp) • Combinators: And, Or, Not.

Patch-level guard — all-or-nothing, for state-machine transitions:

patch := deep.Edit(&order).
    With(deep.Set(statusPath, "shipped")).
    Guard(deep.Eq(statusPath, "paid")).
    Build()

Per-operation conditions — individual ops skip independently:

patch := deep.Edit(&invoice).
    With(deep.Set(paidAtPath, time.Now())).
    With(deep.Set(feePath, 25.0).If(deep.Gt(balancePath, 0.0))).
    With(deep.Set(notePath, "").Unless(deep.Exists(notePath))).
    Build()

A condition evaluating to false skips its operation (or, for a guard, the patch — Apply returns an error so the caller knows it did not run). A condition that cannot be evaluated — malformed value, bad path — is an error, not a silent skip.

Struct Tags

Tag Effect
json:"name" Field appears in paths as /name (the Go field name is accepted on apply as well)
json:"-" or deep:"-" Invisible to deep: skipped by Diff, Equal and Clone; operations targeting it are silently ignored
deep:"readonly" Operations targeting the field fail with an error (log operations are still allowed)
deep:"atomic" Diffed as a single whole-value replace — no per-field/per-element operations
deep:"key" Marks a slice element's identity field: the slice diffs by key (add/remove/modify per element) instead of by index, so reordering produces no operations
type Item struct {
    SKU string `deep:"key" json:"sku"`
    Qty int    `json:"qty"`
}
type Inventory struct {
    Items []Item `json:"items"` // diffs as "/items/<sku>", order-insensitive
}

Embedded fields are addressed by their type name (/Meta/version for an embedded Meta), matching how Go names them.

Strict Mode (Optimistic Concurrency)

strict := patch.AsStrict()
err := deep.Apply(&target, strict) // fails if any Old value no longer matches

Every replace/remove verifies the operation's Old value against the current state before writing. Diff fills Old automatically. Strictness survives JSON Patch interop: ToJSONPatch emits an RFC 6902 test op before each checked operation, and ParseJSONPatch folds them back into Old + Strict.

Conditional Writes

A patch carries its own preconditions, so it can be applied where the data lives rather than round-tripped through the writer. That changes what counts as a conflict.

Whole-payload optimistic concurrency conflicts when anything in the row changed. A conditional patch conflicts only when the fields its conditions name changed, so two writers touching unrelated fields stop knocking each other back:

res, err := deep.ApplyWithResult(&row, patch,
    deep.WithAllowedPaths("/title", "/price", "/stock"))
if err != nil {
    return err              // guard rejected, path refused, or an operation failed
}
if !res.AllApplied() {
    return retry(res)       // a condition no longer holds
}
commit(row)

ApplyWithResult behaves exactly as Apply — operations in order, each condition seeing what the ones before it left — but returns an *ApplyResult with one outcome per operation: StatusApplied, StatusSkipped or StatusFailed. The distinction matters because a skipped operation is not an error: Apply returns nil whether a conditional operation ran or not, so on its own it cannot tell a caller whether its conditional write landed.

Patch.Guard rejects a whole patch with ErrGuardNotMet, which is the direct analogue of a compare-and-swap. WithAllowedPaths confines a patch that arrived from elsewhere to a set of prefixes, checking From as well as Path so a move or copy cannot read outside them; an operation that reaches further voids the whole patch with ErrPathNotAllowed. Struct tags remain the way to express restrictions that belong to the type rather than to one caller — deep:"-" to hide a field, deep:"readonly" to make writing it an error.

Two things to get right:

  • A condition must cover the read set, not just the write set. Anything a writer read in order to compute its new value has to appear in the condition, or a concurrent change to it is a silently lost update. Blind writes need no condition and never conflict; a read-modify-write needs one on every field it read.
  • Retries are only free if the operations are idempotent. add on a slice appends, so a lost response plus a client retry gives two entries. Either make the operation self-guarding with a condition or carry an idempotency key. For genuinely commutative updates — counters, sets, registers — the CRDT types remove the retry entirely instead of narrowing it.

benchmarks below and examples/conditional_writes both work through a store holding serialized rows.

Contention

BenchmarkContention runs both strategies against one shared row, with a jittered 200µs round trip standing in for the network — without it a client's read and its write land nanoseconds apart and compare-and-swap never sees an intervening commit. Writers take eight independent counters in turn, so at eight writers or fewer no two want the same field:

Concurrent writers CAS retries/op Conditional retries/op Throughput
2 0.94 0 2.0×
4 2.74 0 3.7×
8 6.35 0 7.5×
16 13.0 0.85 8.0×
32 21.6 2.09 9.9×

Up to eight writers the conditional patches never retry at all: every conflict compare-and-swap paid there was one its granularity invented. Past eight, writers start sharing a counter and some conflicts become real for both — the conditional patch still takes about a tenth as many.

Neither strategy backs off, which real clients do; backoff trades retries for latency rather than removing conflicts, so read the ratio rather than the absolute numbers.

go test -run=XXX -bench=Contention -benchtime=2000x

Merging Patches

merged := deep.Merge(base, other, resolver) // resolver may be nil: other wins

Operations are deduplicated by path; on conflict a custom ConflictResolver (Resolve(path string, local, remote any) any) decides, and the output is sorted by path for determinism.

Patch Utilities

patch.IsEmpty()       // no operations?
patch.Reverse()       // undo patch (swaps Old/New, add↔remove; log ops are dropped)
patch.WithGuard(cond) // returns a copy with a patch-level guard
patch.String()        // human-readable summary

Serialization

Native JSONPatch[T] marshals directly; compact keys keep the wire format small (k=kind, p=path, f=from, o=old, n=new, if/un=conditions):

{"ops":[{"k":2,"p":"/name","o":"Alice","n":"Bob"}],"strict":true}

RFC 6902 JSON Patch — for interop with other tooling:

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

Deep extensions: a leading test op on / with an "if" key carries the patch guard, per-op conditions ride as "if"/"unless" keys, log is a non-standard op, and strict Old checks map to standard test ops.

Note: after any JSON round-trip, numbers in Old/New are float64 (standard Go JSON behavior). Generated code coerces numerics automatically; be aware of it when inspecting operations directly.

Observability

Embed log operations to emit structured trace messages during Apply — request-scoped loggers, test capture, tracing, all without touching your model types:

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":"/"}

Without WithLogger, slog.Default() is used.

deep-gen

deep-gen -type=TypeA,TypeB [-output file.go] [dir]

Generates Patch, Diff, Equal and Clone methods (plus internal helpers) for the named struct types. Output defaults to <firsttype>_deep.go in the target directory.

Rules worth knowing:

  • Every referenced struct needs generated code too. If Doc has a Detail field (direct, embedded, pointer, or as a slice/map element), Detail must be listed in -type or generated by another run over the same package. Multiple runs per package are supported.
  • Types from other packages (time.Time, generic instantiations, interfaces) are handled through deep.Equal/deep.Clone, which dispatch to the type's own methods when present. A small set of stdlib value types (time.Time, time.Duration, …) stays on a fast assignment path.
  • What generation can't express falls back to reflection — per operation, transparently: channels/funcs, slice-index paths, move/copy, strict map-entry checks, conditions on nested paths.
  • Generated files never need hand-editing and are safe to regenerate; deep-gen ignores its own previous output while parsing.
  • Recursive and shared types are detected and handled. See below.

Recursive and Shared Data

A value can reference itself — a tree with parent pointers, a graph, a linked list — or reach the same value by two different routes. Both engines handle both, with the same behavior.

Clone copies a value reached more than once exactly once, and points every reference at that one copy. That holds for pointers to your own structs and for foreign pointers alike: a *time.Time held by a field and a map entry clones into one copied time.Time with both routes pointing at it. A cycle is rebuilt as a cycle in the copy, closing on the copy rather than on the original:

n := &Node{Name: "n"}
n.Next = n
n.Peers = []*Node{n, n}

c := n.Clone()
c.Next == c        // true — the cycle was rebuilt
c.Peers[0] == c    // true — and every route agrees
c.Next == n        // false — nothing is shared with the original

Equal follows a cycle only until it repeats, so two values with matching cyclic shapes compare equal instead of recursing forever.

Diff descends into a pair of values once, at the first path that reaches it — shared structure can be reachable by exponentially many paths, so repeating the operations at each one is not an option. Every later route to a changed value becomes a single alias operation instead: {Kind: OpAlias, Path: to, From: from} makes to hold the same object from holds, where OpCopy would make an independent deep copy. Applying the patch lands the right values at every route and rebuilds the sharing — whether the target still shared the value (a clone) or was rebuilt without sharing (decoded from JSON, say). Alias ops survive the native flat JSON encoding; the RFC 6902 export maps them to copy, which reproduces the values but not the sharing, since JSON values have no identity.

deep-gen works out which types need any of this by looking for two facts in the package's type graph: cycles, and reference classes reachable by more than one route (two *Meta fields, a *time.Time next to a map[string]*time.Time, an interface next to any pointer). Types where neither is possible — the common case — generate exactly the code they did before, with no bookkeeping and no extra allocation. Types that qualify thread a deep.CloneMemo, deep.VisitSet or deep.DiffMemo, still around 6× faster than the reflection engine on the same value; opaque fields are handed to deep.CloneShared, which runs the reflection engine against the same memo, so sharing survives even across the generated/reflection boundary.

One boundary remains, shared by both engines: slice and map headers are copied per occurrence — two fields holding the same []int clone into two independent slices. Identity is tracked through pointers.

CRDTs and Synchronization

The crdt package builds multi-writer synchronization on top of patches, ordered by hybrid logical clocks (crdt/hlc).

CRDT[T] — wraps any type in a concurrency-safe, convergent container:

nodeA := crdt.NewCRDT(GameState{}, "node-a")
nodeB := crdt.NewCRDT(GameState{}, "node-b")

delta := nodeA.Edit(func(s *GameState) { s.Score = 10 }) // timestamped Delta[T]
nodeB.ApplyDelta(delta)                                  // idempotent, causally ordered

nodeA.Merge(nodeB)     // full-state merge, LWW per path
state := nodeA.View()  // snapshot copy

CRDT[T] is JSON-serializable (state + clock + per-path metadata survive the round-trip).

Undo/redoReverse applies a delta's inverse locally and returns a fresh-timestamped undo delta, safe to propagate; reversing the undo redoes:

delta := node.Edit(func(d *Doc) { d.Title = "Draft" })
undo := node.Reverse(delta)
redo := node.Reverse(undo)

Field-level types:

type Document struct {
    Title   crdt.LWW[string] // last-write-wins register: Set(v, ts)
    Content crdt.Text        // collaborative text: Insert/Delete/String, MergeTextRuns
}

Standalone convergent containerscrdt.Counter (increment/decrement), crdt.Set[T] (add-wins set) and crdt.Map[K,V] (LWW map), each with a commutative, idempotent Merge.

How collections merge inside a CRDT[T] — everything converges, but only the first two merge concurrent edits rather than picking a winner:

Field type Concurrent edits
map[K]V Different keys both survive
[]T with deep:"key" on T Different elements both survive; an element's fields merge independently. Order is not synchronized — replicas keep these in key order, so sort on read if order matters
[]T without a key Whole-slice last-write-wins: one writer's version wins
crdt.List[T] Concurrent insertions and deletions all survive, in an order every replica agrees on

Prefer a map, a keyed slice, or a List for any collection edited concurrently.

crdt.List[T] — a sequence that merges rather than overwrites. Elements are placed relative to their neighbours rather than by index, so concurrent edits do not fight over positions:

type Board struct {
    Tasks crdt.List[string] `json:"tasks"`
}

tasks = tasks.Insert(0, "write tests", node.Clock()) // position, value, clock
tasks = tasks.Delete(2, 1)                           // remove one at index 2
tasks.Items()                                        // []string in order

Observing changesOnChange reports the operations that were actually applied, so a UI can redraw just what moved instead of diffing snapshots:

cancel := node.OnChange(func(c crdt.Change[Doc]) {
    for _, op := range c.Patch.Operations {
        redraw(op.Path, op.New) // c.Source is local, remote, or merge
    }
})
defer cancel()

Only surviving operations are reported: a remote write that lost to a newer local one never appears. Callbacks run on the goroutine that made the change, with no lock held, so they may read or edit the replica.

crdt.Document — the same runs as crdt.Text, kept in a tree ordered by position rather than a slice. Finding a position and editing there costs the same whether the document holds a hundred runs or ten thousand:

doc := crdt.NewDocument(node.Clock())
doc.Insert(0, "hello")     // position, value
doc.Delete(0, 2)
doc.MergeFrom(peer.Text()) // converges with a peer, Text or Document
50 edits to a document of Text Document
500 runs 1.2 ms 39 µs
2,000 runs 4.5 ms 25 µs
8,000 runs 22.4 ms 58 µs

Text slows down as runs accumulate; Document does not. Both serialize identically, so a replica running one converges with a replica running the other.

Inside a CRDT[T], an edit to a Document costs about 4 µs whatever the document holds, and the delta it produces is the size of the edit rather than the size of the document. The wrapper copies its value before every edit to work out what changed, and a document is copied by sharing rather than duplicating — nothing in its index is ever changed in place, so the copy and the original cannot disturb each other.

Syncing only what is missing — a state vector says how much of each writer's output a replica holds, one number per writer rather than per character, so a peer sends only the difference:

update := server.Since(client.StateVector()) // what the client lacks
client.Apply(update)

A 23-character edit to a 5,000-character document sends 151 bytes instead of 5,239, and the state vector asking for it is 35. Deletions travel too, applying an update twice changes nothing, and syncing both directions converges. Hold a Document directly for a large collaborative document; a Document inside a CRDT[T] converges but does not bring its speed with it, because Edit copies and compares the value to work out what changed.

Reclaiming history — a replica remembers when every path was written and deleted, and a sequence keeps deleted elements as tombstones, so a long-lived replica carries more history than data. Compact discards it, down to a watermark you supply:

// Older than anything still in flight: usually the oldest timestamp
// every peer has acknowledged.
node.Compact(watermark)

It reaches the Text and List values inside the replica too, changes nothing about what the replica holds, and emits no delta. A compacted replica still converges with one that has not compacted.

Custom convergent types — implement crdt.Convergent and a CRDT[T] merges your type instead of picking a winner, whether the change arrives as a delta or a full merge:

func (s MySet) MergeFrom(other any) any {
    o, ok := other.(MySet)
    if !ok {
        return s
    }
    return union(s, o)
}

MergeFrom must be commutative, associative and idempotent — merging in any order, any number of times, has to reach the same value.

A type holding history of its own can also implement crdt.Compactable, and Compact will reach it:

func (r Retired) CompactBefore(before hlc.HLC) any { /* drop entries older than before */ }

crdt/hlcClock (per-node: Now, Update, Reserve, SetLatest) and HLC timestamps (Compare, After) giving a total order across nodes without synchronized wall clocks.

Architecture

A patch is a flat operation list[]Operation with JSON Pointer paths — rather than a recursive tree. That makes patches trivially serializable, cheap to iterate, and composable (merging is stateless). Application is a hybrid: generated applyOperation methods handle the common shapes at native speed and report anything else as unhandled, at which point the reflection engine — which understands every Go shape, unexported fields included — takes over for that one operation. Both paths implement the same semantics; divergence is treated as a bug.

Examples

Every directory under examples/ is a runnable program (go run ./examples/<name>) built around one concept. The examples guide describes what each one demonstrates and suggests a reading order.

Core operations and patches

Example Concept
config_manager Diff, apply, and roll back with Reverse
state_management An undo stack built from reverse patches
nested_structs Nested and embedded struct paths; targeted field updates
slice_paths At for positional slice elements; how slice diffs are shaped
keyed_inventory deep:"key" — order-insensitive, identity-based slice diffs
struct_map_keys Non-string map keys in paths
move_copy_ops Move and Copy operations
multi_error Error collection and ApplyError unwrapping

Tags, conditions, and safety

Example Concept
atomic_config deep:"readonly" enforcement and deep:"atomic" whole-value updates
ignored_fields json:"-" / deep:"-" — keeping secrets out of patches
policy_engine Patch-level Guard with composed conditions
conditional_ops Per-operation If / Unless inside one patch
conditional_writes Server-side conditional writes: narrowing the unit of conflict, and reading ApplyResult
concurrent_updates Strict mode as optimistic locking
three_way_merge Merge with a custom ConflictResolver
reflection_fallback Unexported fields and cyclic structures with no generated code at all
cyclic_graph Self-referencing types: cycles rebuilt, shared nodes kept shared, alias ops in diffs

Transport and interop

Example Concept
json_interop Native JSON, RFC 6902 export, and ParseJSONPatch ingest
http_patch_api A patch-driven HTTP PATCH endpoint
websocket_sync Broadcasting state deltas to clients
audit_logging Diffs as an audit trail, plus OpLog tracing

CRDTs

Example Concept
crdt_sync CRDT[T] delta exchange and convergence
crdt_undo_redo Distributed undo/redo via Reverse
crdt_containers Counter, Set and Map
crdt_list List[T], a sequence that merges concurrent insertions and deletions
crdt_observers OnChange for incremental UI updates
crdt_compaction Compact for reclaiming the history a long-lived replica accumulates
crdt_document Document, a text CRDT indexed for editing rather than stored as a slice
crdt_sync_incremental State vectors: sending only what a peer is missing
crdt_custom_type Convergent and Compactable: making your own type merge instead of losing a writer
lww_fields Per-field LWW[T] registers resolving a write conflict
text_sync Collaborative text with crdt.Text

License

Apache 2.0

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

View Source
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

View Source
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.

View Source
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 And

func And(conds ...*condition.Condition) *condition.Condition

And combines multiple conditions with logical AND.

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

func ApplyOpReflection[T any](target *T, op Operation, logger *slog.Logger) error

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 Clone

func Clone[T any](v T) T

Clone returns a deep copy of v.

func CloneShared added in v5.11.0

func CloneShared[T any](v T, memo *CloneMemo) T

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 Eq

func Eq[T, V any](p Path[T, V], val V) *condition.Condition

Eq creates an equality condition.

func Equal

func Equal[T any](a, b T) bool

Equal returns true if a and b are deeply equal.

func EqualCoerced added in v5.12.0

func EqualCoerced(current, expected any) bool

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 added in v5.4.0

func EscapePathKey(key string) string

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 Exists

func Exists[T, V any](p Path[T, V]) *condition.Condition

Exists creates a condition that checks if a path exists.

func Ge

func Ge[T, V any](p Path[T, V], val V) *condition.Condition

Ge creates a greater-than-or-equal condition.

func Gt

func Gt[T, V any](p Path[T, V], val V) *condition.Condition

Gt creates a greater-than condition.

func In

func In[T, V any](p Path[T, V], vals []V) *condition.Condition

In creates a condition that checks if a value is in a list.

func Le

func Le[T, V any](p Path[T, V], val V) *condition.Condition

Le creates a less-than-or-equal condition.

func Lt

func Lt[T, V any](p Path[T, V], val V) *condition.Condition

Lt creates a less-than condition.

func Matches

func Matches[T, V any](p Path[T, V], regex string) *condition.Condition

Matches creates a regex condition.

func Ne

func Ne[T, V any](p Path[T, V], val V) *condition.Condition

Ne creates a non-equality condition.

func Not

Not inverts a condition.

func Or

func Or(conds ...*condition.Condition) *condition.Condition

Or combines multiple conditions with logical OR.

func Type

func Type[T, V any](p Path[T, V], typeName string) *condition.Condition

Type creates a type-check condition.

func UnescapePathKey added in v5.4.0

func UnescapePathKey(token string) string

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 WithAllowedPaths added in v5.12.0

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 added in v5.12.0

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 added in v5.12.0

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 added in v5.12.0

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 added in v5.12.0

func (r *ApplyResult) Counts() (applied, skipped, failed int)

Counts returns how many operations landed in each status.

func (*ApplyResult) String added in v5.12.0

func (r *ApplyResult) String() string

String summarises the result, listing the paths that did not apply.

func (*ApplyResult) WithStatus added in v5.12.0

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 Edit

func Edit[T any](_ *T) *Builder[T]

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]) Build

func (b *Builder[T]) Build() Patch[T]

Build assembles and returns the completed Patch.

func (*Builder[T]) Guard

func (b *Builder[T]) Guard(c *condition.Condition) *Builder[T]

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)).

func (*Builder[T]) Log

func (b *Builder[T]) Log(msg string) *Builder[T]

Log appends a log operation.

func (*Builder[T]) With

func (b *Builder[T]) With(ops ...Op) *Builder[T]

With appends one or more operations to the patch being built. Obtain operations from the typed constructors Set, Add, Remove, Move, and Copy; per-operation conditions can be attached with Op.If and Op.Unless before passing here.

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

func (c *CloneMemo) Load(src any) (any, bool)

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.

func (*CloneMemo) Store added in v5.11.0

func (c *CloneMemo) Store(src, dst any)

Store records dst as the copy of the pointer src. It must be called before descending into src's fields: that is what lets a reference back to src, from anywhere below it, resolve to dst instead of starting the copy over.

type ConflictResolver

type ConflictResolver interface {
	Resolve(path string, local, remote any) any
}

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

func (d *DiffMemo) AliasOperations() []Operation

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

func (d *DiffMemo) Enter(a, b any, path string) bool

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

func (d *DiffMemo) Leave(a, b any, ops int)

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.

func (*DiffMemo) Release added in v5.11.0

func (d *DiffMemo) Release()

Release returns d for reuse. It must not be called while a diff using d is still running. The operations AliasOperations returned stay valid: appending them copies the values.

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 Add

func Add[T, V any](p Path[T, V], val V) Op

Add returns a type-safe add (insert) operation.

func Copy

func Copy[T, V any](from, to Path[T, V]) Op

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

func Move[T, V any](from, to Path[T, V]) Op

Move returns a type-safe move operation that relocates the value at from to to. Both paths must share the same value type V.

func Remove

func Remove[T, V any](p Path[T, V]) Op

Remove returns a type-safe remove operation.

func Set

func Set[T, V any](p Path[T, V], val V) Op

Set returns a type-safe replace operation.

func (Op) If

func (o Op) If(c *condition.Condition) Op

If attaches a condition that must hold for this operation to be applied.

func (Op) Unless

func (o Op) Unless(c *condition.Condition) Op

Unless attaches a condition that must NOT hold for this operation to be applied.

type OpKind

type OpKind = engine.OpKind

OpKind represents the type of operation in a patch.

type OpOutcome added in v5.12.0

type OpOutcome struct {
	// Index is the operation's position in the patch it came from.
	Index int
	Path  string
	Kind  OpKind
	Status
	// Err is set when Status is StatusFailed.
	Err error
}

OpOutcome records what became of a single operation.

type OpStatus added in v5.12.0

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
)

func (OpStatus) String added in v5.12.0

func (s OpStatus) String() string

type Operation

type Operation = engine.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

func Diff[T any](a, b T) (Patch[T], error)

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

func ParseJSONPatch[T any](data []byte) (Patch[T], error)

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

func (p Patch[T]) AsStrict() Patch[T]

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]) IsEmpty

func (p Patch[T]) IsEmpty() bool

IsEmpty reports whether the patch contains no operations.

func (Patch[T]) Reverse

func (p Patch[T]) Reverse() Patch[T]

Reverse returns a new patch that undoes the changes in this patch.

func (Patch[T]) String

func (p Patch[T]) String() string

String returns a human-readable summary of the patch operations.

func (Patch[T]) ToJSONPatch

func (p Patch[T]) ToJSONPatch() ([]byte, error)

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.

func (Patch[T]) WithGuard

func (p Patch[T]) WithGuard(c *condition.Condition) Patch[T]

WithGuard returns a new patch with the global guard condition set.

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 At

func At[T any, S ~[]E, E any](p Path[T, S], i int) Path[T, E]

At returns a type-safe path to the element at index i within a slice field.

func Field

func Field[T, V any](s func(*T) *V) Path[T, V]

Field creates a new type-safe path from a selector function.

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 (Path[T, V]) String

func (p Path[T, V]) String() string

String returns the string representation of the path. Paths built from a selector resolve lazily; the result is cached per selector function so repeated calls are O(1) after the first.

type Status added in v5.12.0

type Status = OpStatus

Status is embedded so an outcome reads as outcome.Status.

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

func (v *VisitSet) Enter(a, b any) bool

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.

func (*VisitSet) Release added in v5.11.0

func (v *VisitSet) Release()

Release returns v for reuse. It must not be called while a comparison using v is still running.

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_document command
crdt_list command
crdt_observers command
crdt_sync 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.
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/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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL