package diff import ( "errors" dmp "github.com/sergi/go-diff/diffmatchpatch" "reflect" "strconv" ) // A Delta represents an atomic difference between two JSON objects. type Delta interface { // Similarity calculates the similarity of the Delta values. // The return value is normalized from 0 to 1, // 0 is completely different and 1 is they are same Similarity() (similarity float64) } // To cache the calculated similarity, // concrete Deltas can use similariter and similarityCache type similariter interface { similarity() (similarity float64) } type similarityCache struct { similariter value float64 } func newSimilarityCache(sim similariter) similarityCache { cache := similarityCache{similariter: sim, value: -1} return cache } func (cache similarityCache) Similarity() (similarity float64) { if cache.value < 0 { cache.value = cache.similariter.similarity() } return cache.value } // A Position represents the position of a Delta in an object or an array. type Position interface { // String returns the position as a string String() (name string) // CompareTo returns a true if the Position is smaller than another Position. // This function is used to sort Positions by the sort package. CompareTo(another Position) bool } // A Name is a Postition with a string, which means the delta is in an object. type Name string func (n Name) String() (name string) { return string(n) } func (n Name) CompareTo(another Position) bool { return n < another.(Name) } // A Index is a Position with an int value, which means the Delta is in an Array. type Index int func (i Index) String() (name string) { return strconv.Itoa(int(i)) } func (i Index) CompareTo(another Position) bool { return i < another.(Index) } // A PreDelta is a Delta that has a position of the left side JSON object. // Deltas implements this interface should be applies before PostDeltas. type PreDelta interface { // PrePosition returns the Position. PrePosition() Position // PreApply applies the delta to object. PreApply(object interface{}) interface{} } type preDelta struct{ Position } func (i preDelta) PrePosition() Position { return Position(i.Position) } type preDeltas []PreDelta // for sorting func (s preDeltas) Len() int { return len(s) } // for sorting func (s preDeltas) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // for sorting func (s preDeltas) Less(i, j int) bool { return !s[i].PrePosition().CompareTo(s[j].PrePosition()) } // A PreDelta is a Delta that has a position of the right side JSON object. // Deltas implements this interface should be applies after PreDeltas. type PostDelta interface { // PostPosition returns the Position. PostPosition() Position // PostApply applies the delta to object. PostApply(object interface{}) interface{} } type postDelta struct{ Position } func (i postDelta) PostPosition() Position { return Position(i.Position) } type postDeltas []PostDelta // for sorting func (s postDeltas) Len() int { return len(s) } // for sorting func (s postDeltas) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // for sorting func (s postDeltas) Less(i, j int) bool { return s[i].PostPosition().CompareTo(s[j].PostPosition()) } // An Object is a Delta that represents an object of JSON type Object struct { postDelta similarityCache // Deltas holds internal Deltas Deltas []Delta } // NewObject returns an Object func NewObject(position Position, deltas []Delta) *Object { d := Object{postDelta: postDelta{position}, Deltas: deltas} d.similarityCache = newSimilarityCache(&d) return &d } func (d *Object) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: o := object.(map[string]interface{}) n := string(d.PostPosition().(Name)) o[n] = applyDeltas(d.Deltas, o[n]) case []interface{}: o := object.([]interface{}) n := int(d.PostPosition().(Index)) o[n] = applyDeltas(d.Deltas, o[n]) } return object } func (d *Object) similarity() (similarity float64) { similarity = deltasSimilarity(d.Deltas) return } // An Array is a Delta that represents an array of JSON type Array struct { postDelta similarityCache // Deltas holds internal Deltas Deltas []Delta } // NewArray returns an Array func NewArray(position Position, deltas []Delta) *Array { d := Array{postDelta: postDelta{position}, Deltas: deltas} d.similarityCache = newSimilarityCache(&d) return &d } func (d *Array) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: o := object.(map[string]interface{}) n := string(d.PostPosition().(Name)) o[n] = applyDeltas(d.Deltas, o[n]) case []interface{}: o := object.([]interface{}) n := int(d.PostPosition().(Index)) o[n] = applyDeltas(d.Deltas, o[n]) } return object } func (d *Array) similarity() (similarity float64) { similarity = deltasSimilarity(d.Deltas) return } // An Added represents a new added field of an object or an array type Added struct { postDelta similarityCache // Values holds the added value Value interface{} } // NewAdded returns a new Added func NewAdded(position Position, value interface{}) *Added { d := Added{postDelta: postDelta{position}, Value: value} return &d } func (d *Added) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: object.(map[string]interface{})[string(d.PostPosition().(Name))] = d.Value case []interface{}: i := int(d.PostPosition().(Index)) o := object.([]interface{}) if i < len(o) { o = append(o, 0) //dummy copy(o[i+1:], o[i:]) o[i] = d.Value object = o } else { object = append(o, d.Value) } } return object } func (d *Added) similarity() (similarity float64) { return 0 } // A Modified represents a field whose value is changed. type Modified struct { postDelta similarityCache // The value before modification OldValue interface{} // The value after modification NewValue interface{} } // NewModified returns a Modified func NewModified(position Position, oldValue, newValue interface{}) *Modified { d := Modified{ postDelta: postDelta{position}, OldValue: oldValue, NewValue: newValue, } d.similarityCache = newSimilarityCache(&d) return &d } func (d *Modified) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: // TODO check old value object.(map[string]interface{})[string(d.PostPosition().(Name))] = d.NewValue case []interface{}: object.([]interface{})[int(d.PostPosition().(Index))] = d.NewValue } return object } func (d *Modified) similarity() (similarity float64) { similarity += 0.3 // at least, they are at the same position if reflect.TypeOf(d.OldValue) == reflect.TypeOf(d.NewValue) { similarity += 0.3 // types are same switch d.OldValue.(type) { case string: similarity += 0.4 * stringSimilarity(d.OldValue.(string), d.NewValue.(string)) case float64: ratio := d.OldValue.(float64) / d.NewValue.(float64) if ratio > 1 { ratio = 1 / ratio } similarity += 0.4 * ratio } } return } // A TextDiff represents a Modified with TextDiff between the old and the new values. type TextDiff struct { Modified // Diff string Diff []dmp.Patch } // NewTextDiff returns func NewTextDiff(position Position, diff []dmp.Patch, oldValue, newValue interface{}) *TextDiff { d := TextDiff{ Modified: *NewModified(position, oldValue, newValue), Diff: diff, } return &d } func (d *TextDiff) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: o := object.(map[string]interface{}) i := string(d.PostPosition().(Name)) // TODO error d.OldValue = o[i] // TODO error d.patch() o[i] = d.NewValue case []interface{}: o := object.([]interface{}) i := d.PostPosition().(Index) d.OldValue = o[i] // TODO error d.patch() o[i] = d.NewValue } return object } func (d *TextDiff) patch() error { if d.OldValue == nil { return errors.New("Old Value is not set") } patcher := dmp.New() patched, successes := patcher.PatchApply(d.Diff, d.OldValue.(string)) for _, success := range successes { if !success { return errors.New("Failed to apply a patch") } } d.NewValue = patched return nil } func (d *TextDiff) DiffString() string { dmp := dmp.New() return dmp.PatchToText(d.Diff) } // A Delted represents deleted field or index of an Object or an Array. type Deleted struct { preDelta // The value deleted Value interface{} } // NewDeleted returns a Deleted func NewDeleted(position Position, value interface{}) *Deleted { d := Deleted{ preDelta: preDelta{position}, Value: value, } return &d } func (d *Deleted) PreApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: // TODO check old value delete(object.(map[string]interface{}), string(d.PrePosition().(Name))) case []interface{}: i := int(d.PrePosition().(Index)) o := object.([]interface{}) object = append(o[:i], o[i+1:]...) } return object } func (d Deleted) Similarity() (similarity float64) { return 0 } // A Moved represents field that is moved, which means the index or name is // changed. Note that, in this library, assigning a Moved and a Modified to // a single position is not allowed. For the compatibility with jsondiffpatch, // the Moved in this library can hold the old and new value in it. type Moved struct { preDelta postDelta similarityCache // The value before moving Value interface{} // The delta applied after moving (for compatibility) Delta interface{} } func NewMoved(oldPosition Position, newPosition Position, value interface{}, delta Delta) *Moved { d := Moved{ preDelta: preDelta{oldPosition}, postDelta: postDelta{newPosition}, Value: value, Delta: delta, } d.similarityCache = newSimilarityCache(&d) return &d } func (d *Moved) PreApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: //not supported case []interface{}: i := int(d.PrePosition().(Index)) o := object.([]interface{}) d.Value = o[i] object = append(o[:i], o[i+1:]...) } return object } func (d *Moved) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: //not supported case []interface{}: i := int(d.PostPosition().(Index)) o := object.([]interface{}) o = append(o, 0) //dummy copy(o[i+1:], o[i:]) o[i] = d.Value object = o } if d.Delta != nil { d.Delta.(PostDelta).PostApply(object) } return object } func (d *Moved) similarity() (similarity float64) { similarity = 0.6 // as type and contens are same ratio := float64(d.PrePosition().(Index)) / float64(d.PostPosition().(Index)) if ratio > 1 { ratio = 1 / ratio } similarity += 0.4 * ratio return }