Skip to content

Commit d35b152

Browse files
authored
Merge pull request #25 from brunoga/feat/crdt-support
feat: implement CRDT support and efficient run-based text synchronization
2 parents 8c921fc + 352160d commit d35b152

31 files changed

Lines changed: 2062 additions & 212 deletions

README.md

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Deep Copy and Patch for Go
22

3-
`deep` is a high-performance, reflection-based library for manipulating complex Go data structures. It provides three primary capabilities: recursive deep copying, structural diffing to produce patches, and a fluent API for manual patch construction.
3+
`deep` is a high-performance, reflection-based library for manipulating complex Go data structures. It provides recursive deep copying, structural diffing to produce patches, a fluent API for manual patch construction, and first-class support for distributed state synchronization (CRDTs).
44

55
## Features
66

77
* **Deep Copy**: Full recursive cloning of structs, maps, slices, and pointers.
88
* **Deep Diff**: Calculate the semantic difference between two objects.
99
* **Rich Patching**: Apply patches with atomicity, move/copy operations, and logging.
10+
* **Conflict Resolution**: Pluggable resolvers for convergent synchronization (CRDTs).
1011
* **Conditional Logic**: A built-in DSL for cross-field validation and soft-skipping (`If`/`Unless`).
1112
* **Standard Compliant**: Full support for JSON Pointer (RFC 6901) and JSON Patch (RFC 6902).
1213
* **Production Ready**: Handles circular references and unexported fields transparently.
@@ -35,14 +36,54 @@ if patch != nil {
3536

3637
---
3738

39+
## Distributed State (CRDT)
40+
41+
`deep` includes a first-class CRDT engine for synchronizing complex Go structures across multiple nodes without a central coordinator.
42+
43+
### Why Deep CRDTs?
44+
Most CRDT libraries only handle primitives. `deep` uses its structural awareness to provide **granular, field-level convergence** for your existing Go types.
45+
46+
### Basic Usage
47+
```go
48+
import "github.com/brunoga/deep/v2/crdt"
49+
50+
// 1. Initialize a CRDT wrapper
51+
nodeA := crdt.NewCRDT(Config{Title: "Initial"}, "node-a")
52+
53+
// 2. Edit the state
54+
delta := nodeA.Edit(func(c *Config) {
55+
c.Title = "Updated Title"
56+
})
57+
58+
// 3. Apply changes from other nodes
59+
nodeB.ApplyDelta(delta)
60+
```
61+
62+
### Semantic Slices
63+
By tagging slice elements with `deep:"key"`, `deep` enables **Yjs-style semantic patching**. This ensures that concurrent insertions into a list interleave correctly rather than overwriting each other or failing due to index shifts.
64+
65+
```go
66+
type Document struct {
67+
Text []Char `deep:"key"` // Enable semantic list merging
68+
}
69+
```
70+
71+
---
72+
3873
## Core Concepts
3974

40-
### The Patch Model
41-
A `Patch[T]` is a tree of operations. Unlike simple key-value maps, `deep` patches understand the structure of your data. A single patch can contain replacements, slice insertions/deletions, map manipulations, and even data movement between paths.
75+
### Pluggable Resolution
76+
You can provide custom logic to mediate how patches are applied using the `ConflictResolver` interface. This is how the CRDT package implements Last-Write-Wins (LWW) via Hybrid Logical Clocks (HLC).
77+
78+
```go
79+
// Use a custom resolver to implement business-specific merge rules
80+
err := patch.ApplyResolved(&target, myResolver)
81+
```
4282

4383
### Consistency Modes
4484
* **Strict (Default)**: `ApplyChecked` ensures the target value matches the `old` value recorded during the `Diff`. If the target has changed since the diff was taken, the patch fails.
45-
* **Flexible**: Disable strict checking using `patch.WithStrict(false)` to apply changes regardless of the current value, relying instead on custom Conditions.
85+
* **Flexible**: Disable strict checking using `patch.WithStrict(false)` to apply changes regardless of the current value.
86+
* **Resolved**: Use `ApplyResolved` to handle concurrent edits via a custom resolution strategy.
4687

4788
---
4889

@@ -68,13 +109,6 @@ builder.Root().Navigate("/network/settings/port").Put(8080)
68109
builder.Root().Navigate("Metadata.Tags[0]").Put("admin")
69110
```
70111

71-
### Advanced Operations
72-
The builder supports more than just "Set":
73-
* **Move**: `Root().Field("Backup").Move("/Active")`
74-
* **Copy**: `Root().Field("Template").Copy("/Target")`
75-
* **Test**: `Root().Field("Status").Test("ready")` (Fails patch if value doesn't match)
76-
* **Log**: `Root().Log("Applying update...")` (Prints to stdout during application)
77-
78112
---
79113

80114
## Conditional Patching
@@ -86,16 +120,6 @@ You can attach logic to any node in a patch using a string-based DSL via `ParseC
86120
* `"Version > 5"` (Literal comparison)
87121
* `"Stock < MinAlertThreshold"` (Cross-field comparison)
88122
* `"Network.Port == 8080 AND Status == 'active'"` (Logical groups)
89-
* `"NOT (Tags[0] == 'internal')"` (Slice access)
90-
91-
### Soft Conditions (If/Unless)
92-
Standard conditions fail the entire patch. Soft conditions simply skip a specific operation while allowing the rest of the patch to proceed.
93-
94-
```go
95-
builder.Root().Field("BetaFeatures").
96-
If(deep.Equal[Config]("Tier", "premium")).
97-
Put(true)
98-
```
99123

100124
---
101125

@@ -104,7 +128,6 @@ builder.Root().Field("BetaFeatures").
104128
### JSON Pointer (RFC 6901)
105129
Use standard pointers to navigate or query your structures:
106130
```go
107-
// Both the DSL and the Builder support JSON pointers
108131
cond, _ := deep.ParseCondition[Config]("/network/settings/port > 1024")
109132
builder.Root().Navigate("/meta/tags/0").Put("new")
110133
```
@@ -117,38 +140,16 @@ jsonBytes, err := patch.ToJSONPatch()
117140

118141
---
119142

120-
## Advanced Options
121-
122-
### Ignoring Paths
123-
Ignore specific fields during a diff or copy (e.g., timestamps or secrets):
124-
```go
125-
// Works for both Copy and Diff
126-
dst, _ := deep.Copy(src, deep.IgnorePath("SecretToken"))
127-
patch := deep.Diff(old, new, deep.IgnorePath("UpdatedAt"))
128-
```
129-
130-
### Skipping Unsupported Types
131-
Tell `Copy` to zero-out types it cannot handle (like functions or channels) instead of returning an error:
132-
```go
133-
dst, _ := deep.Copy(src, deep.SkipUnsupported())
134-
```
135-
136-
---
137-
138143
## Technical Details
139144

145+
### Hybrid Logical Clocks (HLC)
146+
The CRDT package uses HLCs to provide causal ordering of events without requiring perfect clock synchronization between nodes.
147+
140148
### Unexported Fields
141-
`deep` uses `unsafe` pointers to read and write unexported struct fields. This is required for true deep copying of third-party or internal types where fields are not public.
149+
`deep` uses `unsafe` pointers to read and write unexported struct fields. This is required for true deep copying of third-party or internal types.
142150

143151
### Cycle Detection
144152
The library tracks pointers during recursive operations. Circular references are handled correctly without entering infinite loops.
145153

146-
### Custom Copiers
147-
Types can control their own cloning logic by implementing `Copier[T]`:
148-
```go
149-
type Token string
150-
func (t Token) Copy() (Token, error) { return "REDACTED", nil }
151-
```
152-
153154
## License
154155
Apache 2.0

builder.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,12 @@ func (n *Node) ensurePatch() {
267267
p = &slicePatch{}
268268
case reflect.Map:
269269
p = &mapPatch{
270-
added: make(map[interface{}]reflect.Value),
271-
removed: make(map[interface{}]reflect.Value),
272-
modified: make(map[interface{}]diffPatch),
270+
added: make(map[any]reflect.Value),
271+
removed: make(map[any]reflect.Value),
272+
modified: make(map[any]diffPatch),
273273
keyType: n.typ.Key(),
274274
}
275-
case reflect.Ptr:
275+
case reflect.Pointer:
276276
p = &ptrPatch{}
277277
case reflect.Interface:
278278
p = &interfacePatch{}
@@ -413,9 +413,9 @@ func (n *Node) MapKey(key any) (*Node, error) {
413413
mp, ok := n.current.(*mapPatch)
414414
if !ok {
415415
mp = &mapPatch{
416-
added: make(map[interface{}]reflect.Value),
417-
removed: make(map[interface{}]reflect.Value),
418-
modified: make(map[interface{}]diffPatch),
416+
added: make(map[any]reflect.Value),
417+
removed: make(map[any]reflect.Value),
418+
modified: make(map[any]diffPatch),
419419
keyType: n.typ.Key(),
420420
}
421421
n.update(mp)
@@ -433,12 +433,12 @@ func (n *Node) MapKey(key any) (*Node, error) {
433433

434434
// Elem returns a Node for the element type of a pointer or interface.
435435
func (n *Node) Elem() *Node {
436-
if n.typ == nil || (n.typ.Kind() != reflect.Ptr && n.typ.Kind() != reflect.Interface) {
436+
if n.typ == nil || (n.typ.Kind() != reflect.Pointer && n.typ.Kind() != reflect.Interface) {
437437
return n
438438
}
439439
updateFunc := n.update
440440
var currentPatch diffPatch
441-
if n.typ.Kind() == reflect.Ptr {
441+
if n.typ.Kind() == reflect.Pointer {
442442
pp, ok := n.current.(*ptrPatch)
443443
if !ok {
444444
pp = &ptrPatch{}
@@ -458,7 +458,7 @@ func (n *Node) Elem() *Node {
458458
currentPatch = ip.elemPatch
459459
}
460460
var nextTyp reflect.Type
461-
if n.typ.Kind() == reflect.Ptr {
461+
if n.typ.Kind() == reflect.Pointer {
462462
nextTyp = n.typ.Elem()
463463
}
464464
return &Node{
@@ -528,9 +528,9 @@ func (n *Node) Delete(keyOrIndex any, oldVal any) error {
528528
mp, ok := n.current.(*mapPatch)
529529
if !ok {
530530
mp = &mapPatch{
531-
added: make(map[interface{}]reflect.Value),
532-
removed: make(map[interface{}]reflect.Value),
533-
modified: make(map[interface{}]diffPatch),
531+
added: make(map[any]reflect.Value),
532+
removed: make(map[any]reflect.Value),
533+
modified: make(map[any]diffPatch),
534534
keyType: n.typ.Key(),
535535
}
536536
n.update(mp)
@@ -558,9 +558,9 @@ func (n *Node) AddMapEntry(key, val any) error {
558558
mp, ok := n.current.(*mapPatch)
559559
if !ok {
560560
mp = &mapPatch{
561-
added: make(map[interface{}]reflect.Value),
562-
removed: make(map[interface{}]reflect.Value),
563-
modified: make(map[interface{}]diffPatch),
561+
added: make(map[any]reflect.Value),
562+
removed: make(map[any]reflect.Value),
563+
modified: make(map[any]diffPatch),
564564
keyType: n.typ.Key(),
565565
}
566566
n.update(mp)

builder_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,3 +748,30 @@ func TestBuilder_Log(t *testing.T) {
748748
t.Errorf("Unexpected JSON for log: %s", string(jsonBytes))
749749
}
750750
}
751+
752+
func TestBuilder_Put(t *testing.T) {
753+
type State struct {
754+
Data map[string]int
755+
}
756+
b := NewBuilder[State]()
757+
node, _ := b.Root().Navigate("Data")
758+
node.Put(map[string]int{"a": 1})
759+
p, _ := b.Build()
760+
761+
s := State{Data: make(map[string]int)}
762+
p.Apply(&s)
763+
if s.Data["a"] != 1 {
764+
t.Errorf("expected 1, got %d", s.Data["a"])
765+
}
766+
}
767+
768+
func TestPatch_ToJSONPatch_ReadOnly(t *testing.T) {
769+
patch := Diff(1, 2)
770+
ro := &readOnlyPatch{inner: patch.(patchUnwrapper).unwrap()}
771+
772+
// readOnlyPatch toJSONPatch currently returns nil
773+
json := ro.toJSONPatch("/")
774+
if json != nil {
775+
t.Errorf("expected nil for readOnly toJSONPatch")
776+
}
777+
}

condition.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func (p Path) delete(v reflect.Value) error {
241241
}
242242

243243
func dereference(v reflect.Value) (reflect.Value, error) {
244-
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
244+
for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
245245
if v.IsNil() {
246246
return reflect.Value{}, fmt.Errorf("path traversal failed: nil pointer/interface")
247247
}
@@ -416,7 +416,7 @@ func toReflectValue(v any) reflect.Value {
416416
return rv
417417
}
418418
rv := reflect.ValueOf(v)
419-
for rv.Kind() == reflect.Ptr {
419+
for rv.Kind() == reflect.Pointer {
420420
rv = rv.Elem()
421421
}
422422
return rv

condition_impl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (c *rawTypeCondition) evaluateAny(v any) (bool, error) {
8383
return target.Kind() == reflect.Slice || target.Kind() == reflect.Array, nil
8484
case "null":
8585
k := target.Kind()
86-
return (k == reflect.Ptr || k == reflect.Interface || k == reflect.Slice || k == reflect.Map) && target.IsNil(), nil
86+
return (k == reflect.Pointer || k == reflect.Interface || k == reflect.Slice || k == reflect.Map) && target.IsNil(), nil
8787
case "undefined":
8888
return false, nil
8989
default:

condition_serialization.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func marshalConditionAny(c any) (any, error) {
2828

2929
// Use reflection to extract the underlying fields regardless of T.
3030
v := reflect.ValueOf(c)
31-
if v.Kind() == reflect.Ptr {
31+
if v.Kind() == reflect.Pointer {
3232
v = v.Elem()
3333
}
3434

copy.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func Copy[T any](src T, opts ...CopyOption) (T, error) {
8989
// For cyclic reference detection to work reliably for root value types,
9090
// they must be addressable. We ensure this by taking the address.
9191
var rv reflect.Value
92-
if v.Kind() == reflect.Ptr {
92+
if v.Kind() == reflect.Pointer {
9393
rv = v
9494
} else {
9595
pv := reflect.New(v.Type())
@@ -186,12 +186,12 @@ func recursiveCopy(v reflect.Value, pointers pointersMap,
186186
typ := v.Type()
187187
key := pointersMapKey{ptr, typ}
188188
if dst, ok := pointers[key]; ok {
189-
if dst.Kind() == reflect.Ptr && v.Kind() != reflect.Ptr {
189+
if dst.Kind() == reflect.Pointer && v.Kind() != reflect.Pointer {
190190
return dst.Elem(), nil
191191
}
192192
return dst, nil
193193
}
194-
} else if kind == reflect.Ptr && !v.IsNil() {
194+
} else if kind == reflect.Pointer && !v.IsNil() {
195195
ptr := v.Pointer()
196196
typ := v.Type()
197197
key := pointersMapKey{ptr, typ}
@@ -210,14 +210,14 @@ func recursiveCopy(v reflect.Value, pointers pointersMap,
210210
// preserves type safety for the user.
211211
if v.IsValid() && v.CanInterface() {
212212
attemptCopier := true
213-
if kind == reflect.Interface || kind == reflect.Ptr {
213+
if kind == reflect.Interface || kind == reflect.Pointer {
214214
if v.IsNil() {
215215
attemptCopier = false
216216
}
217217
}
218218

219219
if attemptCopier {
220-
if kind == reflect.Struct || kind == reflect.Ptr {
220+
if kind == reflect.Struct || kind == reflect.Pointer {
221221
method := v.MethodByName("Copy")
222222
if method.IsValid() && method.Type().NumIn() == 0 && method.Type().NumOut() == 2 {
223223
if method.Type().Out(0) == v.Type() && method.Type().Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
@@ -239,7 +239,7 @@ func recursiveCopy(v reflect.Value, pointers pointersMap,
239239
return recursiveCopyInterface(v, pointers, config, path)
240240
case reflect.Map:
241241
return recursiveCopyMap(v, pointers, config, path)
242-
case reflect.Ptr:
242+
case reflect.Pointer:
243243
return recursiveCopyPtr(v, pointers, config, path)
244244
case reflect.Slice:
245245
return recursiveCopySlice(v, pointers, config, path)
@@ -444,7 +444,7 @@ func deepCopyValue(v reflect.Value) reflect.Value {
444444

445445
// We ensure the root is addressable for cycle detection.
446446
var rv reflect.Value
447-
isPtr := v.Kind() == reflect.Ptr
447+
isPtr := v.Kind() == reflect.Pointer
448448
if isPtr {
449449
rv = v
450450
} else {
@@ -458,7 +458,7 @@ func deepCopyValue(v reflect.Value) reflect.Value {
458458
return v
459459
}
460460

461-
if !isPtr && copied.Kind() == reflect.Ptr {
461+
if !isPtr && copied.Kind() == reflect.Pointer {
462462
return copied.Elem()
463463
}
464464

0 commit comments

Comments
 (0)