diff --git a/cond/condition.go b/cond/condition.go index d58e01e..6dfcc67 100644 --- a/cond/condition.go +++ b/cond/condition.go @@ -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 } @@ -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]{ diff --git a/cond/condition_serialization.go b/cond/condition_serialization.go index 6f35d34..768a387 100644 --- a/cond/condition_serialization.go +++ b/cond/condition_serialization.go @@ -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) @@ -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 } @@ -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 } @@ -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 } @@ -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 } diff --git a/cond/condition_test.go b/cond/condition_test.go index e775637..9769cfa 100644 --- a/cond/condition_test.go +++ b/cond/condition_test.go @@ -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") } diff --git a/crdt/crdt.go b/crdt/crdt.go index ad71d54..2062661 100644 --- a/crdt/crdt.go +++ b/crdt/crdt.go @@ -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 { diff --git a/patch.go b/patch.go index 8d372a5..d0b2e7d 100644 --- a/patch.go +++ b/patch.go @@ -81,6 +81,9 @@ 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. @@ -88,6 +91,53 @@ 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]() { @@ -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 "" @@ -243,61 +309,36 @@ 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 { @@ -305,22 +346,14 @@ func (p *typedPatch[T]) GobDecode(data []byte) error { 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 } diff --git a/patch_serialization.go b/patch_serialization.go index d109259..0b89b5b 100644 --- a/patch_serialization.go +++ b/patch_serialization.go @@ -18,21 +18,21 @@ type patchSurrogate struct { func makeSurrogate(kind string, data map[string]any, p diffPatch) (*patchSurrogate, error) { c, ifC, unlessC := p.conditions() - cData, err := cond.MarshalConditionAny(c) + cData, err := cond.ConditionToSerializable(c) if err != nil { return nil, err } if cData != nil { data["c"] = cData } - ifCData, err := cond.MarshalConditionAny(ifC) + ifCData, err := cond.ConditionToSerializable(ifC) if err != nil { return nil, err } if ifCData != nil { data["if"] = ifCData } - unlessCData, err := cond.MarshalConditionAny(unlessC) + unlessCData, err := cond.ConditionToSerializable(unlessC) if err != nil { return nil, err } @@ -60,6 +60,25 @@ func RegisterCustomPatch(p any) { customPatchTypes[kind] = reflect.TypeOf(p) } +// PatchToSerializable returns a serializable representation of the patch. +// This is intended for use by pluggable encoders (e.g. gRPC, custom binary formats). +func PatchToSerializable(p any) (any, error) { + if p == nil { + return nil, nil + } + + var dp diffPatch + if typed, ok := p.(patchUnwrapper); ok { + dp = typed.unwrap() + } else if direct, ok := p.(diffPatch); ok { + dp = direct + } else { + return nil, fmt.Errorf("invalid patch type: %T", p) + } + + return marshalDiffPatch(dp) +} + func marshalDiffPatch(p diffPatch) (any, error) { if p == nil { return nil, nil @@ -194,16 +213,8 @@ func unmarshalDiffPatch(data []byte) (diffPatch, error) { func unmarshalCondFromMap(d map[string]any, key string) (any, error) { if cData, ok := d[key]; ok && cData != nil { - jsonData, err := json.Marshal(cData) - if err != nil { - return nil, err - } - // We use unmarshalCondition for 'any' since diffPatch stores 'any' (InternalCondition) - // But UnmarshalCondition returns Condition[T]. - // We need InternalCondition. - // Condition[T] implements InternalCondition. - // So we can use UnmarshalCondition[any] and return it. - c, err := cond.UnmarshalCondition[any](jsonData) + // We use ConditionFromSerializable for 'any' since diffPatch stores 'any' (InternalCondition) + c, err := cond.ConditionFromSerializable[any](cData) if err != nil { return nil, err } @@ -232,6 +243,14 @@ func unmarshalBasePatch(d map[string]any) (basePatch, error) { }, nil } +// PatchFromSerializable reconstructs a patch from its serializable representation. +func PatchFromSerializable(s any) (any, error) { + if s == nil { + return nil, nil + } + return convertFromSurrogate(s) +} + func convertFromSurrogate(s any) (diffPatch, error) { if s == nil { return nil, nil diff --git a/patch_serialization_test.go b/patch_serialization_test.go index 1286bf2..b441bc3 100644 --- a/patch_serialization_test.go +++ b/patch_serialization_test.go @@ -275,3 +275,31 @@ func TestPatchSerialization_SemanticSlice(t *testing.T) { } } } + +func TestPatchSerializable(t *testing.T) { + type TestStruct struct { + A int + B string + } + s1 := TestStruct{A: 1, B: "foo"} + s2 := TestStruct{A: 2, B: "bar"} + patch := MustDiff(s1, s2) + + // Marshal to serializable + data, err := patch.MarshalSerializable() + if err != nil { + t.Fatalf("MarshalSerializable failed: %v", err) + } + + // Unmarshal from serializable + p2, err := UnmarshalPatchSerializable[TestStruct](data) + if err != nil { + t.Fatalf("UnmarshalPatchSerializable failed: %v", err) + } + + s3 := s1 + p2.Apply(&s3) + if !reflect.DeepEqual(s2, s3) { + t.Errorf("Apply after serializable roundtrip failed. Got %+v, want %+v", s3, s2) + } +}