From 325edd0af069e1f5f05263bb0d1730ccf0a622fd Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 18:38:11 +0100 Subject: [PATCH 1/9] ... --- .golangci.yml | 6 - compare.go | 247 ++++++++++++++++++++++++ diff/ascii.go | 361 +++++++++++++++++++++++++++++++++++ diff/deltas.go | 490 ++++++++++++++++++++++++++++++++++++++++++++++++ diff/diff.go | 472 ++++++++++++++++++++++++++++++++++++++++++++++ equal.go | 235 +---------------------- equal_test.go | 31 ++- example_test.go | 4 +- go.mod | 11 +- go.sum | 91 +-------- json5/json5.go | 2 +- 11 files changed, 1610 insertions(+), 340 deletions(-) create mode 100644 compare.go create mode 100644 diff/ascii.go create mode 100644 diff/deltas.go create mode 100644 diff/diff.go diff --git a/.golangci.yml b/.golangci.yml index f13b2c9..a399ba1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,10 +5,6 @@ run: linters: default: all disable: - - nilnil - - tparallel - - err113 - - errorlint - noinlineerr - wsl_v5 - funcorder @@ -41,8 +37,6 @@ linters: check-blank: true gocyclo: min-complexity: 20 - cyclop: - max-complexity: 15 misspell: locale: US unparam: diff --git a/compare.go b/compare.go new file mode 100644 index 0000000..6aa08d4 --- /dev/null +++ b/compare.go @@ -0,0 +1,247 @@ +package assertjson + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/bool64/shared" + d2 "github.com/swaggest/assertjson/diff" +) + +func (c Comparer) varCollected(s string, v interface{}) bool { + if c.Vars != nil && c.Vars.IsVar(s) { + if _, found := c.Vars.Get(s); !found { + if n, ok := v.(json.Number); ok { + v = shared.DecodeJSONNumber(n) + } else if f, ok := v.(float64); ok && f == float64(int64(f)) { + v = int64(f) + } + + c.Vars.Set(s, v) + + return true + } + } + + return false +} + +func (c Comparer) filterDeltas(deltas []d2.Delta, ignoreAdded bool) []d2.Delta { + result := make([]d2.Delta, 0, len(deltas)) + + for _, delta := range deltas { + switch v := delta.(type) { + case *d2.Modified: + if c.IgnoreDiff == "" && c.Vars == nil { + break + } + + if s, ok := v.OldValue.(string); ok { + if s == c.IgnoreDiff { // discarding ignored diff + continue + } + + if c.varCollected(s, v.NewValue) { + continue + } + } + case *d2.Object: + v.Deltas = c.filterDeltas(v.Deltas, ignoreAdded) + if len(v.Deltas) == 0 { + continue + } + + delta = v + case *d2.Array: + v.Deltas = c.filterDeltas(v.Deltas, ignoreAdded) + if len(v.Deltas) == 0 { + continue + } + + delta = v + + case *d2.Added: + if ignoreAdded { + continue + } + } + + result = append(result, delta) + } + + return result +} + +type df struct { + deltas []d2.Delta +} + +func (df *df) Deltas() []d2.Delta { + return df.deltas +} + +func (df *df) Modified() bool { + return len(df.deltas) > 0 +} + +func (c Comparer) filterExpected(expected []byte) ([]byte, error) { + if c.Vars != nil { + for k, v := range c.Vars.GetAll() { + j, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("failed to marshal var %s: %w", k, err) // Not wrapping to support go1.12. + } + + expected = bytes.ReplaceAll(expected, []byte(`"`+k+`"`), j) + } + } + + return expected, nil +} + +func (c Comparer) compare(expDecoded, actDecoded interface{}) (d2.Diff, error) { + switch v := expDecoded.(type) { + case []interface{}: + if actArray, ok := actDecoded.([]interface{}); ok { + return d2.New().CompareArrays(v, actArray), nil + } + + return nil, errors.New("types mismatch, array expected") + + case map[string]interface{}: + if actObject, ok := actDecoded.(map[string]interface{}); ok { + return d2.New().CompareObjects(v, actObject), nil + } + + return nil, errors.New("types mismatch, object expected") + + default: + if !reflect.DeepEqual(expDecoded, actDecoded) { // scalar value comparison + return nil, fmt.Errorf("values %v and %v are not equal", expDecoded, actDecoded) + } + } + + return nil, nil +} + +func unmarshal(data []byte, decoded interface{}) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + + return dec.Decode(decoded) +} + +func (c Comparer) fail(expected, actual []byte, ignoreAdded bool) error { + var expDecoded, actDecoded interface{} + + expected, err := c.filterExpected(expected) + if err != nil { + return err + } + + err = unmarshal(expected, &expDecoded) + if err != nil { + return fmt.Errorf("failed to unmarshal expected:\n%wv", err) + } + + err = unmarshal(actual, &actDecoded) + if err != nil { + return fmt.Errorf("failed to unmarshal actual:\n%wv", err) + } + + if s, ok := expDecoded.(string); ok && c.Vars != nil && c.Vars.IsVar(s) { + if c.varCollected(s, actDecoded) { + return nil + } + + if v, found := c.Vars.Get(s); found { + expDecoded = v + } + } + + diffValue, err := c.compare(expDecoded, actDecoded) + if err != nil { + return err + } + + if diffValue == nil { + return nil + } + + if !diffValue.Modified() { + return nil + } + + diffValue = &df{deltas: c.filterDeltas(diffValue.Deltas(), ignoreAdded)} + if !diffValue.Modified() { + return nil + } + + diffText, err := d2.NewAsciiFormatter(expDecoded, c.FormatterConfig).Format(diffValue) + if err != nil { + return fmt.Errorf("failed to format diff:\n%wv", err) + } + + diffText = c.reduceDiff(diffText) + + return errors.New("not equal:\n" + diffText) +} + +func (c Comparer) reduceDiff(diffText string) string { + if c.KeepFullDiff { + return diffText + } + + if c.FullDiffMaxLines == 0 { + c.FullDiffMaxLines = 50 + } + + if c.DiffSurroundingLines == 0 { + c.DiffSurroundingLines = 5 + } + + diffRows := strings.Split(diffText, "\n") + if len(diffRows) <= c.FullDiffMaxLines { + return diffText + } + + var result []string + + prev := 0 + + for i, r := range diffRows { + if len(r) == 0 { + continue + } + + if r[0] == '-' || r[0] == '+' { + start := i - c.DiffSurroundingLines + if start < prev { + start = prev + } else if start > prev { + result = append(result, "...") + } + + end := i + c.DiffSurroundingLines + if end >= len(diffRows) { + end = len(diffRows) - 1 + } + + prev = end + + for k := start; k < end; k++ { + result = append(result, diffRows[k]) + } + } + } + + if prev < len(diffRows)-1 { + result = append(result, "...") + } + + return strings.Join(result, "\n") +} diff --git a/diff/ascii.go b/diff/ascii.go new file mode 100644 index 0000000..efe45c6 --- /dev/null +++ b/diff/ascii.go @@ -0,0 +1,361 @@ +package diff + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" +) + +func NewAsciiFormatter(left interface{}, config AsciiFormatterConfig) *AsciiFormatter { + return &AsciiFormatter{ + left: left, + config: config, + } +} + +type AsciiFormatter struct { + left interface{} + config AsciiFormatterConfig + buffer *bytes.Buffer + path []string + size []int + inArray []bool + line *AsciiLine +} + +type AsciiFormatterConfig struct { + ShowArrayIndex bool + Coloring bool +} + +var AsciiFormatterDefaultConfig = AsciiFormatterConfig{} + +type AsciiLine struct { + marker string + indent int + buffer *bytes.Buffer +} + +func (f *AsciiFormatter) Format(diff Diff) (result string, err error) { + f.buffer = bytes.NewBuffer([]byte{}) + f.path = []string{} + f.size = []int{} + f.inArray = []bool{} + + if v, ok := f.left.(map[string]interface{}); ok { + f.formatObject(v, diff) + } else if v, ok := f.left.([]interface{}); ok { + f.formatArray(v, diff) + } else { + return "", fmt.Errorf("expected map[string]interface{} or []interface{}, got %T", + f.left) + } + + return f.buffer.String(), nil +} + +func (f *AsciiFormatter) formatObject(left map[string]interface{}, df Diff) { + f.addLineWith(AsciiSame, "{") + f.push("ROOT", len(left), false) + f.processObject(left, df.Deltas()) + f.pop() + f.addLineWith(AsciiSame, "}") +} + +func (f *AsciiFormatter) formatArray(left []interface{}, df Diff) { + f.addLineWith(AsciiSame, "[") + f.push("ROOT", len(left), true) + f.processArray(left, df.Deltas()) + f.pop() + f.addLineWith(AsciiSame, "]") +} + +func (f *AsciiFormatter) processArray(array []interface{}, deltas []Delta) error { + patchedIndex := 0 + for index, value := range array { + f.processItem(value, deltas, Index(index)) + patchedIndex++ + } + + // additional Added + for _, delta := range deltas { + switch delta.(type) { + case *Added: + d := delta.(*Added) + // skip items already processed + if int(d.Position.(Index)) < len(array) { + continue + } + f.printRecursive(d.Position.String(), d.Value, AsciiAdded) + } + } + + return nil +} + +func (f *AsciiFormatter) processObject(object map[string]interface{}, deltas []Delta) error { + names := sortedKeys(object) + for _, name := range names { + value := object[name] + f.processItem(value, deltas, Name(name)) + } + + // Added + for _, delta := range deltas { + switch delta.(type) { + case *Added: + d := delta.(*Added) + f.printRecursive(d.Position.String(), d.Value, AsciiAdded) + } + } + + return nil +} + +func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position Position) error { + matchedDeltas := f.searchDeltas(deltas, position) + positionStr := position.String() + if len(matchedDeltas) > 0 { + for _, matchedDelta := range matchedDeltas { + switch matchedDelta.(type) { + case *Object: + d := matchedDelta.(*Object) + switch value.(type) { + case map[string]interface{}: + // ok + default: + return errors.New("Type mismatch") + } + o := value.(map[string]interface{}) + + f.newLine(AsciiSame) + f.printKey(positionStr) + f.print("{") + f.closeLine() + f.push(positionStr, len(o), false) + f.processObject(o, d.Deltas) + f.pop() + f.newLine(AsciiSame) + f.print("}") + f.printComma() + f.closeLine() + + case *Array: + d := matchedDelta.(*Array) + switch value.(type) { + case []interface{}: + // ok + default: + return errors.New("Type mismatch") + } + a := value.([]interface{}) + + f.newLine(AsciiSame) + f.printKey(positionStr) + f.print("[") + f.closeLine() + f.push(positionStr, len(a), true) + f.processArray(a, d.Deltas) + f.pop() + f.newLine(AsciiSame) + f.print("]") + f.printComma() + f.closeLine() + + case *Added: + d := matchedDelta.(*Added) + f.printRecursive(positionStr, d.Value, AsciiAdded) + f.size[len(f.size)-1]++ + + case *Modified: + d := matchedDelta.(*Modified) + savedSize := f.size[len(f.size)-1] + f.printRecursive(positionStr, d.OldValue, AsciiDeleted) + f.size[len(f.size)-1] = savedSize + f.printRecursive(positionStr, d.NewValue, AsciiAdded) + + case *TextDiff: + savedSize := f.size[len(f.size)-1] + d := matchedDelta.(*TextDiff) + f.printRecursive(positionStr, d.OldValue, AsciiDeleted) + f.size[len(f.size)-1] = savedSize + f.printRecursive(positionStr, d.NewValue, AsciiAdded) + + case *Deleted: + d := matchedDelta.(*Deleted) + f.printRecursive(positionStr, d.Value, AsciiDeleted) + + default: + return errors.New("Unknown Delta type detected") + } + } + } else { + f.printRecursive(positionStr, value, AsciiSame) + } + + return nil +} + +func (f *AsciiFormatter) searchDeltas(deltas []Delta, postion Position) (results []Delta) { + results = make([]Delta, 0) + for _, delta := range deltas { + switch delta.(type) { + case PostDelta: + if delta.(PostDelta).PostPosition() == postion { + results = append(results, delta) + } + case PreDelta: + if delta.(PreDelta).PrePosition() == postion { + results = append(results, delta) + } + default: + panic("heh") + } + } + return +} + +const ( + AsciiSame = " " + AsciiAdded = "+" + AsciiDeleted = "-" +) + +var AsciiStyles = map[string]string{ + AsciiAdded: "30;42", + AsciiDeleted: "30;41", +} + +func (f *AsciiFormatter) push(name string, size int, array bool) { + f.path = append(f.path, name) + f.size = append(f.size, size) + f.inArray = append(f.inArray, array) +} + +func (f *AsciiFormatter) pop() { + f.path = f.path[0 : len(f.path)-1] + f.size = f.size[0 : len(f.size)-1] + f.inArray = f.inArray[0 : len(f.inArray)-1] +} + +func (f *AsciiFormatter) addLineWith(marker string, value string) { + f.line = &AsciiLine{ + marker: marker, + indent: len(f.path), + buffer: bytes.NewBufferString(value), + } + f.closeLine() +} + +func (f *AsciiFormatter) newLine(marker string) { + f.line = &AsciiLine{ + marker: marker, + indent: len(f.path), + buffer: bytes.NewBuffer([]byte{}), + } +} + +func (f *AsciiFormatter) closeLine() { + style, ok := AsciiStyles[f.line.marker] + if f.config.Coloring && ok { + f.buffer.WriteString("\x1b[" + style + "m") + } + + f.buffer.WriteString(f.line.marker) + for n := 0; n < f.line.indent; n++ { + f.buffer.WriteString(" ") + } + f.buffer.Write(f.line.buffer.Bytes()) + + if f.config.Coloring && ok { + f.buffer.WriteString("\x1b[0m") + } + + f.buffer.WriteRune('\n') +} + +func (f *AsciiFormatter) printKey(name string) { + if !f.inArray[len(f.inArray)-1] { + fmt.Fprintf(f.line.buffer, `"%s": `, name) + } else if f.config.ShowArrayIndex { + fmt.Fprintf(f.line.buffer, `%s: `, name) + } +} + +func (f *AsciiFormatter) printComma() { + f.size[len(f.size)-1]-- + if f.size[len(f.size)-1] > 0 { + f.line.buffer.WriteRune(',') + } +} + +func (f *AsciiFormatter) printValue(value interface{}) { + switch v := value.(type) { + case uint64: + fmt.Fprint(f.line.buffer, v) + case json.Number: + fmt.Fprint(f.line.buffer, v.String()) + case string: + fmt.Fprintf(f.line.buffer, `"%s"`, value) + case nil: + f.line.buffer.WriteString("null") + default: + fmt.Fprintf(f.line.buffer, `%#v`, value) + } +} + +func (f *AsciiFormatter) print(a string) { + f.line.buffer.WriteString(a) +} + +func (f *AsciiFormatter) printRecursive(name string, value interface{}, marker string) { + switch value.(type) { + case map[string]interface{}: + f.newLine(marker) + f.printKey(name) + f.print("{") + f.closeLine() + + m := value.(map[string]interface{}) + size := len(m) + f.push(name, size, false) + + keys := sortedKeys(m) + for _, key := range keys { + f.printRecursive(key, m[key], marker) + } + f.pop() + + f.newLine(marker) + f.print("}") + f.printComma() + f.closeLine() + + case []interface{}: + f.newLine(marker) + f.printKey(name) + f.print("[") + f.closeLine() + + s := value.([]interface{}) + size := len(s) + f.push("", size, true) + for _, item := range s { + f.printRecursive("", item, marker) + } + f.pop() + + f.newLine(marker) + f.print("]") + f.printComma() + f.closeLine() + + default: + f.newLine(marker) + f.printKey(name) + f.printValue(value) + f.printComma() + f.closeLine() + } +} diff --git a/diff/deltas.go b/diff/deltas.go new file mode 100644 index 0000000..2e57de1 --- /dev/null +++ b/diff/deltas.go @@ -0,0 +1,490 @@ +package diff + +import ( + "errors" + "reflect" + "strconv" + + dmp "github.com/sergi/go-diff/diffmatchpatch" +) + +// 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.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 similarity +} + +// 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 similarity +} + +// 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 similarity +} + +// 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 contents are same + + ratio := float64(d.PrePosition().(Index)) / float64(d.PostPosition().(Index)) + if ratio > 1 { + ratio = 1 / ratio + } + + similarity += 0.4 * ratio + + return similarity +} diff --git a/diff/diff.go b/diff/diff.go new file mode 100644 index 0000000..aa30da7 --- /dev/null +++ b/diff/diff.go @@ -0,0 +1,472 @@ +// Package diff implements "Diff" that compares two JSON objects and +// generates Deltas that describes differences between them. The package also +// provides "Patch" that apply Deltas to a JSON object. +// +// Updated copy from https://github.com/yudai/gojsondiff. +package diff + +import ( + "container/list" + "encoding/json" + "reflect" + "sort" + + "github.com/bool64/shared" + dmp "github.com/sergi/go-diff/diffmatchpatch" + lcs "github.com/yudai/golcs" +) + +// A Diff holds deltas generated by a Differ. +type Diff interface { + // Deltas returns Deltas that describe differences between two JSON objects + Deltas() []Delta + // Modified returns true if Diff has at least one Delta. + Modified() bool +} + +type diff struct { + deltas []Delta +} + +func (diff *diff) Deltas() []Delta { + return diff.deltas +} + +func (diff *diff) Modified() bool { + return len(diff.deltas) > 0 +} + +// A Differ compares JSON objects and apply patches. +type Differ struct { + textDiffMinimumLength int +} + +// New returns new Differ with default configuration. +func New() *Differ { + return &Differ{ + textDiffMinimumLength: 30, + } +} + +// Compare compares two JSON strings as []bytes and return a Diff object. +func (differ *Differ) Compare( + left []byte, + right []byte, +) (Diff, error) { + var leftMap, rightMap map[string]interface{} + + err := json.Unmarshal(left, &leftMap) + if err != nil { + return nil, err + } + + err = json.Unmarshal(right, &rightMap) + if err != nil { + return nil, err + } + + return differ.CompareObjects(leftMap, rightMap), nil +} + +// CompareObjects compares two JSON object as map[string]interface{} +// and return a Diff object. +func (differ *Differ) CompareObjects( + left map[string]interface{}, + right map[string]interface{}, +) Diff { + deltas := differ.compareMaps(left, right) + + return &diff{deltas: deltas} +} + +// CompareArrays compares two JSON arrays as []interface{} +// and return a Diff object. +func (differ *Differ) CompareArrays( + left []interface{}, + right []interface{}, +) Diff { + deltas := differ.compareArrays(left, right) + + return &diff{deltas: deltas} +} + +func (differ *Differ) compareMaps( + left map[string]interface{}, + right map[string]interface{}, +) (deltas []Delta) { + deltas = make([]Delta, 0) + + names := sortedKeys(left) // stabilize delta order + for _, name := range names { + if rightValue, ok := right[name]; ok { + same, delta := differ.compareValues(Name(name), left[name], rightValue) + if !same { + deltas = append(deltas, delta) + } + } else { + deltas = append(deltas, NewDeleted(Name(name), left[name])) + } + } + + names = sortedKeys(right) // stabilize delta order + for _, name := range names { + if _, ok := left[name]; !ok { + deltas = append(deltas, NewAdded(Name(name), right[name])) + } + } + + return deltas +} + +// ApplyPatch applies a Diff to an JSON object. This method is destructive. +func (differ *Differ) ApplyPatch(json map[string]interface{}, patch Diff) { + applyDeltas(patch.Deltas(), json) +} + +type maybe struct { + index int + lcsIndex int + item interface{} +} + +func (differ *Differ) compareArrays( + left []interface{}, + right []interface{}, +) (deltas []Delta) { + deltas = make([]Delta, 0) + // LCS index pairs + lcsPairs := lcs.New(left, right).IndexPairs() + + // list up items not in LCS, they are maybe deleted + maybeDeleted := list.New() // but maybe moved or modified + + lcsI := 0 + for i, leftValue := range left { + if lcsI < len(lcsPairs) && lcsPairs[lcsI].Left == i { + lcsI++ + } else { + maybeDeleted.PushBack(maybe{index: i, lcsIndex: lcsI, item: leftValue}) + } + } + + // list up items not in LCS, they are maybe Added + maybeAdded := list.New() // but maybe moved or modified + + lcsI = 0 + for i, rightValue := range right { + if lcsI < len(lcsPairs) && lcsPairs[lcsI].Right == i { + lcsI++ + } else { + maybeAdded.PushBack(maybe{index: i, lcsIndex: lcsI, item: rightValue}) + } + } + + // find moved items + var delNext *list.Element // for prefetch to remove item in iteration + + for delCandidate := maybeDeleted.Front(); delCandidate != nil; delCandidate = delNext { + delCan := delCandidate.Value.(maybe) + delNext = delCandidate.Next() + + for addCandidate := maybeAdded.Front(); addCandidate != nil; addCandidate = addCandidate.Next() { + addCan := addCandidate.Value.(maybe) + if reflect.DeepEqual(delCan.item, addCan.item) { + deltas = append(deltas, NewMoved(Index(delCan.index), Index(addCan.index), delCan.item, nil)) + + maybeAdded.Remove(addCandidate) + maybeDeleted.Remove(delCandidate) + + break + } + } + } + + // find modified or add+del + prevIndexDel := 0 + prevIndexAdd := 0 + delElement := maybeDeleted.Front() + addElement := maybeAdded.Front() + + for i := 0; i <= len(lcsPairs); i++ { // not "< len(lcsPairs)" + var lcsPair lcs.IndexPair + + var delSize, addSize int + + if i < len(lcsPairs) { + lcsPair = lcsPairs[i] + delSize = lcsPair.Left - prevIndexDel - 1 + addSize = lcsPair.Right - prevIndexAdd - 1 + prevIndexDel = lcsPair.Left + prevIndexAdd = lcsPair.Right + } + + var delSlice []maybe + if delSize > 0 { + delSlice = make([]maybe, 0, delSize) + } else { + delSlice = make([]maybe, 0, maybeDeleted.Len()) + } + + for ; delElement != nil; delElement = delElement.Next() { + d := delElement.Value.(maybe) + if d.lcsIndex != i { + break + } + + delSlice = append(delSlice, d) + } + + var addSlice []maybe + if addSize > 0 { + addSlice = make([]maybe, 0, addSize) + } else { + addSlice = make([]maybe, 0, maybeAdded.Len()) + } + + for ; addElement != nil; addElement = addElement.Next() { + a := addElement.Value.(maybe) + if a.lcsIndex != i { + break + } + + addSlice = append(addSlice, a) + } + + if len(delSlice) > 0 && len(addSlice) > 0 { + var bestDeltas []Delta + bestDeltas, delSlice, addSlice = differ.maximizeSimilarities(delSlice, addSlice) + + deltas = append(deltas, bestDeltas...) + } + + for _, del := range delSlice { + deltas = append(deltas, NewDeleted(Index(del.index), del.item)) + } + + for _, add := range addSlice { + deltas = append(deltas, NewAdded(Index(add.index), add.item)) + } + } + + return deltas +} + +func (differ *Differ) compareValues( + position Position, + left interface{}, + right interface{}, +) (same bool, delta Delta) { + if reflect.TypeOf(left) != reflect.TypeOf(right) { + return false, NewModified(position, left, right) + } + + switch left.(type) { + case map[string]interface{}: + l := left.(map[string]interface{}) + + childDeltas := differ.compareMaps(l, right.(map[string]interface{})) + if len(childDeltas) > 0 { + return false, NewObject(position, childDeltas) + } + + case []interface{}: + l := left.([]interface{}) + childDeltas := differ.compareArrays(l, right.([]interface{})) + + if len(childDeltas) > 0 { + return false, NewArray(position, childDeltas) + } + + default: + if !reflect.DeepEqual(left, right) { + if ln, ok := left.(json.Number); ok { + left = shared.DecodeJSONNumber(ln) + } + + if rn, ok := right.(json.Number); ok { + right = shared.DecodeJSONNumber(rn) + } + + if reflect.ValueOf(left).Kind() == reflect.String && + reflect.ValueOf(right).Kind() == reflect.String && + differ.textDiffMinimumLength <= len(reflect.ValueOf(left).String()) { + textDiff := dmp.New() + patchs := textDiff.PatchMake(reflect.ValueOf(left).String(), reflect.ValueOf(right).String()) + + return false, NewTextDiff(position, patchs, left, right) + } else { + return false, NewModified(position, left, right) + } + } + } + + return true, nil +} + +func applyDeltas(deltas []Delta, object interface{}) interface{} { + preDeltas := make(preDeltas, 0) + + for _, delta := range deltas { + switch delta := delta.(type) { + case PreDelta: + preDeltas = append(preDeltas, delta) + } + } + + sort.Sort(preDeltas) + + for _, delta := range preDeltas { + object = delta.PreApply(object) + } + + postDeltas := make(postDeltas, 0, len(deltas)-len(preDeltas)) + + for _, delta := range deltas { + switch delta := delta.(type) { + case PostDelta: + postDeltas = append(postDeltas, delta) + } + } + + sort.Sort(postDeltas) + + for _, delta := range postDeltas { + object = delta.PostApply(object) + } + + return object +} + +func (differ *Differ) maximizeSimilarities(left []maybe, right []maybe) (resultDeltas []Delta, freeLeft, freeRight []maybe) { + deltaTable := make([][]Delta, len(left)) + + for i := 0; i < len(left); i++ { + deltaTable[i] = make([]Delta, len(right)) + } + + for i, leftValue := range left { + for j, rightValue := range right { + _, delta := differ.compareValues(Index(rightValue.index), leftValue.item, rightValue.item) + deltaTable[i][j] = delta + } + } + + sizeX := len(left) + 1 // margins for both sides + sizeY := len(right) + 1 + + // fill out with similarities + dpTable := make([][]float64, sizeX) + + for i := 0; i < sizeX; i++ { + dpTable[i] = make([]float64, sizeY) + } + + for x := sizeX - 2; x >= 0; x-- { + for y := sizeY - 2; y >= 0; y-- { + prevX := dpTable[x+1][y] + prevY := dpTable[x][y+1] + score := deltaTable[x][y].Similarity() + dpTable[x+1][y+1] + + dpTable[x][y] = max(prevX, prevY, score) + } + } + + minLength := len(left) + + if minLength > len(right) { + minLength = len(right) + } + + maxInvalidLength := minLength - 1 + + freeLeft = make([]maybe, 0, len(left)-minLength) + freeRight = make([]maybe, 0, len(right)-minLength) + + resultDeltas = make([]Delta, 0, minLength) + + var x, y int + + for x, y = 0, 0; x <= sizeX-2 && y <= sizeY-2; { + current := dpTable[x][y] + nextX := dpTable[x+1][y] + nextY := dpTable[x][y+1] + + xValidLength := len(left) - maxInvalidLength + y + yValidLength := len(right) - maxInvalidLength + x + + if x+1 < xValidLength && current == nextX { + freeLeft = append(freeLeft, left[x]) + x++ + } else if y+1 < yValidLength && current == nextY { + freeRight = append(freeRight, right[y]) + y++ + } else { + resultDeltas = append(resultDeltas, deltaTable[x][y]) + x++ + y++ + } + } + + for ; x < sizeX-1; x++ { + freeLeft = append(freeLeft, left[x-1]) + } + + for ; y < sizeY-1; y++ { + freeRight = append(freeRight, right[y-1]) + } + + return resultDeltas, freeLeft, freeRight +} + +func deltasSimilarity(deltas []Delta) (similarity float64) { + for _, delta := range deltas { + similarity += delta.Similarity() + } + + similarity = similarity / float64(len(deltas)) + + return similarity +} + +func stringSimilarity(left, right string) (similarity float64) { + matchingLength := float64( + lcs.New( + stringToInterfaceSlice(left), + stringToInterfaceSlice(right), + ).Length(), + ) + similarity = (matchingLength / float64(len(left))) * (matchingLength / float64(len(right))) + + return similarity +} + +func stringToInterfaceSlice(str string) []interface{} { + s := make([]interface{}, len(str)) + for i, v := range str { + s[i] = v + } + + return s +} + +func sortedKeys(m map[string]interface{}) (keys []string) { + keys = make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + + sort.Strings(keys) + + return keys +} + +func max(first float64, rest ...float64) (max float64) { + max = first + for _, value := range rest { + if max < value { + max = value + } + } + + return max +} diff --git a/equal.go b/equal.go index a19a768..96acdea 100644 --- a/equal.go +++ b/equal.go @@ -2,17 +2,11 @@ package assertjson import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "reflect" "strings" "github.com/bool64/shared" "github.com/stretchr/testify/assert" - "github.com/yudai/gojsondiff" - "github.com/yudai/gojsondiff/formatter" + d2 "github.com/swaggest/assertjson/diff" ) // Comparer compares JSON documents. @@ -24,7 +18,7 @@ type Comparer struct { Vars *shared.Vars // FormatterConfig controls diff formatter configuration. - FormatterConfig formatter.AsciiFormatterConfig + FormatterConfig d2.AsciiFormatterConfig // KeepFullDiff shows full diff in error message. KeepFullDiff bool @@ -106,80 +100,6 @@ func (c Comparer) EqualMarshal(t TestingT, expected []byte, actualValue interfac return c.Equal(t, expected, actual, msgAndArgs...) } -func (c Comparer) varCollected(s string, v interface{}) bool { - if c.Vars != nil && c.Vars.IsVar(s) { - if _, found := c.Vars.Get(s); !found { - if f, ok := v.(float64); ok && f == float64(int64(f)) { - v = int64(f) - } - - c.Vars.Set(s, v) - - return true - } - } - - return false -} - -func (c Comparer) filterDeltas(deltas []gojsondiff.Delta, ignoreAdded bool) []gojsondiff.Delta { - result := make([]gojsondiff.Delta, 0, len(deltas)) - - for _, delta := range deltas { - switch v := delta.(type) { - case *gojsondiff.Modified: - if c.IgnoreDiff == "" && c.Vars == nil { - break - } - - if s, ok := v.OldValue.(string); ok { - if s == c.IgnoreDiff { // discarding ignored diff - continue - } - - if c.varCollected(s, v.NewValue) { - continue - } - } - case *gojsondiff.Object: - v.Deltas = c.filterDeltas(v.Deltas, ignoreAdded) - if len(v.Deltas) == 0 { - continue - } - - delta = v - case *gojsondiff.Array: - v.Deltas = c.filterDeltas(v.Deltas, ignoreAdded) - if len(v.Deltas) == 0 { - continue - } - - delta = v - - case *gojsondiff.Added: - if ignoreAdded { - continue - } - } - - result = append(result, delta) - } - - return result -} - -type diff struct { - deltas []gojsondiff.Delta -} - -func (diff *diff) Deltas() []gojsondiff.Delta { - return diff.deltas -} - -func (diff *diff) Modified() bool { - return len(diff.deltas) > 0 -} - // FailNotEqual returns error if JSON payloads are different, nil otherwise. func FailNotEqual(expected, actual []byte) error { return defaultComparer.FailNotEqual(expected, actual) @@ -190,46 +110,6 @@ func FailNotEqualMarshal(expected []byte, actualValue interface{}) error { return defaultComparer.FailNotEqualMarshal(expected, actualValue) } -func (c Comparer) filterExpected(expected []byte) ([]byte, error) { - if c.Vars != nil { - for k, v := range c.Vars.GetAll() { - j, err := json.Marshal(v) - if err != nil { - return nil, fmt.Errorf("failed to marshal var %s: %v", k, err) // Not wrapping to support go1.12. - } - - expected = bytes.Replace(expected, []byte(`"`+k+`"`), j, -1) //nolint:gocritic,staticcheck // To support go1.11. - } - } - - return expected, nil -} - -func (c Comparer) compare(expDecoded, actDecoded interface{}) (gojsondiff.Diff, error) { - switch v := expDecoded.(type) { - case []interface{}: - if actArray, ok := actDecoded.([]interface{}); ok { - return gojsondiff.New().CompareArrays(v, actArray), nil - } - - return nil, errors.New("types mismatch, array expected") - - case map[string]interface{}: - if actObject, ok := actDecoded.(map[string]interface{}); ok { - return gojsondiff.New().CompareObjects(v, actObject), nil - } - - return nil, errors.New("types mismatch, object expected") - - default: - if !reflect.DeepEqual(expDecoded, actDecoded) { // scalar value comparison - return nil, fmt.Errorf("values %v and %v are not equal", expDecoded, actDecoded) - } - } - - return nil, nil -} - // FailNotEqualMarshal returns error if expected JSON payload is not equal to marshaled actual value. func (c Comparer) FailNotEqualMarshal(expected []byte, actualValue interface{}) error { actual, err := MarshalIndentCompact(actualValue, "", " ", 80) @@ -245,117 +125,6 @@ func (c Comparer) FailNotEqual(expected, actual []byte) error { return c.fail(expected, actual, false) } -func (c Comparer) fail(expected, actual []byte, ignoreAdded bool) error { - var expDecoded, actDecoded interface{} - - expected, err := c.filterExpected(expected) - if err != nil { - return err - } - - err = json.Unmarshal(expected, &expDecoded) - if err != nil { - return fmt.Errorf("failed to unmarshal expected:\n%+v", err) - } - - err = json.Unmarshal(actual, &actDecoded) - if err != nil { - return fmt.Errorf("failed to unmarshal actual:\n%+v", err) - } - - if s, ok := expDecoded.(string); ok && c.Vars != nil && c.Vars.IsVar(s) { - if c.varCollected(s, actDecoded) { - return nil - } - - if v, found := c.Vars.Get(s); found { - expDecoded = v - } - } - - diffValue, err := c.compare(expDecoded, actDecoded) - if err != nil { - return err - } - - if diffValue == nil { - return nil - } - - if !diffValue.Modified() { - return nil - } - - diffValue = &diff{deltas: c.filterDeltas(diffValue.Deltas(), ignoreAdded)} - if !diffValue.Modified() { - return nil - } - - diffText, err := formatter.NewAsciiFormatter(expDecoded, c.FormatterConfig).Format(diffValue) - if err != nil { - return fmt.Errorf("failed to format diff:\n%+v", err) - } - - diffText = c.reduceDiff(diffText) - - return errors.New("not equal:\n" + diffText) -} - -func (c Comparer) reduceDiff(diffText string) string { - if c.KeepFullDiff { - return diffText - } - - if c.FullDiffMaxLines == 0 { - c.FullDiffMaxLines = 50 - } - - if c.DiffSurroundingLines == 0 { - c.DiffSurroundingLines = 5 - } - - diffRows := strings.Split(diffText, "\n") - if len(diffRows) <= c.FullDiffMaxLines { - return diffText - } - - var result []string - - prev := 0 - - for i, r := range diffRows { - if len(r) == 0 { - continue - } - - if r[0] == '-' || r[0] == '+' { - start := i - c.DiffSurroundingLines - if start < prev { - start = prev - } else if start > prev { - result = append(result, "...") - } - - end := i + c.DiffSurroundingLines - if end >= len(diffRows) { - end = len(diffRows) - 1 - } - - prev = end - - for k := start; k < end; k++ { - result = append(result, diffRows[k]) - } - } - } - - if prev < len(diffRows)-1 { - result = append(result, "...") - } - - return strings.Join(result, "\n") -} - // EqMarshal marshals actual value and compares two JSON documents ignoring string values "". func EqMarshal(t TestingT, expected string, actualValue interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/equal_test.go b/equal_test.go index f2d1d29..d8707c4 100644 --- a/equal_test.go +++ b/equal_test.go @@ -59,8 +59,8 @@ func TestEquals_message(t *testing.T) { assert.Equal(t, "\n%s", format) assert.Len(t, args, 1) - assert.Equal(t, ` Error Trace: equal.go:88 - equal.go:63 + assert.Equal(t, ` Error Trace: equal.go:82 + equal.go:57 equal_test.go:58 Error: Not equal: { @@ -237,7 +237,32 @@ func TestComparer_Equal_vars_scalar(t *testing.T) { b, found := v.Get("$varB") assert.True(t, found) - assert.Equal(t, []interface{}{123.0}, b) + assert.Equal(t, []interface{}{int64(123)}, b) + + assert.NoError(t, c.FailNotEqual([]byte(`"$varC"`), []byte(`{"a":17294094973108486143}`))) + assert.EqualError(t, c.FailNotEqual([]byte(`"$varC"`), []byte(`{"a":17294094973108486144}`)), + "not equal:\n {\n- \"a\": 17294094973108486143\n+ \"a\": 17294094973108486144\n }\n") + assert.NoError(t, c.FailNotEqual([]byte(`"$varC"`), []byte(`{"a":17294094973108486143}`))) + + c1, found := v.Get("$varC") + + assert.True(t, found) + assert.Equal(t, map[string]interface{}{"a": uint64(17294094973108486143)}, c1) +} + +func TestComparer_Equal_vars_uint64(t *testing.T) { + v := &shared.Vars{} + c := assertjson.Comparer{Vars: v} + + assert.NoError(t, c.FailNotEqual([]byte(`"$varC"`), []byte(`{"a":17294094973108486143}`))) + assert.EqualError(t, c.FailNotEqual([]byte(`"$varC"`), []byte(`{"a":17294094973108486144}`)), + "not equal:\n {\n- \"a\": 17294094973108486143\n+ \"a\": 17294094973108486144\n }\n") + assert.NoError(t, c.FailNotEqual([]byte(`"$varC"`), []byte(`{"a":17294094973108486143}`))) + + c1, found := v.Get("$varC") + + assert.True(t, found) + assert.Equal(t, map[string]interface{}{"a": uint64(17294094973108486143)}, c1) } func TestComparer_Equal_long(t *testing.T) { diff --git a/example_test.go b/example_test.go index 7097c48..78bfe0b 100644 --- a/example_test.go +++ b/example_test.go @@ -17,8 +17,8 @@ func Example() { ) // Output: - // Error Trace: equal.go:88 - // equal.go:63 + // Error Trace: equal.go:82 + // equal.go:57 // example_test.go:14 // Error: Not equal: // { diff --git a/go.mod b/go.mod index 56c5685..41a2f10 100644 --- a/go.mod +++ b/go.mod @@ -4,21 +4,16 @@ go 1.17 require ( github.com/bool64/dev v0.2.43 - github.com/bool64/shared v0.1.5 + github.com/bool64/shared v0.1.6 github.com/iancoleman/orderedmap v0.3.0 + github.com/sergi/go-diff v1.4.0 github.com/stretchr/testify v1.4.0 github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff - github.com/yudai/gojsondiff v1.0.0 + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect - github.com/onsi/ginkgo v1.15.2 // indirect - github.com/onsi/gomega v1.11.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sergi/go-diff v1.1.0 // indirect - github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - github.com/yudai/pp v2.0.1+incompatible // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e6f44d8..bb9d246 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,10 @@ -github.com/bool64/dev v0.2.17/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/dev v0.2.43 h1:yQ7qiZVef6WtCl2vDYU0Y+qSq+0aBrQzY8KXkklk9cQ= github.com/bool64/dev v0.2.43/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= -github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= -github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/bool64/shared v0.1.6 h1:1u1IfTU84pZU285Mf1kQC5wX/VzSRE5E/+4KgFRGQ6o= +github.com/bool64/shared v0.1.6/go.mod h1:AByMlOFBjavJDk8VdFBH/atMgv1q7qrKXD1XLAQTgZA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -28,87 +12,20 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.15.2 h1:l77YT15o814C2qVL47NOyjV/6RbaP7kKdrvZnxQ3Org= -github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= -github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff h1:7YqG491bE4vstXRz1lD38rbSgbXnirvROz1lZiOnPO8= github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/json5/json5.go b/json5/json5.go index cc32ccf..fa092d5 100644 --- a/json5/json5.go +++ b/json5/json5.go @@ -60,7 +60,7 @@ func Unmarshal(data []byte, v interface{}) error { // Second decode to make sure there is only one JSON5 value in data and no garbage in tail. err = dec.Decode(&tail) - if err != io.EOF { + if !errors.Is(err, io.EOF) { return errors.New("unexpected bytes after JSON5 payload") } From 4ca1f70e47f123254bda43d1635df2b9ceee69b2 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 18:42:46 +0100 Subject: [PATCH 2/9] ... --- diff/ascii.go | 60 +++++++++++++++++++++++++++++++++----------------- diff/deltas.go | 8 +++---- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/diff/ascii.go b/diff/ascii.go index efe45c6..a357872 100644 --- a/diff/ascii.go +++ b/diff/ascii.go @@ -56,25 +56,29 @@ func (f *AsciiFormatter) Format(diff Diff) (result string, err error) { } func (f *AsciiFormatter) formatObject(left map[string]interface{}, df Diff) { - f.addLineWith(AsciiSame, "{") + f.addLineWith("{") f.push("ROOT", len(left), false) f.processObject(left, df.Deltas()) f.pop() - f.addLineWith(AsciiSame, "}") + f.addLineWith("}") } func (f *AsciiFormatter) formatArray(left []interface{}, df Diff) { - f.addLineWith(AsciiSame, "[") + f.addLineWith("[") f.push("ROOT", len(left), true) f.processArray(left, df.Deltas()) f.pop() - f.addLineWith(AsciiSame, "]") + f.addLineWith("]") } func (f *AsciiFormatter) processArray(array []interface{}, deltas []Delta) error { patchedIndex := 0 + for index, value := range array { - f.processItem(value, deltas, Index(index)) + if err := f.processItem(value, deltas, Index(index)); err != nil { + return err + } + patchedIndex++ } @@ -87,7 +91,8 @@ func (f *AsciiFormatter) processArray(array []interface{}, deltas []Delta) error if int(d.Position.(Index)) < len(array) { continue } - f.printRecursive(d.Position.String(), d.Value, AsciiAdded) + + f.printRecursive(d.String(), d.Value, AsciiAdded) } } @@ -98,15 +103,17 @@ func (f *AsciiFormatter) processObject(object map[string]interface{}, deltas []D names := sortedKeys(object) for _, name := range names { value := object[name] - f.processItem(value, deltas, Name(name)) + if err := f.processItem(value, deltas, Name(name)); err != nil { + return err + } } // Added for _, delta := range deltas { - switch delta.(type) { + switch dt := delta.(type) { case *Added: - d := delta.(*Added) - f.printRecursive(d.Position.String(), d.Value, AsciiAdded) + d := dt + f.printRecursive(d.String(), d.Value, AsciiAdded) } } @@ -116,17 +123,20 @@ func (f *AsciiFormatter) processObject(object map[string]interface{}, deltas []D func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position Position) error { matchedDeltas := f.searchDeltas(deltas, position) positionStr := position.String() + if len(matchedDeltas) > 0 { for _, matchedDelta := range matchedDeltas { switch matchedDelta.(type) { case *Object: d := matchedDelta.(*Object) + switch value.(type) { case map[string]interface{}: // ok default: return errors.New("Type mismatch") } + o := value.(map[string]interface{}) f.newLine(AsciiSame) @@ -143,12 +153,14 @@ func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position case *Array: d := matchedDelta.(*Array) + switch value.(type) { case []interface{}: // ok default: return errors.New("Type mismatch") } + a := value.([]interface{}) f.newLine(AsciiSame) @@ -166,6 +178,7 @@ func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position case *Added: d := matchedDelta.(*Added) f.printRecursive(positionStr, d.Value, AsciiAdded) + f.size[len(f.size)-1]++ case *Modified: @@ -197,23 +210,25 @@ func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position return nil } -func (f *AsciiFormatter) searchDeltas(deltas []Delta, postion Position) (results []Delta) { +func (f *AsciiFormatter) searchDeltas(deltas []Delta, position Position) (results []Delta) { results = make([]Delta, 0) + for _, delta := range deltas { - switch delta.(type) { + switch dt := delta.(type) { case PostDelta: - if delta.(PostDelta).PostPosition() == postion { + if dt.PostPosition() == position { results = append(results, delta) } case PreDelta: - if delta.(PreDelta).PrePosition() == postion { + if dt.PrePosition() == position { results = append(results, delta) } default: panic("heh") } } - return + + return results } const ( @@ -239,9 +254,9 @@ func (f *AsciiFormatter) pop() { f.inArray = f.inArray[0 : len(f.inArray)-1] } -func (f *AsciiFormatter) addLineWith(marker string, value string) { +func (f *AsciiFormatter) addLineWith(value string) { f.line = &AsciiLine{ - marker: marker, + marker: AsciiSame, indent: len(f.path), buffer: bytes.NewBufferString(value), } @@ -263,9 +278,11 @@ func (f *AsciiFormatter) closeLine() { } f.buffer.WriteString(f.line.marker) + for n := 0; n < f.line.indent; n++ { f.buffer.WriteString(" ") } + f.buffer.Write(f.line.buffer.Bytes()) if f.config.Coloring && ok { @@ -310,14 +327,14 @@ func (f *AsciiFormatter) print(a string) { } func (f *AsciiFormatter) printRecursive(name string, value interface{}, marker string) { - switch value.(type) { + switch value := value.(type) { case map[string]interface{}: f.newLine(marker) f.printKey(name) f.print("{") f.closeLine() - m := value.(map[string]interface{}) + m := value size := len(m) f.push(name, size, false) @@ -325,6 +342,7 @@ func (f *AsciiFormatter) printRecursive(name string, value interface{}, marker s for _, key := range keys { f.printRecursive(key, m[key], marker) } + f.pop() f.newLine(marker) @@ -338,12 +356,14 @@ func (f *AsciiFormatter) printRecursive(name string, value interface{}, marker s f.print("[") f.closeLine() - s := value.([]interface{}) + s := value size := len(s) f.push("", size, true) + for _, item := range s { f.printRecursive("", item, marker) } + f.pop() f.newLine(marker) diff --git a/diff/deltas.go b/diff/deltas.go index 2e57de1..7097e71 100644 --- a/diff/deltas.go +++ b/diff/deltas.go @@ -120,22 +120,22 @@ type PostDelta interface { type postDelta struct{ Position } func (i postDelta) PostPosition() Position { - return Position(i.Position) + return i.Position } type postDeltas []PostDelta -// for sorting. +// Len returns the number of elements in the postDeltas slice. It is used for implementing the sort.Interface. func (s postDeltas) Len() int { return len(s) } -// for sorting. +// Swap swaps the elements at indices i and j in the postDeltas slice. It is used to implement the sort.Interface. func (s postDeltas) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -// for sorting. +// Less compares the PostPosition of elements at indices i and j and returns true if the i-th element is less than the j-th. func (s postDeltas) Less(i, j int) bool { return s[i].PostPosition().CompareTo(s[j].PostPosition()) } From 24fcde8779566b47a540bdb47d160b5d59e2fa3c Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 18:51:13 +0100 Subject: [PATCH 3/9] ... --- .golangci.yml | 1 + diff/ascii.go | 5 +++-- diff/deltas.go | 38 +++++++++++++++++++++++++++----------- diff/diff.go | 4 ++-- equal_test.go | 3 ++- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a399ba1..bc03826 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -45,6 +45,7 @@ linters: generated: lax rules: - linters: + - tparallel - gosec - dupl - funlen diff --git a/diff/ascii.go b/diff/ascii.go index a357872..b6fb275 100644 --- a/diff/ascii.go +++ b/diff/ascii.go @@ -237,7 +237,8 @@ const ( AsciiDeleted = "-" ) -var AsciiStyles = map[string]string{ +// ACSIIStyles is a map defining ANSI color styles for different ASCII markers used in formatting output. +var ACSIIStyles = map[string]string{ AsciiAdded: "30;42", AsciiDeleted: "30;41", } @@ -272,7 +273,7 @@ func (f *AsciiFormatter) newLine(marker string) { } func (f *AsciiFormatter) closeLine() { - style, ok := AsciiStyles[f.line.marker] + style, ok := ACSIIStyles[f.line.marker] if f.config.Coloring && ok { f.buffer.WriteString("\x1b[" + style + "m") } diff --git a/diff/deltas.go b/diff/deltas.go index 7097e71..361d6c4 100644 --- a/diff/deltas.go +++ b/diff/deltas.go @@ -55,21 +55,25 @@ type Position interface { // A Name is a Postition with a string, which means the delta is in an object. type Name string +// String returns the string representation of the Name. func (n Name) String() (name string) { return string(n) } +// CompareTo returns true if the Name is lexicographically less than the given Position, which must be of type Name. 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. +// Index is a Position with an int value, which means the Delta is in an Array. type Index int +// String converts the Index value to its string representation and returns it. func (i Index) String() (name string) { return strconv.Itoa(int(i)) } +// CompareTo compares the current Index with another Position and returns true if the current Index is smaller. func (i Index) CompareTo(another Position) bool { return i < another.(Index) } @@ -87,28 +91,28 @@ type PreDelta interface { type preDelta struct{ Position } func (i preDelta) PrePosition() Position { - return Position(i.Position) + return i.Position } type preDeltas []PreDelta -// for sorting. +// Len returns the number of elements in the preDeltas collection. func (s preDeltas) Len() int { return len(s) } -// for sorting. +// Swap exchanges the elements at indices i and j in the preDeltas collection. func (s preDeltas) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -// for sorting. +// Less reports whether the element with index i should sort before the element with index j in the preDeltas collection. 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. +// PostDelta represents an interface for deltas applied post object modification. +// It requires methods to retrieve the position and apply the delta to an object. type PostDelta interface { // PostPosition returns the Position. PostPosition() Position @@ -157,6 +161,7 @@ func NewObject(position Position, deltas []Delta) *Object { return &d } +// PostApply processes the given object by applying deltas at positions determined by the object's type (map or slice). func (d *Object) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -195,6 +200,7 @@ func NewArray(position Position, deltas []Delta) *Array { return &d } +// PostApply applies the stored deltas to the provided object based on their positions, modifying and returning the object. func (d *Array) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -232,6 +238,7 @@ func NewAdded(position Position, value interface{}) *Added { return &d } +// PostApply applies the added value to the given object at the position specified by the PostPosition method. func (d *Added) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -281,6 +288,7 @@ func NewModified(position Position, oldValue, newValue interface{}) *Modified { return &d } +// PostApply updates a map or slice at a specific position with a new value and returns the modified object. func (d *Modified) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -322,7 +330,7 @@ type TextDiff struct { Diff []dmp.Patch } -// NewTextDiff returns. +// NewTextDiff creates a new TextDiff instance with the provided position, diff, oldValue, and newValue. func NewTextDiff(position Position, diff []dmp.Patch, oldValue, newValue interface{}) *TextDiff { d := TextDiff{ Modified: *NewModified(position, oldValue, newValue), @@ -332,6 +340,7 @@ func NewTextDiff(position Position, diff []dmp.Patch, oldValue, newValue interfa return &d } +// PostApply updates the provided object with the changes specified in the TextDiff and returns the modified object. func (d *TextDiff) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -373,13 +382,15 @@ func (d *TextDiff) patch() error { return nil } +// DiffString returns the textual representation of the diff stored in the TextDiff instance. func (d *TextDiff) DiffString() string { - dmp := dmp.New() + dm := dmp.New() - return dmp.PatchToText(d.Diff) + return dm.PatchToText(d.Diff) } -// A Delted represents deleted field or index of an Object or an Array. +// Deleted represents a change where an element is removed from a map or slice at a specific position. +// It embeds preDelta to store positional metadata and includes the Value field to reference the deleted element. type Deleted struct { preDelta @@ -397,6 +408,7 @@ func NewDeleted(position Position, value interface{}) *Deleted { return &d } +// PreApply removes an element from a map or slice based on the position specified in the Deleted instance. func (d *Deleted) PreApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -411,6 +423,7 @@ func (d *Deleted) PreApply(object interface{}) interface{} { return object } +// Similarity calculates and returns the similarity for the Deleted delta type as a floating-point value. func (d Deleted) Similarity() (similarity float64) { return 0 } @@ -430,6 +443,7 @@ type Moved struct { Delta interface{} } +// NewMoved creates and returns a new Moved instance representing a field that has been relocated with old and new positions. func NewMoved(oldPosition Position, newPosition Position, value interface{}, delta Delta) *Moved { d := Moved{ preDelta: preDelta{oldPosition}, @@ -442,6 +456,7 @@ func NewMoved(oldPosition Position, newPosition Position, value interface{}, del return &d } +// PreApply modifies the given object by removing the element at the pre-move index and storing its value in the Moved instance. func (d *Moved) PreApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: @@ -456,6 +471,7 @@ func (d *Moved) PreApply(object interface{}) interface{} { return object } +// PostApply applies the stored delta after a move operation and adjusts the position of a value in the object. func (d *Moved) PostApply(object interface{}) interface{} { switch object.(type) { case map[string]interface{}: diff --git a/diff/diff.go b/diff/diff.go index aa30da7..641cabd 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -294,9 +294,9 @@ func (differ *Differ) compareValues( patchs := textDiff.PatchMake(reflect.ValueOf(left).String(), reflect.ValueOf(right).String()) return false, NewTextDiff(position, patchs, left, right) - } else { - return false, NewModified(position, left, right) } + + return false, NewModified(position, left, right) } } diff --git a/equal_test.go b/equal_test.go index d8707c4..f86da9e 100644 --- a/equal_test.go +++ b/equal_test.go @@ -100,7 +100,6 @@ func run( equal func(t assertjson.TestingT, expected, actual []byte, msgAndArgs ...interface{}) bool, ) { t.Helper() - t.Parallel() tt := testingT(func(format string, args ...interface{}) {}) @@ -114,6 +113,8 @@ func run( } func TestComparer_Equal_EmptyIgnoreDiff(t *testing.T) { + t.Parallel() + c := assertjson.Comparer{} run(t, []testcase{ From 87c1aa27b408ab5762d1d259417e32551107281f Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 18:54:24 +0100 Subject: [PATCH 4/9] ... --- compare.go | 2 +- diff/ascii.go | 104 +++++++++++++++++++++++++++----------------------- diff/diff.go | 4 +- equal.go | 2 +- 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/compare.go b/compare.go index 6aa08d4..0b45f28 100644 --- a/compare.go +++ b/compare.go @@ -181,7 +181,7 @@ func (c Comparer) fail(expected, actual []byte, ignoreAdded bool) error { return nil } - diffText, err := d2.NewAsciiFormatter(expDecoded, c.FormatterConfig).Format(diffValue) + diffText, err := d2.NewASCIIFormatter(expDecoded, c.FormatterConfig).Format(diffValue) if err != nil { return fmt.Errorf("failed to format diff:\n%wv", err) } diff --git a/diff/ascii.go b/diff/ascii.go index b6fb275..40e99dd 100644 --- a/diff/ascii.go +++ b/diff/ascii.go @@ -7,37 +7,42 @@ import ( "fmt" ) -func NewAsciiFormatter(left interface{}, config AsciiFormatterConfig) *AsciiFormatter { - return &AsciiFormatter{ +// NewASCIIFormatter creates a new ASCIIFormatter instance with the specified left data and configuration settings. +func NewASCIIFormatter(left interface{}, config ASCIIFormatterConfig) *ASCIIFormatter { + return &ASCIIFormatter{ left: left, config: config, } } -type AsciiFormatter struct { +// ASCIIFormatter is used to generate ASCII representations of differences between JSON-like data structures. +type ASCIIFormatter struct { left interface{} - config AsciiFormatterConfig + config ASCIIFormatterConfig buffer *bytes.Buffer path []string size []int inArray []bool - line *AsciiLine + line *ASCIILine } -type AsciiFormatterConfig struct { +// ASCIIFormatterConfig specifies configuration options for formatting ASCII representations of data structures. +// ShowArrayIndex determines if array indices should be displayed in the formatted output. +// Coloring enables or disables colored output in the formatted result. +type ASCIIFormatterConfig struct { ShowArrayIndex bool Coloring bool } -var AsciiFormatterDefaultConfig = AsciiFormatterConfig{} - -type AsciiLine struct { +// ASCIILine represents a line in an ASCII-formatted output with a marker, indentation, and a buffer containing content. +type ASCIILine struct { marker string indent int buffer *bytes.Buffer } -func (f *AsciiFormatter) Format(diff Diff) (result string, err error) { +// Format formats the differences between two JSON objects into an ASCII representation using the provided Diff. +func (f *ASCIIFormatter) Format(diff Diff) (result string, err error) { f.buffer = bytes.NewBuffer([]byte{}) f.path = []string{} f.size = []int{} @@ -55,7 +60,7 @@ func (f *AsciiFormatter) Format(diff Diff) (result string, err error) { return f.buffer.String(), nil } -func (f *AsciiFormatter) formatObject(left map[string]interface{}, df Diff) { +func (f *ASCIIFormatter) formatObject(left map[string]interface{}, df Diff) { f.addLineWith("{") f.push("ROOT", len(left), false) f.processObject(left, df.Deltas()) @@ -63,7 +68,7 @@ func (f *AsciiFormatter) formatObject(left map[string]interface{}, df Diff) { f.addLineWith("}") } -func (f *AsciiFormatter) formatArray(left []interface{}, df Diff) { +func (f *ASCIIFormatter) formatArray(left []interface{}, df Diff) { f.addLineWith("[") f.push("ROOT", len(left), true) f.processArray(left, df.Deltas()) @@ -71,7 +76,7 @@ func (f *AsciiFormatter) formatArray(left []interface{}, df Diff) { f.addLineWith("]") } -func (f *AsciiFormatter) processArray(array []interface{}, deltas []Delta) error { +func (f *ASCIIFormatter) processArray(array []interface{}, deltas []Delta) error { patchedIndex := 0 for index, value := range array { @@ -92,14 +97,14 @@ func (f *AsciiFormatter) processArray(array []interface{}, deltas []Delta) error continue } - f.printRecursive(d.String(), d.Value, AsciiAdded) + f.printRecursive(d.String(), d.Value, ASCIIAdded) } } return nil } -func (f *AsciiFormatter) processObject(object map[string]interface{}, deltas []Delta) error { +func (f *ASCIIFormatter) processObject(object map[string]interface{}, deltas []Delta) error { names := sortedKeys(object) for _, name := range names { value := object[name] @@ -113,14 +118,14 @@ func (f *AsciiFormatter) processObject(object map[string]interface{}, deltas []D switch dt := delta.(type) { case *Added: d := dt - f.printRecursive(d.String(), d.Value, AsciiAdded) + f.printRecursive(d.String(), d.Value, ASCIIAdded) } } return nil } -func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position Position) error { +func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position Position) error { matchedDeltas := f.searchDeltas(deltas, position) positionStr := position.String() @@ -139,14 +144,14 @@ func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position o := value.(map[string]interface{}) - f.newLine(AsciiSame) + f.newLine(ASCIISame) f.printKey(positionStr) f.print("{") f.closeLine() f.push(positionStr, len(o), false) f.processObject(o, d.Deltas) f.pop() - f.newLine(AsciiSame) + f.newLine(ASCIISame) f.print("}") f.printComma() f.closeLine() @@ -163,54 +168,54 @@ func (f *AsciiFormatter) processItem(value interface{}, deltas []Delta, position a := value.([]interface{}) - f.newLine(AsciiSame) + f.newLine(ASCIISame) f.printKey(positionStr) f.print("[") f.closeLine() f.push(positionStr, len(a), true) f.processArray(a, d.Deltas) f.pop() - f.newLine(AsciiSame) + f.newLine(ASCIISame) f.print("]") f.printComma() f.closeLine() case *Added: d := matchedDelta.(*Added) - f.printRecursive(positionStr, d.Value, AsciiAdded) + f.printRecursive(positionStr, d.Value, ASCIIAdded) f.size[len(f.size)-1]++ case *Modified: d := matchedDelta.(*Modified) savedSize := f.size[len(f.size)-1] - f.printRecursive(positionStr, d.OldValue, AsciiDeleted) + f.printRecursive(positionStr, d.OldValue, ASCIIDeleted) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, AsciiAdded) + f.printRecursive(positionStr, d.NewValue, ASCIIAdded) case *TextDiff: savedSize := f.size[len(f.size)-1] d := matchedDelta.(*TextDiff) - f.printRecursive(positionStr, d.OldValue, AsciiDeleted) + f.printRecursive(positionStr, d.OldValue, ASCIIDeleted) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, AsciiAdded) + f.printRecursive(positionStr, d.NewValue, ASCIIAdded) case *Deleted: d := matchedDelta.(*Deleted) - f.printRecursive(positionStr, d.Value, AsciiDeleted) + f.printRecursive(positionStr, d.Value, ASCIIDeleted) default: return errors.New("Unknown Delta type detected") } } } else { - f.printRecursive(positionStr, value, AsciiSame) + f.printRecursive(positionStr, value, ASCIISame) } return nil } -func (f *AsciiFormatter) searchDeltas(deltas []Delta, position Position) (results []Delta) { +func (f *ASCIIFormatter) searchDeltas(deltas []Delta, position Position) (results []Delta) { results = make([]Delta, 0) for _, delta := range deltas { @@ -232,47 +237,52 @@ func (f *AsciiFormatter) searchDeltas(deltas []Delta, position Position) (result } const ( - AsciiSame = " " - AsciiAdded = "+" - AsciiDeleted = "-" + // ASCIISame represents the ASCII string " " used to indicate unchanged or identical elements in a comparison. + ASCIISame = " " + + // ASCIIAdded represents the ASCII string "+" used to indicate newly added items or elements in comparisons. + ASCIIAdded = "+" + + // ASCIIDeleted represents the ASCII string "-" used to indicate deleted items or removed elements. + ASCIIDeleted = "-" ) // ACSIIStyles is a map defining ANSI color styles for different ASCII markers used in formatting output. var ACSIIStyles = map[string]string{ - AsciiAdded: "30;42", - AsciiDeleted: "30;41", + ASCIIAdded: "30;42", + ASCIIDeleted: "30;41", } -func (f *AsciiFormatter) push(name string, size int, array bool) { +func (f *ASCIIFormatter) push(name string, size int, array bool) { f.path = append(f.path, name) f.size = append(f.size, size) f.inArray = append(f.inArray, array) } -func (f *AsciiFormatter) pop() { +func (f *ASCIIFormatter) pop() { f.path = f.path[0 : len(f.path)-1] f.size = f.size[0 : len(f.size)-1] f.inArray = f.inArray[0 : len(f.inArray)-1] } -func (f *AsciiFormatter) addLineWith(value string) { - f.line = &AsciiLine{ - marker: AsciiSame, +func (f *ASCIIFormatter) addLineWith(value string) { + f.line = &ASCIILine{ + marker: ASCIISame, indent: len(f.path), buffer: bytes.NewBufferString(value), } f.closeLine() } -func (f *AsciiFormatter) newLine(marker string) { - f.line = &AsciiLine{ +func (f *ASCIIFormatter) newLine(marker string) { + f.line = &ASCIILine{ marker: marker, indent: len(f.path), buffer: bytes.NewBuffer([]byte{}), } } -func (f *AsciiFormatter) closeLine() { +func (f *ASCIIFormatter) closeLine() { style, ok := ACSIIStyles[f.line.marker] if f.config.Coloring && ok { f.buffer.WriteString("\x1b[" + style + "m") @@ -293,7 +303,7 @@ func (f *AsciiFormatter) closeLine() { f.buffer.WriteRune('\n') } -func (f *AsciiFormatter) printKey(name string) { +func (f *ASCIIFormatter) printKey(name string) { if !f.inArray[len(f.inArray)-1] { fmt.Fprintf(f.line.buffer, `"%s": `, name) } else if f.config.ShowArrayIndex { @@ -301,14 +311,14 @@ func (f *AsciiFormatter) printKey(name string) { } } -func (f *AsciiFormatter) printComma() { +func (f *ASCIIFormatter) printComma() { f.size[len(f.size)-1]-- if f.size[len(f.size)-1] > 0 { f.line.buffer.WriteRune(',') } } -func (f *AsciiFormatter) printValue(value interface{}) { +func (f *ASCIIFormatter) printValue(value interface{}) { switch v := value.(type) { case uint64: fmt.Fprint(f.line.buffer, v) @@ -323,11 +333,11 @@ func (f *AsciiFormatter) printValue(value interface{}) { } } -func (f *AsciiFormatter) print(a string) { +func (f *ASCIIFormatter) print(a string) { f.line.buffer.WriteString(a) } -func (f *AsciiFormatter) printRecursive(name string, value interface{}, marker string) { +func (f *ASCIIFormatter) printRecursive(name string, value interface{}, marker string) { switch value := value.(type) { case map[string]interface{}: f.newLine(marker) diff --git a/diff/diff.go b/diff/diff.go index 641cabd..74d2ab2 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -367,7 +367,7 @@ func (differ *Differ) maximizeSimilarities(left []maybe, right []maybe) (resultD prevY := dpTable[x][y+1] score := deltaTable[x][y].Similarity() + dpTable[x+1][y+1] - dpTable[x][y] = max(prevX, prevY, score) + dpTable[x][y] = maxFloat(prevX, prevY, score) } } @@ -460,7 +460,7 @@ func sortedKeys(m map[string]interface{}) (keys []string) { return keys } -func max(first float64, rest ...float64) (max float64) { +func maxFloat(first float64, rest ...float64) (max float64) { max = first for _, value := range rest { if max < value { diff --git a/equal.go b/equal.go index 96acdea..ebaa9b8 100644 --- a/equal.go +++ b/equal.go @@ -18,7 +18,7 @@ type Comparer struct { Vars *shared.Vars // FormatterConfig controls diff formatter configuration. - FormatterConfig d2.AsciiFormatterConfig + FormatterConfig d2.ASCIIFormatterConfig // KeepFullDiff shows full diff in error message. KeepFullDiff bool From 0def3983a051eaf456c304feef571bbbd440f2bb Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 18:59:28 +0100 Subject: [PATCH 5/9] ... --- .golangci.yml | 1 + compare.go | 26 +++++++++++++------------- diff/deltas.go | 16 ++++------------ diff/diff.go | 16 +++++++++------- 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index bc03826..d28a15f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,6 +5,7 @@ run: linters: default: all disable: + - nilnil - noinlineerr - wsl_v5 - funcorder diff --git a/compare.go b/compare.go index 0b45f28..71323db 100644 --- a/compare.go +++ b/compare.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/bool64/shared" - d2 "github.com/swaggest/assertjson/diff" + "github.com/swaggest/assertjson/diff" ) func (c Comparer) varCollected(s string, v interface{}) bool { @@ -30,12 +30,12 @@ func (c Comparer) varCollected(s string, v interface{}) bool { return false } -func (c Comparer) filterDeltas(deltas []d2.Delta, ignoreAdded bool) []d2.Delta { - result := make([]d2.Delta, 0, len(deltas)) +func (c Comparer) filterDeltas(deltas []diff.Delta, ignoreAdded bool) []diff.Delta { + result := make([]diff.Delta, 0, len(deltas)) for _, delta := range deltas { switch v := delta.(type) { - case *d2.Modified: + case *diff.Modified: if c.IgnoreDiff == "" && c.Vars == nil { break } @@ -49,14 +49,14 @@ func (c Comparer) filterDeltas(deltas []d2.Delta, ignoreAdded bool) []d2.Delta { continue } } - case *d2.Object: + case *diff.Object: v.Deltas = c.filterDeltas(v.Deltas, ignoreAdded) if len(v.Deltas) == 0 { continue } delta = v - case *d2.Array: + case *diff.Array: v.Deltas = c.filterDeltas(v.Deltas, ignoreAdded) if len(v.Deltas) == 0 { continue @@ -64,7 +64,7 @@ func (c Comparer) filterDeltas(deltas []d2.Delta, ignoreAdded bool) []d2.Delta { delta = v - case *d2.Added: + case *diff.Added: if ignoreAdded { continue } @@ -77,10 +77,10 @@ func (c Comparer) filterDeltas(deltas []d2.Delta, ignoreAdded bool) []d2.Delta { } type df struct { - deltas []d2.Delta + deltas []diff.Delta } -func (df *df) Deltas() []d2.Delta { +func (df *df) Deltas() []diff.Delta { return df.deltas } @@ -103,18 +103,18 @@ func (c Comparer) filterExpected(expected []byte) ([]byte, error) { return expected, nil } -func (c Comparer) compare(expDecoded, actDecoded interface{}) (d2.Diff, error) { +func (c Comparer) compare(expDecoded, actDecoded interface{}) (diff.Diff, error) { switch v := expDecoded.(type) { case []interface{}: if actArray, ok := actDecoded.([]interface{}); ok { - return d2.New().CompareArrays(v, actArray), nil + return diff.New().CompareArrays(v, actArray), nil } return nil, errors.New("types mismatch, array expected") case map[string]interface{}: if actObject, ok := actDecoded.(map[string]interface{}); ok { - return d2.New().CompareObjects(v, actObject), nil + return diff.New().CompareObjects(v, actObject), nil } return nil, errors.New("types mismatch, object expected") @@ -181,7 +181,7 @@ func (c Comparer) fail(expected, actual []byte, ignoreAdded bool) error { return nil } - diffText, err := d2.NewASCIIFormatter(expDecoded, c.FormatterConfig).Format(diffValue) + diffText, err := diff.NewASCIIFormatter(expDecoded, c.FormatterConfig).Format(diffValue) if err != nil { return fmt.Errorf("failed to format diff:\n%wv", err) } diff --git a/diff/deltas.go b/diff/deltas.go index 361d6c4..52c9fb3 100644 --- a/diff/deltas.go +++ b/diff/deltas.go @@ -1,7 +1,6 @@ package diff import ( - "errors" "reflect" "strconv" @@ -292,7 +291,6 @@ func NewModified(position Position, oldValue, newValue interface{}) *Modified { 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 @@ -346,16 +344,13 @@ func (d *TextDiff) PostApply(object interface{}) interface{} { 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 } @@ -363,9 +358,9 @@ func (d *TextDiff) PostApply(object interface{}) interface{} { return object } -func (d *TextDiff) patch() error { +func (d *TextDiff) patch() { if d.OldValue == nil { - return errors.New("Old Value is not set") + panic("old Value is not set") } patcher := dmp.New() @@ -373,13 +368,11 @@ func (d *TextDiff) patch() error { patched, successes := patcher.PatchApply(d.Diff, d.OldValue.(string)) for _, success := range successes { if !success { - return errors.New("Failed to apply a patch") + panic("failed to apply a patch") } } d.NewValue = patched - - return nil } // DiffString returns the textual representation of the diff stored in the TextDiff instance. @@ -409,10 +402,9 @@ func NewDeleted(position Position, value interface{}) *Deleted { } // PreApply removes an element from a map or slice based on the position specified in the Deleted instance. -func (d *Deleted) PreApply(object interface{}) interface{} { +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)) diff --git a/diff/diff.go b/diff/diff.go index 74d2ab2..420a0f5 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -418,12 +418,14 @@ func (differ *Differ) maximizeSimilarities(left []maybe, right []maybe) (resultD return resultDeltas, freeLeft, freeRight } -func deltasSimilarity(deltas []Delta) (similarity float64) { +func deltasSimilarity(deltas []Delta) float64 { + similarity := float64(0) + for _, delta := range deltas { similarity += delta.Similarity() } - similarity = similarity / float64(len(deltas)) + similarity /= float64(len(deltas)) return similarity } @@ -460,13 +462,13 @@ func sortedKeys(m map[string]interface{}) (keys []string) { return keys } -func maxFloat(first float64, rest ...float64) (max float64) { - max = first +func maxFloat(first float64, rest ...float64) float64 { + m := first for _, value := range rest { - if max < value { - max = value + if m < value { + m = value } } - return max + return m } From ba514f2a0bb4a820c28c08189bd1f46af640f433 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 19:03:04 +0100 Subject: [PATCH 6/9] ... --- diff/diff.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/diff/diff.go b/diff/diff.go index 420a0f5..cae71ac 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -307,9 +307,8 @@ func applyDeltas(deltas []Delta, object interface{}) interface{} { preDeltas := make(preDeltas, 0) for _, delta := range deltas { - switch delta := delta.(type) { - case PreDelta: - preDeltas = append(preDeltas, delta) + if dt, ok := delta.(PreDelta); ok { + preDeltas = append(preDeltas, dt) } } @@ -322,9 +321,8 @@ func applyDeltas(deltas []Delta, object interface{}) interface{} { postDeltas := make(postDeltas, 0, len(deltas)-len(preDeltas)) for _, delta := range deltas { - switch delta := delta.(type) { - case PostDelta: - postDeltas = append(postDeltas, delta) + if dt, ok := delta.(PostDelta); ok { + postDeltas = append(postDeltas, dt) } } @@ -394,13 +392,14 @@ func (differ *Differ) maximizeSimilarities(left []maybe, right []maybe) (resultD xValidLength := len(left) - maxInvalidLength + y yValidLength := len(right) - maxInvalidLength + x - if x+1 < xValidLength && current == nextX { + switch { + case x+1 < xValidLength && current == nextX: freeLeft = append(freeLeft, left[x]) x++ - } else if y+1 < yValidLength && current == nextY { + case y+1 < yValidLength && current == nextY: freeRight = append(freeRight, right[y]) y++ - } else { + default: resultDeltas = append(resultDeltas, deltaTable[x][y]) x++ y++ From 32dc5d19d5b7d8f61a0959f6a2ab85c6ee7220e6 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 19:11:49 +0100 Subject: [PATCH 7/9] ... --- diff/ascii.go | 15 +++------------ diff/deltas.go | 50 ++++++++++++++++++++++---------------------------- diff/diff.go | 9 +++------ 3 files changed, 28 insertions(+), 46 deletions(-) diff --git a/diff/ascii.go b/diff/ascii.go index 40e99dd..c0dd52e 100644 --- a/diff/ascii.go +++ b/diff/ascii.go @@ -115,8 +115,7 @@ func (f *ASCIIFormatter) processObject(object map[string]interface{}, deltas []D // Added for _, delta := range deltas { - switch dt := delta.(type) { - case *Added: + if dt, ok := delta.(*Added); ok { d := dt f.printRecursive(d.String(), d.Value, ASCIIAdded) } @@ -131,10 +130,8 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position if len(matchedDeltas) > 0 { for _, matchedDelta := range matchedDeltas { - switch matchedDelta.(type) { + switch d := matchedDelta.(type) { case *Object: - d := matchedDelta.(*Object) - switch value.(type) { case map[string]interface{}: // ok @@ -157,8 +154,6 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position f.closeLine() case *Array: - d := matchedDelta.(*Array) - switch value.(type) { case []interface{}: // ok @@ -181,13 +176,11 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position f.closeLine() case *Added: - d := matchedDelta.(*Added) f.printRecursive(positionStr, d.Value, ASCIIAdded) f.size[len(f.size)-1]++ case *Modified: - d := matchedDelta.(*Modified) savedSize := f.size[len(f.size)-1] f.printRecursive(positionStr, d.OldValue, ASCIIDeleted) f.size[len(f.size)-1] = savedSize @@ -195,17 +188,15 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position case *TextDiff: savedSize := f.size[len(f.size)-1] - d := matchedDelta.(*TextDiff) f.printRecursive(positionStr, d.OldValue, ASCIIDeleted) f.size[len(f.size)-1] = savedSize f.printRecursive(positionStr, d.NewValue, ASCIIAdded) case *Deleted: - d := matchedDelta.(*Deleted) f.printRecursive(positionStr, d.Value, ASCIIDeleted) default: - return errors.New("Unknown Delta type detected") + return errors.New("unknown Delta type detected") } } } else { diff --git a/diff/deltas.go b/diff/deltas.go index 52c9fb3..bb4bcf2 100644 --- a/diff/deltas.go +++ b/diff/deltas.go @@ -162,13 +162,11 @@ func NewObject(position Position, deltas []Delta) *Object { // PostApply processes the given object by applying deltas at positions determined by the object's type (map or slice). func (d *Object) PostApply(object interface{}) interface{} { - switch object.(type) { + switch o := 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]) } @@ -201,13 +199,11 @@ func NewArray(position Position, deltas []Delta) *Array { // PostApply applies the stored deltas to the provided object based on their positions, modifying and returning the object. func (d *Array) PostApply(object interface{}) interface{} { - switch object.(type) { + switch o := 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]) } @@ -239,21 +235,21 @@ func NewAdded(position Position, value interface{}) *Added { // PostApply applies the added value to the given object at the position specified by the PostPosition method. func (d *Added) PostApply(object interface{}) interface{} { - switch object.(type) { + switch o := 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 o } + + return append(o, d.Value) } return object @@ -289,11 +285,11 @@ func NewModified(position Position, oldValue, newValue interface{}) *Modified { // PostApply updates a map or slice at a specific position with a new value and returns the modified object. func (d *Modified) PostApply(object interface{}) interface{} { - switch object.(type) { + switch o := object.(type) { case map[string]interface{}: - object.(map[string]interface{})[string(d.PostPosition().(Name))] = d.NewValue + o[string(d.PostPosition().(Name))] = d.NewValue case []interface{}: - object.([]interface{})[int(d.PostPosition().(Index))] = d.NewValue + o[(d.PostPosition().(Index))] = d.NewValue } return object @@ -304,11 +300,11 @@ func (d *Modified) similarity() (similarity float64) { if reflect.TypeOf(d.OldValue) == reflect.TypeOf(d.NewValue) { similarity += 0.3 // types are same - switch d.OldValue.(type) { + switch t := d.OldValue.(type) { case string: - similarity += 0.4 * stringSimilarity(d.OldValue.(string), d.NewValue.(string)) + similarity += 0.4 * stringSimilarity(t, d.NewValue.(string)) case float64: - ratio := d.OldValue.(float64) / d.NewValue.(float64) + ratio := t / d.NewValue.(float64) if ratio > 1 { ratio = 1 / ratio } @@ -340,15 +336,13 @@ func NewTextDiff(position Position, diff []dmp.Patch, oldValue, newValue interfa // PostApply updates the provided object with the changes specified in the TextDiff and returns the modified object. func (d *TextDiff) PostApply(object interface{}) interface{} { - switch object.(type) { + switch o := object.(type) { case map[string]interface{}: - o := object.(map[string]interface{}) i := string(d.PostPosition().(Name)) d.OldValue = o[i] d.patch() o[i] = d.NewValue case []interface{}: - o := object.([]interface{}) i := d.PostPosition().(Index) d.OldValue = o[i] d.patch() @@ -403,13 +397,13 @@ func NewDeleted(position Position, value interface{}) *Deleted { // PreApply removes an element from a map or slice based on the position specified in the Deleted instance. func (d Deleted) PreApply(object interface{}) interface{} { - switch object.(type) { + switch o := object.(type) { case map[string]interface{}: 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 append(o[:i], o[i+1:]...) } return object @@ -450,14 +444,14 @@ func NewMoved(oldPosition Position, newPosition Position, value interface{}, del // PreApply modifies the given object by removing the element at the pre-move index and storing its value in the Moved instance. func (d *Moved) PreApply(object interface{}) interface{} { - switch object.(type) { + switch o := 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 append(o[:i], o[i+1:]...) } return object @@ -465,12 +459,12 @@ func (d *Moved) PreApply(object interface{}) interface{} { // PostApply applies the stored delta after a move operation and adjusts the position of a value in the object. func (d *Moved) PostApply(object interface{}) interface{} { - switch object.(type) { + switch o := 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 diff --git a/diff/diff.go b/diff/diff.go index cae71ac..b60f33c 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -260,17 +260,14 @@ func (differ *Differ) compareValues( return false, NewModified(position, left, right) } - switch left.(type) { + switch l := left.(type) { case map[string]interface{}: - l := left.(map[string]interface{}) - childDeltas := differ.compareMaps(l, right.(map[string]interface{})) if len(childDeltas) > 0 { return false, NewObject(position, childDeltas) } case []interface{}: - l := left.([]interface{}) childDeltas := differ.compareArrays(l, right.([]interface{})) if len(childDeltas) > 0 { @@ -291,9 +288,9 @@ func (differ *Differ) compareValues( reflect.ValueOf(right).Kind() == reflect.String && differ.textDiffMinimumLength <= len(reflect.ValueOf(left).String()) { textDiff := dmp.New() - patchs := textDiff.PatchMake(reflect.ValueOf(left).String(), reflect.ValueOf(right).String()) + patches := textDiff.PatchMake(reflect.ValueOf(left).String(), reflect.ValueOf(right).String()) - return false, NewTextDiff(position, patchs, left, right) + return false, NewTextDiff(position, patches, left, right) } return false, NewModified(position, left, right) From ec2b408616cf4b5f512324f082cf8c4bbb344d7f Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 19:20:09 +0100 Subject: [PATCH 8/9] ... --- .golangci.yml | 10 ++++++++++ cmd/jsoncompact/main.go | 2 +- diff/ascii.go | 32 +++++++++++++++++++++++--------- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d28a15f..186a57f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,6 +5,9 @@ run: linters: default: all disable: + - gocyclo + - errcheck + - err113 - nilnil - noinlineerr - wsl_v5 @@ -31,6 +34,11 @@ linters: - varnamelen - wrapcheck settings: + gocognit: + min-complexity: 50 + funlen: + statements: 65 + lines: 120 dupl: threshold: 100 errcheck: @@ -42,6 +50,8 @@ linters: locale: US unparam: check-exported: true + cyclop: + max-complexity: 25 exclusions: generated: lax rules: diff --git a/cmd/jsoncompact/main.go b/cmd/jsoncompact/main.go index 509f099..a006634 100644 --- a/cmd/jsoncompact/main.go +++ b/cmd/jsoncompact/main.go @@ -16,7 +16,7 @@ import ( ) // The function is a bit lengthy, but I'm not sure if it would be more approachable divided in several functions. -func main() { //nolint +func main() { var ( input, output string length int diff --git a/diff/ascii.go b/diff/ascii.go index c0dd52e..84b5284 100644 --- a/diff/ascii.go +++ b/diff/ascii.go @@ -63,7 +63,11 @@ func (f *ASCIIFormatter) Format(diff Diff) (result string, err error) { func (f *ASCIIFormatter) formatObject(left map[string]interface{}, df Diff) { f.addLineWith("{") f.push("ROOT", len(left), false) - f.processObject(left, df.Deltas()) + + if err := f.processObject(left, df.Deltas()); err != nil { + panic(err) + } + f.pop() f.addLineWith("}") } @@ -71,7 +75,11 @@ func (f *ASCIIFormatter) formatObject(left map[string]interface{}, df Diff) { func (f *ASCIIFormatter) formatArray(left []interface{}, df Diff) { f.addLineWith("[") f.push("ROOT", len(left), true) - f.processArray(left, df.Deltas()) + + if err := f.processArray(left, df.Deltas()); err != nil { + panic(err) + } + f.pop() f.addLineWith("]") } @@ -89,9 +97,7 @@ func (f *ASCIIFormatter) processArray(array []interface{}, deltas []Delta) error // additional Added for _, delta := range deltas { - switch delta.(type) { - case *Added: - d := delta.(*Added) + if d, ok := delta.(*Added); ok { // skip items already processed if int(d.Position.(Index)) < len(array) { continue @@ -136,7 +142,7 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position case map[string]interface{}: // ok default: - return errors.New("Type mismatch") + return errors.New("type mismatch") } o := value.(map[string]interface{}) @@ -146,7 +152,11 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position f.print("{") f.closeLine() f.push(positionStr, len(o), false) - f.processObject(o, d.Deltas) + + if err := f.processObject(o, d.Deltas); err != nil { + return err + } + f.pop() f.newLine(ASCIISame) f.print("}") @@ -158,7 +168,7 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position case []interface{}: // ok default: - return errors.New("Type mismatch") + return errors.New("type mismatch") } a := value.([]interface{}) @@ -168,7 +178,11 @@ func (f *ASCIIFormatter) processItem(value interface{}, deltas []Delta, position f.print("[") f.closeLine() f.push(positionStr, len(a), true) - f.processArray(a, d.Deltas) + + if err := f.processArray(a, d.Deltas); err != nil { + return err + } + f.pop() f.newLine(ASCIISame) f.print("]") From ac4ef7b9e3ec668bfeb081c899fc575ecdd1db3f Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Fri, 14 Nov 2025 19:26:38 +0100 Subject: [PATCH 9/9] ... --- cmd/jsoncompact/main.go | 2 +- equal.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/jsoncompact/main.go b/cmd/jsoncompact/main.go index a006634..13900ba 100644 --- a/cmd/jsoncompact/main.go +++ b/cmd/jsoncompact/main.go @@ -40,7 +40,7 @@ func main() { input = flag.Arg(0) if input == "" { - _, _ = fmt.Fprintln(flag.CommandLine.Output(), "Missing input path argument, use `-` for stdin.") //nolint + _, _ = fmt.Fprintln(flag.CommandLine.Output(), "Missing input path argument, use `-` for stdin.") flag.Usage() return diff --git a/equal.go b/equal.go index ebaa9b8..da91716 100644 --- a/equal.go +++ b/equal.go @@ -6,7 +6,7 @@ import ( "github.com/bool64/shared" "github.com/stretchr/testify/assert" - d2 "github.com/swaggest/assertjson/diff" + "github.com/swaggest/assertjson/diff" ) // Comparer compares JSON documents. @@ -18,7 +18,7 @@ type Comparer struct { Vars *shared.Vars // FormatterConfig controls diff formatter configuration. - FormatterConfig d2.ASCIIFormatterConfig + FormatterConfig diff.ASCIIFormatterConfig // KeepFullDiff shows full diff in error message. KeepFullDiff bool