mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			494 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			494 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| package dashdiffs
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"html/template"
 | |
| 	"sort"
 | |
| 
 | |
| 	diff "github.com/yudai/gojsondiff"
 | |
| )
 | |
| 
 | |
| type ChangeType int
 | |
| 
 | |
| const (
 | |
| 	ChangeNil ChangeType = iota
 | |
| 	ChangeAdded
 | |
| 	ChangeDeleted
 | |
| 	ChangeOld
 | |
| 	ChangeNew
 | |
| 	ChangeUnchanged
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	// changeTypeToSymbol is used for populating the terminating character in
 | |
| 	// the diff
 | |
| 	changeTypeToSymbol = map[ChangeType]string{
 | |
| 		ChangeNil:     "",
 | |
| 		ChangeAdded:   "+",
 | |
| 		ChangeDeleted: "-",
 | |
| 		ChangeOld:     "-",
 | |
| 		ChangeNew:     "+",
 | |
| 	}
 | |
| 
 | |
| 	// changeTypeToName is used for populating class names in the diff
 | |
| 	changeTypeToName = map[ChangeType]string{
 | |
| 		ChangeNil:     "same",
 | |
| 		ChangeAdded:   "added",
 | |
| 		ChangeDeleted: "deleted",
 | |
| 		ChangeOld:     "old",
 | |
| 		ChangeNew:     "new",
 | |
| 	}
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	// tplJSONDiffWrapper is the template that wraps a diff
 | |
| 	tplJSONDiffWrapper = `{{ define "JSONDiffWrapper" -}}
 | |
| 	{{ range $index, $element := . }}
 | |
| 		{{ template "JSONDiffLine" $element }}
 | |
| 	{{ end }}
 | |
| {{ end }}`
 | |
| 
 | |
| 	// tplJSONDiffLine is the template that prints each line in a diff
 | |
| 	tplJSONDiffLine = `{{ define "JSONDiffLine" -}}
 | |
| <p id="l{{ .LineNum }}" class="diff-line diff-json-{{ cton .Change }}">
 | |
| 	<span class="diff-line-number">
 | |
| 		{{if .LeftLine }}{{ .LeftLine }}{{ end }}
 | |
| 	</span>
 | |
| 	<span class="diff-line-number">
 | |
| 		{{if .RightLine }}{{ .RightLine }}{{ end }}
 | |
| 	</span>
 | |
| 	<span class="diff-value diff-indent-{{ .Indent }}" title="{{ .Text }}" ng-non-bindable>
 | |
| 		{{ .Text }}
 | |
| 	</span>
 | |
| 	<span class="diff-line-icon">{{ ctos .Change }}</span>
 | |
| </p>
 | |
| {{ end }}`
 | |
| )
 | |
| 
 | |
| var diffTplFuncs = template.FuncMap{
 | |
| 	"ctos": func(c ChangeType) string {
 | |
| 		if symbol, ok := changeTypeToSymbol[c]; ok {
 | |
| 			return symbol
 | |
| 		}
 | |
| 		return ""
 | |
| 	},
 | |
| 	"cton": func(c ChangeType) string {
 | |
| 		if name, ok := changeTypeToName[c]; ok {
 | |
| 			return name
 | |
| 		}
 | |
| 		return ""
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // JSONLine contains the data required to render each line of the JSON diff
 | |
| // and contains the data required to produce the tokens output in the basic
 | |
| // diff.
 | |
| type JSONLine struct {
 | |
| 	LineNum   int        `json:"line"`
 | |
| 	LeftLine  int        `json:"leftLine"`
 | |
| 	RightLine int        `json:"rightLine"`
 | |
| 	Indent    int        `json:"indent"`
 | |
| 	Text      string     `json:"text"`
 | |
| 	Change    ChangeType `json:"changeType"`
 | |
| 	Key       string     `json:"key"`
 | |
| 	Val       any        `json:"value"`
 | |
| }
 | |
| 
 | |
| func NewJSONFormatter(left any) *JSONFormatter {
 | |
| 	tpl := template.Must(template.New("JSONDiffWrapper").Funcs(diffTplFuncs).Parse(tplJSONDiffWrapper))
 | |
| 	tpl = template.Must(tpl.New("JSONDiffLine").Funcs(diffTplFuncs).Parse(tplJSONDiffLine))
 | |
| 
 | |
| 	return &JSONFormatter{
 | |
| 		left:      left,
 | |
| 		Lines:     []*JSONLine{},
 | |
| 		tpl:       tpl,
 | |
| 		path:      []string{},
 | |
| 		size:      []int{},
 | |
| 		lineCount: 0,
 | |
| 		inArray:   []bool{},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type JSONFormatter struct {
 | |
| 	left      any
 | |
| 	path      []string
 | |
| 	size      []int
 | |
| 	inArray   []bool
 | |
| 	lineCount int
 | |
| 	leftLine  int
 | |
| 	rightLine int
 | |
| 	line      *AsciiLine
 | |
| 	Lines     []*JSONLine
 | |
| 	tpl       *template.Template
 | |
| }
 | |
| 
 | |
| type AsciiLine struct {
 | |
| 	// the type of change
 | |
| 	change ChangeType
 | |
| 
 | |
| 	// the actual changes - no formatting
 | |
| 	key string
 | |
| 	val any
 | |
| 
 | |
| 	// level of indentation for the current line
 | |
| 	indent int
 | |
| 
 | |
| 	// buffer containing the fully formatted line
 | |
| 	buffer *bytes.Buffer
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) Format(diff diff.Diff) (result string, err error) {
 | |
| 	if v, ok := f.left.(map[string]any); ok {
 | |
| 		if err := f.formatObject(v, diff); err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 	} else if v, ok := f.left.([]any); ok {
 | |
| 		if err := f.formatArray(v, diff); err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 	} else {
 | |
| 		return "", fmt.Errorf("expected map[string]any or []any, got %T",
 | |
| 			f.left)
 | |
| 	}
 | |
| 
 | |
| 	b := &bytes.Buffer{}
 | |
| 	err = f.tpl.ExecuteTemplate(b, "JSONDiffWrapper", f.Lines)
 | |
| 	if err != nil {
 | |
| 		fmt.Printf("%v\n", err)
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return b.String(), nil
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) formatObject(left map[string]any, df diff.Diff) error {
 | |
| 	f.addLineWith(ChangeNil, "{")
 | |
| 	f.push("ROOT", len(left), false)
 | |
| 	if err := f.processObject(left, df.Deltas()); err != nil {
 | |
| 		f.pop()
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	f.pop()
 | |
| 	f.addLineWith(ChangeNil, "}")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) formatArray(left []any, df diff.Diff) error {
 | |
| 	f.addLineWith(ChangeNil, "[")
 | |
| 	f.push("ROOT", len(left), true)
 | |
| 	if err := f.processArray(left, df.Deltas()); err != nil {
 | |
| 		f.pop()
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	f.pop()
 | |
| 	f.addLineWith(ChangeNil, "]")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) processArray(array []any, deltas []diff.Delta) error {
 | |
| 	patchedIndex := 0
 | |
| 	for index, value := range array {
 | |
| 		if err := f.processItem(value, deltas, diff.Index(index)); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		patchedIndex++
 | |
| 	}
 | |
| 
 | |
| 	// additional Added
 | |
| 	for _, delta := range deltas {
 | |
| 		d, ok := delta.(*diff.Added)
 | |
| 		if ok {
 | |
| 			// skip items already processed
 | |
| 			if int(d.Position.(diff.Index)) < len(array) {
 | |
| 				continue
 | |
| 			}
 | |
| 			f.printRecursive(d.String(), d.Value, ChangeAdded)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) processObject(object map[string]any, deltas []diff.Delta) error {
 | |
| 	names := sortKeys(object)
 | |
| 	for _, name := range names {
 | |
| 		value := object[name]
 | |
| 		if err := f.processItem(value, deltas, diff.Name(name)); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Added
 | |
| 	for _, delta := range deltas {
 | |
| 		d, ok := delta.(*diff.Added)
 | |
| 		if ok {
 | |
| 			f.printRecursive(d.String(), d.Value, ChangeAdded)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) processItem(value any, deltas []diff.Delta, position diff.Position) error {
 | |
| 	matchedDeltas := f.searchDeltas(deltas, position)
 | |
| 	positionStr := position.String()
 | |
| 	if len(matchedDeltas) > 0 {
 | |
| 		for _, matchedDelta := range matchedDeltas {
 | |
| 			switch matchedDelta := matchedDelta.(type) {
 | |
| 			case *diff.Object:
 | |
| 				switch value.(type) {
 | |
| 				case map[string]any:
 | |
| 					// ok
 | |
| 				default:
 | |
| 					return errors.New("type mismatch")
 | |
| 				}
 | |
| 				o := value.(map[string]any)
 | |
| 
 | |
| 				f.newLine(ChangeNil)
 | |
| 				f.printKey(positionStr)
 | |
| 				f.print("{")
 | |
| 				f.closeLine()
 | |
| 				f.push(positionStr, len(o), false)
 | |
| 				if err := f.processObject(o, matchedDelta.Deltas); err != nil {
 | |
| 					f.pop()
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				f.pop()
 | |
| 				f.newLine(ChangeNil)
 | |
| 				f.print("}")
 | |
| 				f.printComma()
 | |
| 				f.closeLine()
 | |
| 
 | |
| 			case *diff.Array:
 | |
| 				switch value.(type) {
 | |
| 				case []any:
 | |
| 					// ok
 | |
| 				default:
 | |
| 					return errors.New("type mismatch")
 | |
| 				}
 | |
| 				a := value.([]any)
 | |
| 
 | |
| 				f.newLine(ChangeNil)
 | |
| 				f.printKey(positionStr)
 | |
| 				f.print("[")
 | |
| 				f.closeLine()
 | |
| 				f.push(positionStr, len(a), true)
 | |
| 				if err := f.processArray(a, matchedDelta.Deltas); err != nil {
 | |
| 					f.pop()
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				f.pop()
 | |
| 				f.newLine(ChangeNil)
 | |
| 				f.print("]")
 | |
| 				f.printComma()
 | |
| 				f.closeLine()
 | |
| 
 | |
| 			case *diff.Added:
 | |
| 				f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded)
 | |
| 				f.size[len(f.size)-1]++
 | |
| 
 | |
| 			case *diff.Modified:
 | |
| 				savedSize := f.size[len(f.size)-1]
 | |
| 				f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld)
 | |
| 				f.size[len(f.size)-1] = savedSize
 | |
| 				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)
 | |
| 
 | |
| 			case *diff.TextDiff:
 | |
| 				savedSize := f.size[len(f.size)-1]
 | |
| 				f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld)
 | |
| 				f.size[len(f.size)-1] = savedSize
 | |
| 				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)
 | |
| 
 | |
| 			case *diff.Deleted:
 | |
| 				f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted)
 | |
| 
 | |
| 			default:
 | |
| 				return fmt.Errorf("unknown Delta type detected %#v", matchedDelta)
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		f.printRecursive(positionStr, value, ChangeUnchanged)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
 | |
| 	results = make([]diff.Delta, 0)
 | |
| 	for _, delta := range deltas {
 | |
| 		switch typedDelta := delta.(type) {
 | |
| 		case diff.PostDelta:
 | |
| 			if typedDelta.PostPosition() == position {
 | |
| 				results = append(results, delta)
 | |
| 			}
 | |
| 		case diff.PreDelta:
 | |
| 			if typedDelta.PrePosition() == position {
 | |
| 				results = append(results, delta)
 | |
| 			}
 | |
| 		default:
 | |
| 			panic("heh")
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) 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 *JSONFormatter) 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 *JSONFormatter) addLineWith(change ChangeType, value string) {
 | |
| 	f.line = &AsciiLine{
 | |
| 		change: change,
 | |
| 		indent: len(f.path),
 | |
| 		buffer: bytes.NewBufferString(value),
 | |
| 	}
 | |
| 	f.closeLine()
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) newLine(change ChangeType) {
 | |
| 	f.line = &AsciiLine{
 | |
| 		change: change,
 | |
| 		indent: len(f.path),
 | |
| 		buffer: bytes.NewBuffer([]byte{}),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) closeLine() {
 | |
| 	leftLine := 0
 | |
| 	rightLine := 0
 | |
| 	f.lineCount++
 | |
| 
 | |
| 	switch f.line.change {
 | |
| 	case ChangeAdded, ChangeNew:
 | |
| 		f.rightLine++
 | |
| 		rightLine = f.rightLine
 | |
| 
 | |
| 	case ChangeDeleted, ChangeOld:
 | |
| 		f.leftLine++
 | |
| 		leftLine = f.leftLine
 | |
| 
 | |
| 	case ChangeNil, ChangeUnchanged:
 | |
| 		f.rightLine++
 | |
| 		f.leftLine++
 | |
| 		rightLine = f.rightLine
 | |
| 		leftLine = f.leftLine
 | |
| 	}
 | |
| 
 | |
| 	s := f.line.buffer.String()
 | |
| 	f.Lines = append(f.Lines, &JSONLine{
 | |
| 		LineNum:   f.lineCount,
 | |
| 		RightLine: rightLine,
 | |
| 		LeftLine:  leftLine,
 | |
| 		Indent:    f.line.indent,
 | |
| 		Text:      s,
 | |
| 		Change:    f.line.change,
 | |
| 		Key:       f.line.key,
 | |
| 		Val:       f.line.val,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) printKey(name string) {
 | |
| 	if !f.inArray[len(f.inArray)-1] {
 | |
| 		f.line.key = name
 | |
| 		fmt.Fprintf(f.line.buffer, `"%s": `, name)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) printComma() {
 | |
| 	f.size[len(f.size)-1]--
 | |
| 	if f.size[len(f.size)-1] > 0 {
 | |
| 		f.line.buffer.WriteRune(',')
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) printValue(value any) {
 | |
| 	switch value.(type) {
 | |
| 	case string:
 | |
| 		f.line.val = value
 | |
| 		fmt.Fprintf(f.line.buffer, `"%s"`, value)
 | |
| 	case nil:
 | |
| 		f.line.val = "null"
 | |
| 		f.line.buffer.WriteString("null")
 | |
| 	default:
 | |
| 		f.line.val = value
 | |
| 		fmt.Fprintf(f.line.buffer, `%#v`, value)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) print(a string) {
 | |
| 	f.line.buffer.WriteString(a)
 | |
| }
 | |
| 
 | |
| func (f *JSONFormatter) printRecursive(name string, value any, change ChangeType) {
 | |
| 	switch value := value.(type) {
 | |
| 	case map[string]any:
 | |
| 		f.newLine(change)
 | |
| 		f.printKey(name)
 | |
| 		f.print("{")
 | |
| 		f.closeLine()
 | |
| 
 | |
| 		size := len(value)
 | |
| 		f.push(name, size, false)
 | |
| 
 | |
| 		keys := sortKeys(value)
 | |
| 		for _, key := range keys {
 | |
| 			f.printRecursive(key, value[key], change)
 | |
| 		}
 | |
| 		f.pop()
 | |
| 
 | |
| 		f.newLine(change)
 | |
| 		f.print("}")
 | |
| 		f.printComma()
 | |
| 		f.closeLine()
 | |
| 
 | |
| 	case []any:
 | |
| 		f.newLine(change)
 | |
| 		f.printKey(name)
 | |
| 		f.print("[")
 | |
| 		f.closeLine()
 | |
| 
 | |
| 		size := len(value)
 | |
| 		f.push("", size, true)
 | |
| 		for _, item := range value {
 | |
| 			f.printRecursive("", item, change)
 | |
| 		}
 | |
| 		f.pop()
 | |
| 
 | |
| 		f.newLine(change)
 | |
| 		f.print("]")
 | |
| 		f.printComma()
 | |
| 		f.closeLine()
 | |
| 
 | |
| 	default:
 | |
| 		f.newLine(change)
 | |
| 		f.printKey(name)
 | |
| 		f.printValue(value)
 | |
| 		f.printComma()
 | |
| 		f.closeLine()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func sortKeys(m map[string]any) (keys []string) {
 | |
| 	keys = make([]string, 0, len(m))
 | |
| 	for key := range m {
 | |
| 		keys = append(keys, key)
 | |
| 	}
 | |
| 	sort.Strings(keys)
 | |
| 	return
 | |
| }
 |