Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cond/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type Condition[T any] interface {
// MarshalJSON returns the JSON representation of the condition.
MarshalJSON() ([]byte, error)

// MarshalSerializable returns a serializable representation of the condition.
MarshalSerializable() (any, error)

InternalCondition
}

Expand Down Expand Up @@ -53,6 +56,37 @@ func (c *typedCondition[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(s)
}

func (c *typedCondition[T]) MarshalSerializable() (any, error) {
return MarshalConditionAny(c.inner)
}

func (c *typedCondition[T]) GobEncode() ([]byte, error) {
s, err := MarshalConditionAny(c.inner)
if err != nil {
return nil, err
}
return json.Marshal(s)
}

func (c *typedCondition[T]) GobDecode(data []byte) error {
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
return err
}
cond, err := UnmarshalConditionSurrogate[T](m)
if err != nil {
return err
}
if tc, ok := cond.(*typedCondition[T]); ok {
c.inner = tc.inner
}
return nil
}

func (c *typedCondition[T]) unwrap() InternalCondition {
return c.inner
}

// Eq returns a condition that checks if the value at the path is equal to the given value.
func Eq[T any](p string, val any) Condition[T] {
return &typedCondition[T]{
Expand Down
30 changes: 23 additions & 7 deletions cond/condition_serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,19 @@ type condSurrogate struct {
Data any `json:"d,omitempty" gob:"d,omitempty"`
}

// MarshalCondition returns a serializable surrogate (struct or map) for the condition.
// To get the JSON bytes, pass the result to json.Marshal.
// ConditionToSerializable returns a serializable representation of the condition.
func ConditionToSerializable(c any) (any, error) {
if c == nil {
return nil, nil
}

if t, ok := c.(interface{ unwrap() InternalCondition }); ok {
return ConditionToSerializable(t.unwrap())
}

return MarshalConditionAny(c)
}

func MarshalCondition[T any](c Condition[T]) (any, error) {
if t, ok := c.(*typedCondition[T]); ok {
return MarshalConditionAny(t.inner)
Expand Down Expand Up @@ -179,10 +190,15 @@ func UnmarshalCondition[T any](data []byte) (Condition[T], error) {
if err := json.Unmarshal(data, &s); err != nil {
return nil, err
}
return ConvertFromCondSurrogate[T](&s)
return UnmarshalConditionSurrogate[T](&s)
}

// ConditionFromSerializable reconstructs a condition from its serializable representation.
func ConditionFromSerializable[T any](s any) (Condition[T], error) {
return UnmarshalConditionSurrogate[T](s)
}

func ConvertFromCondSurrogate[T any](s any) (Condition[T], error) {
func UnmarshalConditionSurrogate[T any](s any) (Condition[T], error) {
if s == nil {
return nil, nil
}
Expand Down Expand Up @@ -253,7 +269,7 @@ func ConvertFromCondSurrogate[T any](s any) (Condition[T], error) {
d := data.([]any)
conds := make([]InternalCondition, 0, len(d))
for _, subData := range d {
sub, err := ConvertFromCondSurrogate[T](subData)
sub, err := UnmarshalConditionSurrogate[T](subData)
if err != nil {
return nil, err
}
Expand All @@ -269,7 +285,7 @@ func ConvertFromCondSurrogate[T any](s any) (Condition[T], error) {
d := data.([]any)
conds := make([]InternalCondition, 0, len(d))
for _, subData := range d {
sub, err := ConvertFromCondSurrogate[T](subData)
sub, err := UnmarshalConditionSurrogate[T](subData)
if err != nil {
return nil, err
}
Expand All @@ -281,7 +297,7 @@ func ConvertFromCondSurrogate[T any](s any) (Condition[T], error) {
}
inner = &rawOrCondition{Conditions: conds}
case "not":
sub, err := ConvertFromCondSurrogate[T](data)
sub, err := UnmarshalConditionSurrogate[T](data)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions cond/condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,11 @@ func TestCondition_Errors(t *testing.T) {
if err == nil {
t.Error("Expected error marshalling unknown type")
}
_, err = ConvertFromCondSurrogate[any](123)
_, err = UnmarshalConditionSurrogate[any](123)
if err == nil {
t.Error("Expected error converting from invalid surrogate type")
}
_, err = ConvertFromCondSurrogate[any](map[string]any{"k": "unknown", "d": nil})
_, err = UnmarshalConditionSurrogate[any](map[string]any{"k": "unknown", "d": nil})
if err == nil {
t.Error("Expected error converting from unknown kind")
}
Expand Down
11 changes: 8 additions & 3 deletions crdt/crdt.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,14 @@ func (p *textPatch) Walk(fn func(path string, op deep.OpKind, old, new any) erro
func (p *textPatch) WithCondition(c cond.Condition[Text]) deep.Patch[Text] { return p }
func (p *textPatch) WithStrict(strict bool) deep.Patch[Text] { return p }
func (p *textPatch) Reverse() deep.Patch[Text] { return p }
func (p *textPatch) ToJSONPatch() ([]byte, error) { return nil, nil }
func (p *textPatch) Summary() string { return "Text update" }
func (p *textPatch) String() string { return "TextPatch" }
func (p *textPatch) ToJSONPatch() ([]byte, error) { return nil, nil }
func (p *textPatch) Summary() string { return "Text update" }
func (p *textPatch) String() string { return "TextPatch" }

func (p *textPatch) MarshalSerializable() (any, error) {
return deep.PatchToSerializable(p)
}


// CRDT represents a Conflict-free Replicated Data Type wrapper around type T.
type CRDT[T any] struct {
Expand Down
137 changes: 85 additions & 52 deletions patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,63 @@ type Patch[T any] interface {

// Summary returns a human-readable summary of the changes in the patch.
Summary() string

// MarshalSerializable returns a serializable representation of the patch.
MarshalSerializable() (any, error)
}

// NewPatch returns a new, empty patch for type T.
func NewPatch[T any]() Patch[T] {
return &typedPatch[T]{}
}

// UnmarshalPatchSerializable reconstructs a patch from its serializable representation.
func UnmarshalPatchSerializable[T any](data any) (Patch[T], error) {
if data == nil {
return &typedPatch[T]{}, nil
}

m, ok := data.(map[string]any)
if !ok {
// Try direct unmarshal if it's not the wrapped map
inner, err := PatchFromSerializable(data)
if err != nil {
return nil, err
}
return &typedPatch[T]{inner: inner.(diffPatch)}, nil
}

innerData, ok := m["inner"]
if !ok {
// It might be a direct surrogate map
inner, err := PatchFromSerializable(m)
if err != nil {
return nil, err
}
return &typedPatch[T]{inner: inner.(diffPatch)}, nil
}

inner, err := PatchFromSerializable(innerData)
if err != nil {
return nil, err
}

p := &typedPatch[T]{
inner: inner.(diffPatch),
}
if condData, ok := m["cond"]; ok && condData != nil {
c, err := cond.ConditionFromSerializable[T](condData)
if err != nil {
return nil, err
}
p.cond = c
}
if strict, ok := m["strict"].(bool); ok {
p.strict = strict
}
return p, nil
}

// Register registers the Patch implementation for type T with the gob package.
// This is required if you want to use Gob serialization with Patch[T].
func Register[T any]() {
Expand Down Expand Up @@ -235,6 +285,22 @@ func (p *typedPatch[T]) Summary() string {
return p.inner.summary("/")
}

func (p *typedPatch[T]) MarshalSerializable() (any, error) {
inner, err := PatchToSerializable(p.inner)
if err != nil {
return nil, err
}
c, err := cond.ConditionToSerializable(p.cond)
if err != nil {
return nil, err
}
return map[string]any{
"inner": inner,
"cond": c,
"strict": p.strict,
}, nil
}

func (p *typedPatch[T]) String() string {
if p.inner == nil {
return "<nil>"
Expand All @@ -243,84 +309,51 @@ func (p *typedPatch[T]) String() string {
}

func (p *typedPatch[T]) MarshalJSON() ([]byte, error) {
inner, err := marshalDiffPatch(p.inner)
s, err := p.MarshalSerializable()
if err != nil {
return nil, err
}
c, err := cond.MarshalCondition(p.cond)
if err != nil {
return nil, err
}
return json.Marshal(map[string]any{
"inner": inner,
"cond": c,
"strict": p.strict,
})
return json.Marshal(s)
}

func (p *typedPatch[T]) UnmarshalJSON(data []byte) error {
var m map[string]json.RawMessage
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
return err
}
if innerData, ok := m["inner"]; ok && len(innerData) > 0 && string(innerData) != "null" {
inner, err := unmarshalDiffPatch(innerData)
if err != nil {
return err
}
p.inner = inner
}
if condData, ok := m["cond"]; ok && len(condData) > 0 && string(condData) != "null" {
c, err := cond.UnmarshalCondition[T](condData)
if err != nil {
return err
}
p.cond = c
res, err := UnmarshalPatchSerializable[T](m)
if err != nil {
return err
}
if strictData, ok := m["strict"]; ok {
json.Unmarshal(strictData, &p.strict)
if tp, ok := res.(*typedPatch[T]); ok {
p.inner = tp.inner
p.cond = tp.cond
p.strict = tp.strict
}
return nil
}

func (p *typedPatch[T]) GobEncode() ([]byte, error) {
inner, err := marshalDiffPatch(p.inner)
s, err := p.MarshalSerializable()
if err != nil {
return nil, err
}
c, err := cond.MarshalCondition(p.cond)
if err != nil {
return nil, err
}
// Note: We use json-like map for consistency with surrogates
return json.Marshal(map[string]any{
"inner": inner,
"cond": c,
"strict": p.strict,
})
return json.Marshal(s)
}

func (p *typedPatch[T]) GobDecode(data []byte) error {
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
return err
}
if innerData, ok := m["inner"]; ok && innerData != nil {
inner, err := convertFromSurrogate(innerData)
if err != nil {
return err
}
p.inner = inner
}
if condData, ok := m["cond"]; ok && condData != nil {
c, err := cond.ConvertFromCondSurrogate[T](condData)
if err != nil {
return err
}
p.cond = c
res, err := UnmarshalPatchSerializable[T](m)
if err != nil {
return err
}
if strict, ok := m["strict"].(bool); ok {
p.strict = strict
if tp, ok := res.(*typedPatch[T]); ok {
p.inner = tp.inner
p.cond = tp.cond
p.strict = tp.strict
}
return nil
}
Loading