formatter_basic.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. package dashdiffs
  2. import (
  3. "bytes"
  4. "html/template"
  5. diff "github.com/yudai/gojsondiff"
  6. )
  7. // A BasicDiff holds the stateful values that are used when generating a basic
  8. // diff from JSON tokens.
  9. type BasicDiff struct {
  10. narrow string
  11. keysIdent int
  12. writing bool
  13. LastIndent int
  14. Block *BasicBlock
  15. Change *BasicChange
  16. Summary *BasicSummary
  17. }
  18. // A BasicBlock represents a top-level element in a basic diff.
  19. type BasicBlock struct {
  20. Title string
  21. Old interface{}
  22. New interface{}
  23. Change ChangeType
  24. Changes []*BasicChange
  25. Summaries []*BasicSummary
  26. LineStart int
  27. LineEnd int
  28. }
  29. // A BasicChange represents the change from an old to new value. There are many
  30. // BasicChanges in a BasicBlock.
  31. type BasicChange struct {
  32. Key string
  33. Old interface{}
  34. New interface{}
  35. Change ChangeType
  36. LineStart int
  37. LineEnd int
  38. }
  39. // A BasicSummary represents the changes within a basic block that're too deep
  40. // or verbose to be represented in the top-level BasicBlock element, or in the
  41. // BasicChange. Instead of showing the values in this case, we simply print
  42. // the key and count how many times the given change was applied to that
  43. // element.
  44. type BasicSummary struct {
  45. Key string
  46. Change ChangeType
  47. Count int
  48. LineStart int
  49. LineEnd int
  50. }
  51. type BasicFormatter struct {
  52. jsonDiff *JSONFormatter
  53. tpl *template.Template
  54. }
  55. func NewBasicFormatter(left interface{}) *BasicFormatter {
  56. tpl := template.Must(template.New("block").Funcs(tplFuncMap).Parse(tplBlock))
  57. tpl = template.Must(tpl.New("change").Funcs(tplFuncMap).Parse(tplChange))
  58. tpl = template.Must(tpl.New("summary").Funcs(tplFuncMap).Parse(tplSummary))
  59. return &BasicFormatter{
  60. jsonDiff: NewJSONFormatter(left),
  61. tpl: tpl,
  62. }
  63. }
  64. // Format takes the diff of two JSON documents, and returns the difference
  65. // between them summarized in an HTML document.
  66. func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
  67. // calling jsonDiff.Format(d) populates the JSON diff's "Lines" value,
  68. // which we use to compute the basic dif
  69. _, err := b.jsonDiff.Format(d)
  70. if err != nil {
  71. return nil, err
  72. }
  73. bd := &BasicDiff{}
  74. blocks := bd.Basic(b.jsonDiff.Lines)
  75. buf := &bytes.Buffer{}
  76. err = b.tpl.ExecuteTemplate(buf, "block", blocks)
  77. if err != nil {
  78. return nil, err
  79. }
  80. return buf.Bytes(), nil
  81. }
  82. // Basic transforms a slice of JSONLines into a slice of BasicBlocks.
  83. func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
  84. // init an array you can append to for the basic "blocks"
  85. blocks := make([]*BasicBlock, 0)
  86. for _, line := range lines {
  87. if b.returnToTopLevelKey(line) {
  88. if b.Block != nil {
  89. blocks = append(blocks, b.Block)
  90. }
  91. }
  92. // Record the last indent level at each pass in case we need to
  93. // check for a change in depth inside the JSON data structures.
  94. b.LastIndent = line.Indent
  95. if line.Indent == 1 {
  96. if block, ok := b.handleTopLevelChange(line); ok {
  97. blocks = append(blocks, block)
  98. }
  99. }
  100. // Here is where we handle changes for all types, appending each change
  101. // to the current block based on the value.
  102. //
  103. // Values which only occupy a single line in JSON (like a string or
  104. // int, for example) are treated as "Basic Changes" that we append to
  105. // the current block as soon as they're detected.
  106. //
  107. // Values which occupy multiple lines (either slices or maps) are
  108. // treated as "Basic Summaries". When we detect the "ChangeNil" type,
  109. // we know we've encountered one of these types, so we record the
  110. // starting position as well the type of the change, and stop
  111. // performing comparisons until we find the end of that change. Upon
  112. // finding the change, we append it to the current block, and begin
  113. // performing comparisons again.
  114. if line.Indent > 1 {
  115. // check to ensure a single line change
  116. if b.isSingleLineChange(line) {
  117. switch line.Change {
  118. case ChangeAdded, ChangeDeleted:
  119. b.Block.Changes = append(b.Block.Changes, &BasicChange{
  120. Key: line.Key,
  121. Change: line.Change,
  122. New: line.Val,
  123. LineStart: line.LineNum,
  124. })
  125. case ChangeOld:
  126. b.Change = &BasicChange{
  127. Key: line.Key,
  128. Change: line.Change,
  129. Old: line.Val,
  130. LineStart: line.LineNum,
  131. }
  132. case ChangeNew:
  133. b.Change.New = line.Val
  134. b.Change.LineEnd = line.LineNum
  135. b.Block.Changes = append(b.Block.Changes, b.Change)
  136. default:
  137. //ok
  138. }
  139. // otherwise, we're dealing with a change at a deeper level. We
  140. // know there's a change somewhere in the JSON tree, but we
  141. // don't know exactly where, so we go deeper.
  142. } else {
  143. // if the change is anything but unchanged, continue processing
  144. //
  145. // we keep "narrowing" the key as we go deeper, in order to
  146. // correctly report the key name for changes found within an
  147. // object or array.
  148. if line.Change != ChangeUnchanged {
  149. if line.Key != "" {
  150. b.narrow = line.Key
  151. b.keysIdent = line.Indent
  152. }
  153. // if the change isn't nil, and we're not already writing
  154. // out a change, then we've found something.
  155. //
  156. // First, try to determine the title of the embedded JSON
  157. // object. If it's an empty string, then we're in an object
  158. // or array, so we default to using the "narrowed" key.
  159. //
  160. // We also start recording the basic summary, until we find
  161. // the next `ChangeUnchanged`.
  162. if line.Change != ChangeNil {
  163. if !b.writing {
  164. b.writing = true
  165. key := b.Block.Title
  166. if b.narrow != "" {
  167. key = b.narrow
  168. if b.keysIdent > line.Indent {
  169. key = b.Block.Title
  170. }
  171. }
  172. b.Summary = &BasicSummary{
  173. Key: key,
  174. Change: line.Change,
  175. LineStart: line.LineNum,
  176. }
  177. }
  178. }
  179. // if we find a `ChangeUnchanged`, we do one of two things:
  180. //
  181. // - if we're recording a change already, then we know
  182. // we've come to the end of that change block, so we write
  183. // that change out be recording the line number of where
  184. // that change ends, and append it to the current block's
  185. // summary.
  186. //
  187. // - if we're not recording a change, then we do nothing,
  188. // since the BasicDiff doesn't report on unchanged JSON
  189. // values.
  190. } else {
  191. if b.writing {
  192. b.writing = false
  193. b.Summary.LineEnd = line.LineNum
  194. b.Block.Summaries = append(b.Block.Summaries, b.Summary)
  195. }
  196. }
  197. }
  198. }
  199. }
  200. return blocks
  201. }
  202. // returnToTopLevelKey indicates that we've moved from a key at one level deep
  203. // in the JSON document to a top level key.
  204. //
  205. // In order to produce distinct "blocks" when rendering the basic diff,
  206. // we need a way to distinguish between different sections of data.
  207. // To do this, we consider the value(s) of each top-level JSON key to
  208. // represent a distinct block for Grafana's JSON data structure, so
  209. // we perform this check to see if we've entered a new "block". If we
  210. // have, we simply append the existing block to the array of blocks.
  211. func (b *BasicDiff) returnToTopLevelKey(line *JSONLine) bool {
  212. return b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil
  213. }
  214. // handleTopLevelChange handles a change on one of the top-level keys on a JSON
  215. // document.
  216. //
  217. // If the line's indentation is at level 1, then we know it's a top
  218. // level key in the JSON document. As mentioned earlier, we treat these
  219. // specially as they indicate their values belong to distinct blocks.
  220. //
  221. // At level 1, we only record single-line changes, ie, the "added",
  222. // "deleted", "old" or "new" cases, since we know those values aren't
  223. // arrays or maps. We only handle these cases at level 2 or deeper,
  224. // since for those we either output a "change" or "summary". This is
  225. // done for formatting reasons only, so we have logical "blocks" to
  226. // display.
  227. func (b *BasicDiff) handleTopLevelChange(line *JSONLine) (*BasicBlock, bool) {
  228. switch line.Change {
  229. case ChangeNil:
  230. if line.Change == ChangeNil {
  231. if line.Key != "" {
  232. b.Block = &BasicBlock{
  233. Title: line.Key,
  234. Change: line.Change,
  235. }
  236. }
  237. }
  238. case ChangeAdded, ChangeDeleted:
  239. return &BasicBlock{
  240. Title: line.Key,
  241. Change: line.Change,
  242. New: line.Val,
  243. LineStart: line.LineNum,
  244. }, true
  245. case ChangeOld:
  246. b.Block = &BasicBlock{
  247. Title: line.Key,
  248. Old: line.Val,
  249. Change: line.Change,
  250. LineStart: line.LineNum,
  251. }
  252. case ChangeNew:
  253. b.Block.New = line.Val
  254. b.Block.LineEnd = line.LineNum
  255. // For every "old" change there is a corresponding "new", which
  256. // is why we wait until we detect the "new" change before
  257. // appending the change.
  258. return b.Block, true
  259. default:
  260. // ok
  261. }
  262. return nil, false
  263. }
  264. // isSingleLineChange ensures we're iterating over a single line change (ie,
  265. // either a single line or a old-new value pair was changed in the JSON file).
  266. func (b *BasicDiff) isSingleLineChange(line *JSONLine) bool {
  267. return line.Key != "" && line.Val != nil && !b.writing
  268. }
  269. // encStateMap is used in the template helper
  270. var (
  271. encStateMap = map[ChangeType]string{
  272. ChangeAdded: "added",
  273. ChangeDeleted: "deleted",
  274. ChangeOld: "changed",
  275. ChangeNew: "changed",
  276. }
  277. // tplFuncMap is the function map for each template
  278. tplFuncMap = template.FuncMap{
  279. "getChange": func(c ChangeType) string {
  280. state, ok := encStateMap[c]
  281. if !ok {
  282. return "changed"
  283. }
  284. return state
  285. },
  286. }
  287. )
  288. var (
  289. // tplBlock is the container for the basic diff. It iterates over each
  290. // basic block, expanding each "change" and "summary" belonging to every
  291. // block.
  292. tplBlock = `{{ define "block" -}}
  293. {{ range . }}
  294. <div class="diff-group">
  295. <div class="diff-block">
  296. <h2 class="diff-block-title">
  297. <i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle"></i>
  298. <strong class="diff-title">{{ .Title }}</strong> {{ getChange .Change }}
  299. </h2>
  300. <!-- Overview -->
  301. {{ if .Old }}
  302. <div class="diff-label">{{ .Old }}</div>
  303. <i class="diff-arrow fa fa-long-arrow-right"></i>
  304. {{ end }}
  305. {{ if .New }}
  306. <div class="diff-label">{{ .New }}</div>
  307. {{ end }}
  308. {{ if .LineStart }}
  309. <diff-link-json
  310. line-link="{{ .LineStart }}"
  311. line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
  312. switch-view="ctrl.getDiff('html')"
  313. />
  314. {{ end }}
  315. </div>
  316. <!-- Basic Changes -->
  317. {{ range .Changes }}
  318. <ul class="diff-change-container">
  319. {{ template "change" . }}
  320. </ul>
  321. {{ end }}
  322. <!-- Basic Summary -->
  323. {{ range .Summaries }}
  324. {{ template "summary" . }}
  325. {{ end }}
  326. </div>
  327. {{ end }}
  328. {{ end }}`
  329. // tplChange is the template for basic changes.
  330. tplChange = `{{ define "change" -}}
  331. <li class="diff-change-group">
  332. <span class="bullet-position-container">
  333. <div class="diff-change-item diff-change-title">{{ getChange .Change }} {{ .Key }}</div>
  334. <div class="diff-change-item">
  335. {{ if .Old }}
  336. <div class="diff-label">{{ .Old }}</div>
  337. <i class="diff-arrow fa fa-long-arrow-right"></i>
  338. {{ end }}
  339. {{ if .New }}
  340. <div class="diff-label">{{ .New }}</div>
  341. {{ end }}
  342. </div>
  343. {{ if .LineStart }}
  344. <diff-link-json
  345. line-link="{{ .LineStart }}"
  346. line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
  347. switch-view="ctrl.getDiff('json')"
  348. />
  349. {{ end }}
  350. </span>
  351. </li>
  352. {{ end }}`
  353. // tplSummary is for basic summaries.
  354. tplSummary = `{{ define "summary" -}}
  355. <div class="diff-group-name">
  356. <i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i>
  357. {{ if .Count }}
  358. <strong>{{ .Count }}</strong>
  359. {{ end }}
  360. {{ if .Key }}
  361. <strong class="diff-summary-key">{{ .Key }}</strong>
  362. {{ getChange .Change }}
  363. {{ end }}
  364. {{ if .LineStart }}
  365. <diff-link-json
  366. line-link="{{ .LineStart }}"
  367. line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
  368. switch-view="ctrl.getDiff('json')"
  369. />
  370. {{ end }}
  371. </div>
  372. {{ end }}`
  373. )