Commit e269b3b2 by Torkel Ödegaard

Merge branch 'refactor-basic-diff' of https://github.com/walmartlabs/grafana…

Merge branch 'refactor-basic-diff' of https://github.com/walmartlabs/grafana into walmartlabs-refactor-basic-diff
parents 1499c2bf 1fd7b60e
...@@ -71,6 +71,8 @@ func NewBasicFormatter(left interface{}) *BasicFormatter { ...@@ -71,6 +71,8 @@ func NewBasicFormatter(left interface{}) *BasicFormatter {
} }
} }
// Format takes the diff of two JSON documents, and returns the difference
// between them summarized in an HTML document.
func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) { func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
// calling jsonDiff.Format(d) populates the JSON diff's "Lines" value, // calling jsonDiff.Format(d) populates the JSON diff's "Lines" value,
// which we use to compute the basic dif // which we use to compute the basic dif
...@@ -90,67 +92,45 @@ func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) { ...@@ -90,67 +92,45 @@ func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// Basic is V2 of the basic diff // Basic transforms a slice of JSONLines into a slice of BasicBlocks.
func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
// init an array you can append to for the basic "blocks" // init an array you can append to for the basic "blocks"
blocks := make([]*BasicBlock, 0) blocks := make([]*BasicBlock, 0)
// iterate through each line
for _, line := range lines { for _, line := range lines {
// TODO: this condition needs an explaination? what does it mean? if b.returnToTopLevelKey(line) {
if b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil {
if b.Block != nil { if b.Block != nil {
blocks = append(blocks, b.Block) blocks = append(blocks, b.Block)
} }
} }
// Record the last indent level at each pass in case we need to
// check for a change in depth inside the JSON data structures.
b.LastIndent = line.Indent b.LastIndent = line.Indent
// TODO: why special handling for indent 2?
if line.Indent == 1 { if line.Indent == 1 {
switch line.Change { if block, ok := b.handleTopLevelChange(line); ok {
case ChangeNil: blocks = append(blocks, block)
if line.Change == ChangeNil {
if line.Key != "" {
b.Block = &BasicBlock{
Title: line.Key,
Change: line.Change,
}
}
}
case ChangeAdded, ChangeDeleted:
blocks = append(blocks, &BasicBlock{
Title: line.Key,
Change: line.Change,
New: line.Val,
LineStart: line.LineNum,
})
case ChangeOld:
b.Block = &BasicBlock{
Title: line.Key,
Old: line.Val,
Change: line.Change,
LineStart: line.LineNum,
}
case ChangeNew:
b.Block.New = line.Val
b.Block.LineEnd = line.LineNum
// then write out the change
blocks = append(blocks, b.Block)
default:
// ok
} }
} }
// TODO: why special handling for indent > 2 ? // Here is where we handle changes for all types, appending each change
// Other Lines // to the current block based on the value.
//
// Values which only occupy a single line in JSON (like a string or
// int, for example) are treated as "Basic Changes" that we append to
// the current block as soon as they're detected.
//
// Values which occupy multiple lines (either slices or maps) are
// treated as "Basic Summaries". When we detect the "ChangeNil" type,
// we know we've encountered one of these types, so we record the
// starting position as well the type of the change, and stop
// performing comparisons until we find the end of that change. Upon
// finding the change, we append it to the current block, and begin
// performing comparisons again.
if line.Indent > 1 { if line.Indent > 1 {
// Ensure single line change // check to ensure a single line change
if line.Key != "" && line.Val != nil && !b.writing { if b.isSingleLineChange(line) {
switch line.Change { switch line.Change {
case ChangeAdded, ChangeDeleted: case ChangeAdded, ChangeDeleted:
...@@ -178,13 +158,31 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { ...@@ -178,13 +158,31 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
//ok //ok
} }
// otherwise, we're dealing with a change at a deeper level. We
// know there's a change somewhere in the JSON tree, but we
// don't know exactly where, so we go deeper.
} else { } else {
// if the change is anything but unchanged, continue processing
//
// we keep "narrowing" the key as we go deeper, in order to
// correctly report the key name for changes found within an
// object or array.
if line.Change != ChangeUnchanged { if line.Change != ChangeUnchanged {
if line.Key != "" { if line.Key != "" {
b.narrow = line.Key b.narrow = line.Key
b.keysIdent = line.Indent b.keysIdent = line.Indent
} }
// if the change isn't nil, and we're not already writing
// out a change, then we've found something.
//
// First, try to determine the title of the embedded JSON
// object. If it's an empty string, then we're in an object
// or array, so we default to using the "narrowed" key.
//
// We also start recording the basic summary, until we find
// the next `ChangeUnchanged`.
if line.Change != ChangeNil { if line.Change != ChangeNil {
if !b.writing { if !b.writing {
b.writing = true b.writing = true
...@@ -204,6 +202,17 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { ...@@ -204,6 +202,17 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
} }
} }
} }
// if we find a `ChangeUnchanged`, we do one of two things:
//
// - if we're recording a change already, then we know
// we've come to the end of that change block, so we write
// that change out be recording the line number of where
// that change ends, and append it to the current block's
// summary.
//
// - if we're not recording a change, then we do nothing,
// since the BasicDiff doesn't report on unchanged JSON
// values.
} else { } else {
if b.writing { if b.writing {
b.writing = false b.writing = false
...@@ -218,6 +227,81 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock { ...@@ -218,6 +227,81 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
return blocks return blocks
} }
// returnToTopLevelKey indicates that we've moved from a key at one level deep
// in the JSON document to a top level key.
//
// In order to produce distinct "blocks" when rendering the basic diff,
// we need a way to distinguish between differnt sections of data.
// To do this, we consider the value(s) of each top-level JSON key to
// represent a distinct block for Grafana's JSON data structure, so
// we perform this check to see if we've entered a new "block". If we
// have, we simply append the existing block to the array of blocks.
func (b *BasicDiff) returnToTopLevelKey(line *JSONLine) bool {
return b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil
}
// handleTopLevelChange handles a change on one of the top-level keys on a JSON
// document.
//
// If the line's indentation is at level 1, then we know it's a top
// level key in the JSON document. As mentioned earlier, we treat these
// specially as they indicate their values belong to distinct blocks.
//
// At level 1, we only record single-line changes, ie, the "added",
// "deleted", "old" or "new" cases, since we know those values aren't
// arrays or maps. We only handle these cases at level 2 or deeper,
// since for those we either output a "change" or "summary". This is
// done for formatting reasons only, so we have logical "blocks" to
// display.
func (b *BasicDiff) handleTopLevelChange(line *JSONLine) (*BasicBlock, bool) {
switch line.Change {
case ChangeNil:
if line.Change == ChangeNil {
if line.Key != "" {
b.Block = &BasicBlock{
Title: line.Key,
Change: line.Change,
}
}
}
case ChangeAdded, ChangeDeleted:
return &BasicBlock{
Title: line.Key,
Change: line.Change,
New: line.Val,
LineStart: line.LineNum,
}, true
case ChangeOld:
b.Block = &BasicBlock{
Title: line.Key,
Old: line.Val,
Change: line.Change,
LineStart: line.LineNum,
}
case ChangeNew:
b.Block.New = line.Val
b.Block.LineEnd = line.LineNum
// For every "old" change there is a corresponding "new", which
// is why we wait until we detect the "new" change before
// appending the change.
return b.Block, true
default:
// ok
}
return nil, false
}
// isSingleLineChange ensures we're iterating over a single line change (ie,
// either a single line or a old-new value pair was changed in the JSON file).
func (b *BasicDiff) isSingleLineChange(line *JSONLine) bool {
return line.Key != "" && line.Val != nil && !b.writing
}
// encStateMap is used in the template helper // encStateMap is used in the template helper
var ( var (
encStateMap = map[ChangeType]string{ encStateMap = map[ChangeType]string{
...@@ -240,7 +324,9 @@ var ( ...@@ -240,7 +324,9 @@ var (
) )
var ( var (
// tplBlock is the whole thing // tplBlock is the container for the basic diff. It iterates over each
// basic block, expanding each "change" and "summary" belonging to every
// block.
tplBlock = `{{ define "block" -}} tplBlock = `{{ define "block" -}}
{{ range . }} {{ range . }}
<div class="diff-group"> <div class="diff-group">
...@@ -286,7 +372,7 @@ var ( ...@@ -286,7 +372,7 @@ var (
{{ end }} {{ end }}
{{ end }}` {{ end }}`
// tplChange is the template for changes // tplChange is the template for basic changes.
tplChange = `{{ define "change" -}} tplChange = `{{ define "change" -}}
<li class="diff-change-group"> <li class="diff-change-group">
<span class="bullet-position-container"> <span class="bullet-position-container">
...@@ -313,7 +399,7 @@ var ( ...@@ -313,7 +399,7 @@ var (
</li> </li>
{{ end }}` {{ end }}`
// tplSummary is for basis summaries // tplSummary is for basic summaries.
tplSummary = `{{ define "summary" -}} tplSummary = `{{ define "summary" -}}
<div class="diff-group-name"> <div class="diff-group-name">
<i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i> <i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i>
......
package dashdiffs
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestDiff(t *testing.T) {
// Sample json docs for tests only
const (
leftJSON = `{
"key": "value",
"object": {
"key": "value",
"anotherObject": {
"same": "this field is the same in rightJSON",
"change": "this field should change in rightJSON",
"delete": "this field doesn't appear in rightJSON"
}
},
"array": [
"same",
"change",
"delete"
],
"embeddedArray": {
"array": [
"same",
"change",
"delete"
]
}
}`
rightJSON = `{
"key": "differentValue",
"object": {
"key": "value",
"newKey": "value",
"anotherObject": {
"same": "this field is the same in rightJSON",
"change": "this field should change in rightJSON",
"add": "this field is added"
}
},
"array": [
"same",
"changed!",
"add"
],
"embeddedArray": {
"array": [
"same",
"changed!",
"add"
]
}
}`
)
Convey("Testing dashboard diffs", t, func() {
// Compute the diff between the two JSON objects
baseData, err := simplejson.NewJson([]byte(leftJSON))
So(err, ShouldBeNil)
newData, err := simplejson.NewJson([]byte(rightJSON))
So(err, ShouldBeNil)
left, jsonDiff, err := getDiff(baseData, newData)
So(err, ShouldBeNil)
Convey("The JSONFormatter should produce the expected JSON tokens", func() {
f := NewJSONFormatter(left)
_, err := f.Format(jsonDiff)
So(err, ShouldBeNil)
// Total up the change types. If the number of different change
// types is correct, it means that the diff is producing correct
// output to the template rendered.
changeCounts := make(map[ChangeType]int)
for _, line := range f.Lines {
changeCounts[line.Change]++
}
// The expectedChangeCounts here were determined by manually
// looking at the JSON
expectedChangeCounts := map[ChangeType]int{
ChangeNil: 12,
ChangeAdded: 2,
ChangeDeleted: 1,
ChangeOld: 5,
ChangeNew: 5,
ChangeUnchanged: 5,
}
So(changeCounts, ShouldResemble, expectedChangeCounts)
})
Convey("The BasicFormatter should produce the expected BasicBlocks", func() {
f := NewBasicFormatter(left)
_, err := f.Format(jsonDiff)
So(err, ShouldBeNil)
bd := &BasicDiff{}
blocks := bd.Basic(f.jsonDiff.Lines)
changeCounts := make(map[ChangeType]int)
for _, block := range blocks {
for _, change := range block.Changes {
changeCounts[change.Change]++
}
for _, summary := range block.Summaries {
changeCounts[summary.Change]++
}
changeCounts[block.Change]++
}
expectedChangeCounts := map[ChangeType]int{
ChangeNil: 3,
ChangeAdded: 2,
ChangeDeleted: 1,
ChangeOld: 3,
}
So(changeCounts, ShouldResemble, expectedChangeCounts)
})
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment