You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/01/08 23:05:40 UTC

[trafficcontrol] branch master updated: Ort diff-tool for t3c (#5337)

This is an automated email from the ASF dual-hosted git repository.

ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 7a3506a  Ort diff-tool for t3c  (#5337)
7a3506a is described below

commit 7a3506a19a064f787b1f02de28c12feab24ff7c1
Author: Joe Pappano <jo...@cable.comcast.com>
AuthorDate: Fri Jan 8 18:05:26 2021 -0500

    Ort diff-tool for t3c  (#5337)
    
    * Initial commit of t3c-diff tool
    
    * Initial commit of t3c-util package
    
    * Added regex to only print changes.
    
    * Changed variable name.
    
    * Fixed typos, and diff variable that collided with package name.
    
    * Adding new dependency to vendor folder
    
    * Adding new dependency to vendor folder one more time to get subdirectories.
    
    * Swapped regex for less than, greater than, and ampersand with html.UnescapeString.
    
    * Swapped regex for less than, greater than, and ampersand with html.UnescapeString.
    
    * Added license info for the kylelemons/godebug component
    
    * Updated paths for license info for the kylelemons/godebug component
---
 .dependency_license                                |   1 +
 LICENSE                                            |   4 +
 traffic_ops_ort/t3c-diff-tool/t3c-diff-tool.go     |  71 ++++
 traffic_ops_ort/t3cutil/t3cutil.go                 |  87 ++++
 vendor/github.com/kylelemons/godebug/.travis.yml   |  10 +
 vendor/github.com/kylelemons/godebug/LICENSE       | 202 +++++++++
 vendor/github.com/kylelemons/godebug/README.md     |  65 +++
 vendor/github.com/kylelemons/godebug/diff/diff.go  | 189 +++++++++
 .../kylelemons/godebug/diff/diff_test.go           | 228 +++++++++++
 vendor/github.com/kylelemons/godebug/go.mod        |   3 +
 vendor/github.com/kylelemons/godebug/pretty/doc.go |  22 +
 .../kylelemons/godebug/pretty/examples_test.go     | 373 +++++++++++++++++
 .../github.com/kylelemons/godebug/pretty/public.go | 188 +++++++++
 .../kylelemons/godebug/pretty/public_test.go       | 155 +++++++
 .../kylelemons/godebug/pretty/reflect.go           | 255 ++++++++++++
 .../kylelemons/godebug/pretty/reflect_test.go      | 456 +++++++++++++++++++++
 .../kylelemons/godebug/pretty/structure.go         | 223 ++++++++++
 .../kylelemons/godebug/pretty/structure_test.go    | 371 +++++++++++++++++
 18 files changed, 2903 insertions(+)

diff --git a/.dependency_license b/.dependency_license
index cca8aab..3d7039f 100644
--- a/.dependency_license
+++ b/.dependency_license
@@ -111,6 +111,7 @@ jquery\.tree\.min\.css$, MIT
 jquery\.dataTables\..*\.(css|js)$, MIT
 github\.com/basho/backoff/.*, MIT
 github\.com/dchest/siphash/.*, CC0
+github\.com/kylelemons/godebug.*, Apache-2.0
 github\.com/pkg/errors\..*, BSD
 traffic_portal/app/src/assets/js/chartjs/angular-chart\..*, BSD
 traffic_portal/app/src/assets/css/jsonformatter\..*, Apache-2.0
diff --git a/LICENSE b/LICENSE
index 63d8da3..ebfc84d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -625,6 +625,10 @@ For the kolo/xmlrpc component:
 @traffic_ops/traffic_ops_golang/vendor/github.com/kolo/xmlrpc/*
 ./traffic_ops/traffic_ops_golang/vendor/github.com/kolo/xmlrpc/LICENSE
 
+For the kylelemons/godebug component:
+@vendor/github.com/kylelemons/godebug/*
+.vendor/github.com/kylelemons/godebug/LICENSE
+
 For the labbsr0x/bindman-dns-webhook component:
 @traffic_ops/traffic_ops_golang/vendor/github.com/labbsr0x/bindman-dns-webhook/*
 ./traffic_ops/traffic_ops_golang/vendor/github.com/labbsr0x/bindman-dns-webhook/LICENSE
diff --git a/traffic_ops_ort/t3c-diff-tool/t3c-diff-tool.go b/traffic_ops_ort/t3c-diff-tool/t3c-diff-tool.go
new file mode 100644
index 0000000..8e8d623
--- /dev/null
+++ b/traffic_ops_ort/t3c-diff-tool/t3c-diff-tool.go
@@ -0,0 +1,71 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/apache/trafficcontrol/traffic_ops_ort/t3cutil"
+	"github.com/kylelemons/godebug/diff"
+	"github.com/pborman/getopt/v2"
+)
+
+func main() {
+	tropsFile := getopt.StringLong("trops-file", 't', "", "Required: Config file name in Traffic Ops")
+	diskFile := getopt.StringLong("disk-file", 'd', "", "Required: Config file on disk")
+	help := getopt.BoolLong("help", 'h', "Print usage info and exit")
+	getopt.ParseV2()
+
+	if *help {
+		getopt.PrintUsage(os.Stdout)
+		os.Exit(0)
+	}
+	if len(strings.TrimSpace(*tropsFile)) == 0 || len(strings.TrimSpace(*diskFile)) == 0 {
+		getopt.PrintUsage(os.Stdout)
+		os.Exit(1)
+	}
+	trafOpsInput := t3cutil.ReadFile(*tropsFile)
+	diskInput := t3cutil.ReadFile(*diskFile)
+
+	trOpsData := strings.Split(string(trafOpsInput), "\n")
+	trOpsData = t3cutil.UnencodeFilter(trOpsData)
+	trOpsData = t3cutil.CommentsFilter(trOpsData)
+	trOps := strings.Join(trOpsData, "\n")
+	trOps = t3cutil.NewLineFilter(trOps)
+
+	diskData := strings.Split(string(diskInput), "\n")
+	diskData = t3cutil.UnencodeFilter(diskData)
+	diskData = t3cutil.CommentsFilter(diskData)
+	disk := strings.Join(diskData, "\n")
+	disk = t3cutil.NewLineFilter(disk)
+
+	if trOps != disk {
+		match := regexp.MustCompile(`(?m)^\+.*|^-.*`)
+		changes := diff.Diff(disk, trOps)
+		for _, change := range match.FindAllString(changes, -1) {
+			fmt.Println(change)
+		}
+	} else {
+		os.Exit(0)
+	}
+}
diff --git a/traffic_ops_ort/t3cutil/t3cutil.go b/traffic_ops_ort/t3cutil/t3cutil.go
new file mode 100644
index 0000000..ba8b728
--- /dev/null
+++ b/traffic_ops_ort/t3cutil/t3cutil.go
@@ -0,0 +1,87 @@
+package t3cutil
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+	"html"
+	"io/ioutil"
+	"os"
+	"regexp"
+	"strings"
+)
+
+// commentsFilter is used to remove comment
+// lines from config files while making
+// comparisons.
+func CommentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+// NewLineFilter removes carriage returns
+// from config files while making comparisons.
+func NewLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+// ReadFile reads a file and returns the
+// file contents.
+func ReadFile(f string) []byte {
+	data, err := ioutil.ReadFile(f)
+	if err != nil {
+		fmt.Println("Error reading file ", f)
+		os.Exit(1)
+	}
+	return data
+}
+
+// UnencodeFilter translates HTML escape
+// sequences while making config file comparisons.
+func UnencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = html.UnescapeString(s)
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
diff --git a/vendor/github.com/kylelemons/godebug/.travis.yml b/vendor/github.com/kylelemons/godebug/.travis.yml
new file mode 100644
index 0000000..3ddfb8a
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/.travis.yml
@@ -0,0 +1,10 @@
+language: go
+
+go:
+  - "1.14.x"
+  - "1.15.x"
+
+arch:
+  - amd64
+  - arm64
+  - ppc64le
diff --git a/vendor/github.com/kylelemons/godebug/LICENSE b/vendor/github.com/kylelemons/godebug/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/vendor/github.com/kylelemons/godebug/README.md b/vendor/github.com/kylelemons/godebug/README.md
new file mode 100644
index 0000000..f225f48
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/README.md
@@ -0,0 +1,65 @@
+Pretty Printing for Go
+======================
+
+[![godebug build status][ciimg]][ci]
+
+Have you ever wanted to get a pretty-printed version of a Go data structure,
+complete with indentation?  I have found this especially useful in unit tests
+and in debugging my code, and thus godebug was born!
+
+[ciimg]: https://travis-ci.org/kylelemons/godebug.svg?branch=master
+[ci]:    https://travis-ci.org/kylelemons/godebug
+
+Quick Examples
+--------------
+
+By default, pretty will write out a very compact representation of a data structure.
+From the [Print example][printex]:
+
+```
+{Name:     "Spaceship Heart of Gold",
+ Crew:     {Arthur Dent:       "Along for the Ride",
+            Ford Prefect:      "A Hoopy Frood",
+            Trillian:          "Human",
+            Zaphod Beeblebrox: "Galactic President"},
+ Androids: 1,
+ Stolen:   true}
+```
+
+It can also produce a much more verbose, one-item-per-line representation suitable for
+[computing diffs][diffex].  See the documentation for more examples and customization.
+
+[printex]: https://godoc.org/github.com/kylelemons/godebug/pretty#example-Print
+[diffex]:  https://godoc.org/github.com/kylelemons/godebug/pretty#example-Compare
+
+Documentation
+-------------
+
+Documentation for this package is available at [godoc.org][doc]:
+
+ * Pretty: [![godoc for godebug/pretty][prettyimg]][prettydoc]
+ * Diff:   [![godoc for godebug/diff][diffimg]][diffdoc]
+
+[doc]:       https://godoc.org/
+[prettyimg]: https://godoc.org/github.com/kylelemons/godebug/pretty?status.png
+[prettydoc]: https://godoc.org/github.com/kylelemons/godebug/pretty
+[diffimg]:   https://godoc.org/github.com/kylelemons/godebug/diff?status.png
+[diffdoc]:   https://godoc.org/github.com/kylelemons/godebug/diff
+
+Installation
+------------
+
+These packages are available via `go get`:
+
+```bash
+$ go get -u github.com/kylelemons/godebug/{pretty,diff}
+```
+
+Other Packages
+--------------
+
+If `godebug/pretty` is not granular enough, I highly recommend
+checking out [cmp][cmp] or [go-spew][spew].
+
+[cmp]: https://godoc.org/github.com/google/go-cmp/cmp
+[spew]: http://godoc.org/github.com/davecgh/go-spew/spew
diff --git a/vendor/github.com/kylelemons/godebug/diff/diff.go b/vendor/github.com/kylelemons/godebug/diff/diff.go
new file mode 100644
index 0000000..71b459f
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/diff/diff.go
@@ -0,0 +1,189 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package diff implements a linewise diff algorithm.
+package diff
+
+import (
+	"fmt"
+	"strings"
+)
+
+// Chunk represents a piece of the diff.  A chunk will not have both added and
+// deleted lines.  Equal lines are always after any added or deleted lines.
+// A Chunk may or may not have any lines in it, especially for the first or last
+// chunk in a computation.
+type Chunk struct {
+	Added   []string
+	Deleted []string
+	Equal   []string
+}
+
+func (c *Chunk) empty() bool {
+	return len(c.Added) == 0 && len(c.Deleted) == 0 && len(c.Equal) == 0
+}
+
+// Diff returns a string containing a line-by-line unified diff of the linewise
+// changes required to make A into B.  Each line is prefixed with '+', '-', or
+// ' ' to indicate if it should be added, removed, or is correct respectively.
+func Diff(A, B string) string {
+	aLines := strings.Split(A, "\n")
+	bLines := strings.Split(B, "\n")
+	return Render(DiffChunks(aLines, bLines))
+}
+
+// Render renders the slice of chunks into a representation that prefixes
+// the lines with '+', '-', or ' ' depending on whether the line was added,
+// removed, or equal (respectively).
+func Render(chunks []Chunk) string {
+	buf := new(strings.Builder)
+	for _, c := range chunks {
+		for _, line := range c.Added {
+			fmt.Fprintf(buf, "+%s\n", line)
+		}
+		for _, line := range c.Deleted {
+			fmt.Fprintf(buf, "-%s\n", line)
+		}
+		for _, line := range c.Equal {
+			fmt.Fprintf(buf, " %s\n", line)
+		}
+	}
+	return strings.TrimRight(buf.String(), "\n")
+}
+
+// DiffChunks uses an O(D(N+M)) shortest-edit-script algorithm
+// to compute the edits required from A to B and returns the
+// edit chunks.
+func DiffChunks(a, b []string) []Chunk {
+	// algorithm: http://www.xmailserver.org/diff2.pdf
+
+	// We'll need these quantities a lot.
+	alen, blen := len(a), len(b) // M, N
+
+	// At most, it will require len(a) deletions and len(b) additions
+	// to transform a into b.
+	maxPath := alen + blen // MAX
+	if maxPath == 0 {
+		// degenerate case: two empty lists are the same
+		return nil
+	}
+
+	// Store the endpoint of the path for diagonals.
+	// We store only the a index, because the b index on any diagonal
+	// (which we know during the loop below) is aidx-diag.
+	// endpoint[maxPath] represents the 0 diagonal.
+	//
+	// Stated differently:
+	// endpoint[d] contains the aidx of a furthest reaching path in diagonal d
+	endpoint := make([]int, 2*maxPath+1) // V
+
+	saved := make([][]int, 0, 8) // Vs
+	save := func() {
+		dup := make([]int, len(endpoint))
+		copy(dup, endpoint)
+		saved = append(saved, dup)
+	}
+
+	var editDistance int // D
+dLoop:
+	for editDistance = 0; editDistance <= maxPath; editDistance++ {
+		// The 0 diag(onal) represents equality of a and b.  Each diagonal to
+		// the left is numbered one lower, to the right is one higher, from
+		// -alen to +blen.  Negative diagonals favor differences from a,
+		// positive diagonals favor differences from b.  The edit distance to a
+		// diagonal d cannot be shorter than d itself.
+		//
+		// The iterations of this loop cover either odds or evens, but not both,
+		// If odd indices are inputs, even indices are outputs and vice versa.
+		for diag := -editDistance; diag <= editDistance; diag += 2 { // k
+			var aidx int // x
+			switch {
+			case diag == -editDistance:
+				// This is a new diagonal; copy from previous iter
+				aidx = endpoint[maxPath-editDistance+1] + 0
+			case diag == editDistance:
+				// This is a new diagonal; copy from previous iter
+				aidx = endpoint[maxPath+editDistance-1] + 1
+			case endpoint[maxPath+diag+1] > endpoint[maxPath+diag-1]:
+				// diagonal d+1 was farther along, so use that
+				aidx = endpoint[maxPath+diag+1] + 0
+			default:
+				// diagonal d-1 was farther (or the same), so use that
+				aidx = endpoint[maxPath+diag-1] + 1
+			}
+			// On diagonal d, we can compute bidx from aidx.
+			bidx := aidx - diag // y
+			// See how far we can go on this diagonal before we find a difference.
+			for aidx < alen && bidx < blen && a[aidx] == b[bidx] {
+				aidx++
+				bidx++
+			}
+			// Store the end of the current edit chain.
+			endpoint[maxPath+diag] = aidx
+			// If we've found the end of both inputs, we're done!
+			if aidx >= alen && bidx >= blen {
+				save() // save the final path
+				break dLoop
+			}
+		}
+		save() // save the current path
+	}
+	if editDistance == 0 {
+		return nil
+	}
+	chunks := make([]Chunk, editDistance+1)
+
+	x, y := alen, blen
+	for d := editDistance; d > 0; d-- {
+		endpoint := saved[d]
+		diag := x - y
+		insert := diag == -d || (diag != d && endpoint[maxPath+diag-1] < endpoint[maxPath+diag+1])
+
+		x1 := endpoint[maxPath+diag]
+		var x0, xM, kk int
+		if insert {
+			kk = diag + 1
+			x0 = endpoint[maxPath+kk]
+			xM = x0
+		} else {
+			kk = diag - 1
+			x0 = endpoint[maxPath+kk]
+			xM = x0 + 1
+		}
+		y0 := x0 - kk
+
+		var c Chunk
+		if insert {
+			c.Added = b[y0:][:1]
+		} else {
+			c.Deleted = a[x0:][:1]
+		}
+		if xM < x1 {
+			c.Equal = a[xM:][:x1-xM]
+		}
+
+		x, y = x0, y0
+		chunks[d] = c
+	}
+	if x > 0 {
+		chunks[0].Equal = a[:x]
+	}
+	if chunks[0].empty() {
+		chunks = chunks[1:]
+	}
+	if len(chunks) == 0 {
+		return nil
+	}
+	return chunks
+}
diff --git a/vendor/github.com/kylelemons/godebug/diff/diff_test.go b/vendor/github.com/kylelemons/godebug/diff/diff_test.go
new file mode 100644
index 0000000..ebdd450
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/diff/diff_test.go
@@ -0,0 +1,228 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package diff
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func TestDiff(t *testing.T) {
+	tests := []struct {
+		desc   string
+		A, B   []string
+		chunks []Chunk
+	}{
+		{
+			desc: "nil",
+		},
+		{
+			desc: "empty",
+			A:    []string{},
+			B:    []string{},
+		},
+		{
+			desc: "same",
+			A:    []string{"foo"},
+			B:    []string{"foo"},
+		},
+		{
+			desc: "a empty",
+			A:    []string{},
+		},
+		{
+			desc: "b empty",
+			B:    []string{},
+		},
+		{
+			desc: "b nil",
+			A:    []string{"foo"},
+			chunks: []Chunk{
+				0: {Deleted: []string{"foo"}},
+			},
+		},
+		{
+			desc: "a nil",
+			B:    []string{"foo"},
+			chunks: []Chunk{
+				0: {Added: []string{"foo"}},
+			},
+		},
+		{
+			desc: "start with change",
+			A:    []string{"a", "b", "c"},
+			B:    []string{"A", "b", "c"},
+			chunks: []Chunk{
+				0: {Deleted: []string{"a"}},
+				1: {Added: []string{"A"}, Equal: []string{"b", "c"}},
+			},
+		},
+		{
+			desc: "constitution",
+			A: []string{
+				"We the People of the United States, in Order to form a more perfect Union,",
+				"establish Justice, insure domestic Tranquility, provide for the common defence,",
+				"and secure the Blessings of Liberty to ourselves",
+				"and our Posterity, do ordain and establish this Constitution for the United",
+				"States of America.",
+			},
+			B: []string{
+				"We the People of the United States, in Order to form a more perfect Union,",
+				"establish Justice, insure domestic Tranquility, provide for the common defence,",
+				"promote the general Welfare, and secure the Blessings of Liberty to ourselves",
+				"and our Posterity, do ordain and establish this Constitution for the United",
+				"States of America.",
+			},
+			chunks: []Chunk{
+				0: {
+					Equal: []string{
+						"We the People of the United States, in Order to form a more perfect Union,",
+						"establish Justice, insure domestic Tranquility, provide for the common defence,",
+					},
+				},
+				1: {
+					Deleted: []string{
+						"and secure the Blessings of Liberty to ourselves",
+					},
+				},
+				2: {
+					Added: []string{
+						"promote the general Welfare, and secure the Blessings of Liberty to ourselves",
+					},
+					Equal: []string{
+						"and our Posterity, do ordain and establish this Constitution for the United",
+						"States of America.",
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			got := DiffChunks(test.A, test.B)
+			if got, want := len(got), len(test.chunks); got != want {
+				t.Errorf("edit distance = %v, want %v", got-1, want-1)
+				return
+			}
+			for i := range got {
+				got, want := got[i], test.chunks[i]
+				if got, want := got.Added, want.Added; !reflect.DeepEqual(got, want) {
+					t.Errorf("chunks[%d]: Added = %v, want %v", i, got, want)
+				}
+				if got, want := got.Deleted, want.Deleted; !reflect.DeepEqual(got, want) {
+					t.Errorf("chunks[%d]: Deleted = %v, want %v", i, got, want)
+				}
+				if got, want := got.Equal, want.Equal; !reflect.DeepEqual(got, want) {
+					t.Errorf("chunks[%d]: Equal = %v, want %v", i, got, want)
+				}
+			}
+		})
+	}
+}
+
+func TestRender(t *testing.T) {
+	tests := []struct {
+		desc   string
+		chunks []Chunk
+		out    string
+	}{
+		{
+			desc: "ordering",
+			chunks: []Chunk{
+				{
+					Added:   []string{"1"},
+					Deleted: []string{"2"},
+					Equal:   []string{"3"},
+				},
+				{
+					Added:   []string{"4"},
+					Deleted: []string{"5"},
+				},
+			},
+			out: strings.TrimSpace(`
++1
+-2
+ 3
++4
+-5
+			`),
+		},
+		{
+			desc: "only_added",
+			chunks: []Chunk{
+				{
+					Added: []string{"1"},
+				},
+			},
+			out: strings.TrimSpace(`
++1
+			`),
+		},
+		{
+			desc: "only_deleted",
+			chunks: []Chunk{
+				{
+					Deleted: []string{"1"},
+				},
+			},
+			out: strings.TrimSpace(`
+-1
+			`),
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			if got, want := Render(test.chunks), test.out; got != want {
+				t.Errorf("Render(%q):", test.chunks)
+				t.Errorf("GOT\n%s", got)
+				t.Errorf("WANT\n%s", want)
+			}
+		})
+	}
+}
+
+func ExampleDiff() {
+	constitution := strings.TrimSpace(`
+We the People of the United States, in Order to form a more perfect Union,
+establish Justice, insure domestic Tranquility, provide for the common defence,
+promote the general Welfare, and secure the Blessings of Liberty to ourselves
+and our Posterity, do ordain and establish this Constitution for the United
+States of America.
+`)
+
+	got := strings.TrimSpace(`
+:wq
+We the People of the United States, in Order to form a more perfect Union,
+establish Justice, insure domestic Tranquility, provide for the common defence,
+and secure the Blessings of Liberty to ourselves
+and our Posterity, do ordain and establish this Constitution for the United
+States of America.
+`)
+
+	fmt.Println(Diff(got, constitution))
+
+	// Output:
+	// -:wq
+	//  We the People of the United States, in Order to form a more perfect Union,
+	//  establish Justice, insure domestic Tranquility, provide for the common defence,
+	// -and secure the Blessings of Liberty to ourselves
+	// +promote the general Welfare, and secure the Blessings of Liberty to ourselves
+	//  and our Posterity, do ordain and establish this Constitution for the United
+	//  States of America.
+}
diff --git a/vendor/github.com/kylelemons/godebug/go.mod b/vendor/github.com/kylelemons/godebug/go.mod
new file mode 100644
index 0000000..92e0120
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/go.mod
@@ -0,0 +1,3 @@
+module github.com/kylelemons/godebug
+
+go 1.11
diff --git a/vendor/github.com/kylelemons/godebug/pretty/doc.go b/vendor/github.com/kylelemons/godebug/pretty/doc.go
new file mode 100644
index 0000000..c7b4356
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/doc.go
@@ -0,0 +1,22 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pretty pretty-prints Go structures.
+//
+// This package uses reflection to examine a Go value and can
+// print out in a nice, aligned fashion.  It supports three
+// modes (normal, compact, and extended) for advanced use.
+//
+// See the Reflect and Print examples for what the output looks like.
+package pretty
diff --git a/vendor/github.com/kylelemons/godebug/pretty/examples_test.go b/vendor/github.com/kylelemons/godebug/pretty/examples_test.go
new file mode 100644
index 0000000..8578b95
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/examples_test.go
@@ -0,0 +1,373 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty_test
+
+import (
+	"fmt"
+	"net"
+	"reflect"
+
+	"github.com/kylelemons/godebug/pretty"
+)
+
+func ExampleConfig_Sprint() {
+	type Pair [2]int
+	type Map struct {
+		Name      string
+		Players   map[string]Pair
+		Obstacles map[Pair]string
+	}
+
+	m := Map{
+		Name: "Rock Creek",
+		Players: map[string]Pair{
+			"player1": {1, 3},
+			"player2": {0, -1},
+		},
+		Obstacles: map[Pair]string{
+			Pair{0, 0}: "rock",
+			Pair{2, 1}: "pond",
+			Pair{1, 1}: "stream",
+			Pair{0, 1}: "stream",
+		},
+	}
+
+	// Specific output formats
+	compact := &pretty.Config{
+		Compact: true,
+	}
+	diffable := &pretty.Config{
+		Diffable: true,
+	}
+
+	// Print out a summary
+	fmt.Printf("Players: %s\n", compact.Sprint(m.Players))
+
+	// Print diffable output
+	fmt.Printf("Map State:\n%s", diffable.Sprint(m))
+
+	// Output:
+	// Players: {player1:[1,3],player2:[0,-1]}
+	// Map State:
+	// {
+	//  Name: "Rock Creek",
+	//  Players: {
+	//   player1: [
+	//    1,
+	//    3,
+	//   ],
+	//   player2: [
+	//    0,
+	//    -1,
+	//   ],
+	//  },
+	//  Obstacles: {
+	//   [0,0]: "rock",
+	//   [0,1]: "stream",
+	//   [1,1]: "stream",
+	//   [2,1]: "pond",
+	//  },
+	// }
+}
+
+func ExampleConfig_fmtFormatter() {
+	pretty.DefaultFormatter[reflect.TypeOf(&net.IPNet{})] = fmt.Sprint
+	pretty.DefaultFormatter[reflect.TypeOf(net.HardwareAddr{})] = fmt.Sprint
+	pretty.Print(&net.IPNet{
+		IP:   net.IPv4(192, 168, 1, 100),
+		Mask: net.CIDRMask(24, 32),
+	})
+	pretty.Print(net.HardwareAddr{1, 2, 3, 4, 5, 6})
+
+	// Output:
+	// 192.168.1.100/24
+	// 01:02:03:04:05:06
+}
+
+func ExampleConfig_customFormatter() {
+	pretty.DefaultFormatter[reflect.TypeOf(&net.IPNet{})] = func(n *net.IPNet) string {
+		return fmt.Sprintf("CIDR=%s", n)
+	}
+	pretty.Print(&net.IPNet{
+		IP:   net.IPv4(192, 168, 1, 100),
+		Mask: net.CIDRMask(24, 32),
+	})
+
+	// Output:
+	// CIDR=192.168.1.100/24
+}
+
+func ExamplePrint() {
+	type ShipManifest struct {
+		Name     string
+		Crew     map[string]string
+		Androids int
+		Stolen   bool
+	}
+
+	manifest := &ShipManifest{
+		Name: "Spaceship Heart of Gold",
+		Crew: map[string]string{
+			"Zaphod Beeblebrox": "Galactic President",
+			"Trillian":          "Human",
+			"Ford Prefect":      "A Hoopy Frood",
+			"Arthur Dent":       "Along for the Ride",
+		},
+		Androids: 1,
+		Stolen:   true,
+	}
+
+	pretty.Print(manifest)
+
+	// Output:
+	// {Name:     "Spaceship Heart of Gold",
+	//  Crew:     {Arthur Dent:       "Along for the Ride",
+	//             Ford Prefect:      "A Hoopy Frood",
+	//             Trillian:          "Human",
+	//             Zaphod Beeblebrox: "Galactic President"},
+	//  Androids: 1,
+	//  Stolen:   true}
+}
+
+var t = struct {
+	Errorf func(string, ...interface{})
+}{
+	Errorf: func(format string, args ...interface{}) {
+		fmt.Println(fmt.Sprintf(format, args...) + "\n")
+	},
+}
+
+func ExampleCompare_testing() {
+	// Code under test:
+
+	type ShipManifest struct {
+		Name     string
+		Crew     map[string]string
+		Androids int
+		Stolen   bool
+	}
+
+	// AddCrew tries to add the given crewmember to the manifest.
+	AddCrew := func(m *ShipManifest, name, title string) {
+		if m.Crew == nil {
+			m.Crew = make(map[string]string)
+		}
+		m.Crew[title] = name
+	}
+
+	// Test function:
+	tests := []struct {
+		desc        string
+		before      *ShipManifest
+		name, title string
+		after       *ShipManifest
+	}{
+		{
+			desc:   "add first",
+			before: &ShipManifest{},
+			name:   "Zaphod Beeblebrox",
+			title:  "Galactic President",
+			after: &ShipManifest{
+				Crew: map[string]string{
+					"Zaphod Beeblebrox": "Galactic President",
+				},
+			},
+		},
+		{
+			desc: "add another",
+			before: &ShipManifest{
+				Crew: map[string]string{
+					"Zaphod Beeblebrox": "Galactic President",
+				},
+			},
+			name:  "Trillian",
+			title: "Human",
+			after: &ShipManifest{
+				Crew: map[string]string{
+					"Zaphod Beeblebrox": "Galactic President",
+					"Trillian":          "Human",
+				},
+			},
+		},
+		{
+			desc: "overwrite",
+			before: &ShipManifest{
+				Crew: map[string]string{
+					"Zaphod Beeblebrox": "Galactic President",
+				},
+			},
+			name:  "Zaphod Beeblebrox",
+			title: "Just this guy, you know?",
+			after: &ShipManifest{
+				Crew: map[string]string{
+					"Zaphod Beeblebrox": "Just this guy, you know?",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		AddCrew(test.before, test.name, test.title)
+		if diff := pretty.Compare(test.before, test.after); diff != "" {
+			t.Errorf("%s: post-AddCrew diff: (-got +want)\n%s", test.desc, diff)
+		}
+	}
+
+	// Output:
+	// add first: post-AddCrew diff: (-got +want)
+	//  {
+	//   Name: "",
+	//   Crew: {
+	// -  Galactic President: "Zaphod Beeblebrox",
+	// +  Zaphod Beeblebrox: "Galactic President",
+	//   },
+	//   Androids: 0,
+	//   Stolen: false,
+	//  }
+	//
+	// add another: post-AddCrew diff: (-got +want)
+	//  {
+	//   Name: "",
+	//   Crew: {
+	// -  Human: "Trillian",
+	// +  Trillian: "Human",
+	//    Zaphod Beeblebrox: "Galactic President",
+	//   },
+	//   Androids: 0,
+	//   Stolen: false,
+	//  }
+	//
+	// overwrite: post-AddCrew diff: (-got +want)
+	//  {
+	//   Name: "",
+	//   Crew: {
+	// -  Just this guy, you know?: "Zaphod Beeblebrox",
+	// -  Zaphod Beeblebrox: "Galactic President",
+	// +  Zaphod Beeblebrox: "Just this guy, you know?",
+	//   },
+	//   Androids: 0,
+	//   Stolen: false,
+	//  }
+}
+
+func ExampleCompare_debugging() {
+	type ShipManifest struct {
+		Name     string
+		Crew     map[string]string
+		Androids int
+		Stolen   bool
+	}
+
+	reported := &ShipManifest{
+		Name: "Spaceship Heart of Gold",
+		Crew: map[string]string{
+			"Zaphod Beeblebrox": "Galactic President",
+			"Trillian":          "Human",
+			"Ford Prefect":      "A Hoopy Frood",
+			"Arthur Dent":       "Along for the Ride",
+		},
+		Androids: 1,
+		Stolen:   true,
+	}
+
+	expected := &ShipManifest{
+		Name: "Spaceship Heart of Gold",
+		Crew: map[string]string{
+			"Trillian":      "Human",
+			"Rowan Artosok": "Captain",
+		},
+		Androids: 1,
+		Stolen:   false,
+	}
+
+	fmt.Println(pretty.Compare(reported, expected))
+	// Output:
+	//  {
+	//   Name: "Spaceship Heart of Gold",
+	//   Crew: {
+	// -  Arthur Dent: "Along for the Ride",
+	// -  Ford Prefect: "A Hoopy Frood",
+	// +  Rowan Artosok: "Captain",
+	//    Trillian: "Human",
+	// -  Zaphod Beeblebrox: "Galactic President",
+	//   },
+	//   Androids: 1,
+	// - Stolen: true,
+	// + Stolen: false,
+	//  }
+}
+
+type ListNode struct {
+	Value int
+	Next  *ListNode
+}
+
+func circular(nodes int) *ListNode {
+	final := &ListNode{
+		Value: nodes,
+	}
+	final.Next = final
+
+	recent := final
+	for i := nodes - 1; i > 0; i-- {
+		n := &ListNode{
+			Value: i,
+			Next:  recent,
+		}
+		final.Next = n
+		recent = n
+	}
+	return recent
+}
+
+func ExamplePrint_withCycles() {
+	pretty.CycleTracker.Print(circular(3))
+
+	// Output:
+	// <#1> {
+	//  Value: 1,
+	//  Next: {
+	//   Value: 2,
+	//   Next: {
+	//    Value: 3,
+	//    Next: <see #1>,
+	//   },
+	//  },
+	// }
+}
+
+func ExampleCompare_withCycles() {
+	got, want := circular(3), circular(3)
+
+	// Make the got one broken
+	got.Next.Next.Next = got.Next
+
+	fmt.Printf("Diff: (-got +want)\n%s", pretty.CycleTracker.Compare(got, want))
+
+	// Output:
+	// Diff: (-got +want)
+	// -{
+	// +<#1> {
+	//   Value: 1,
+	// - Next: <#1> {
+	// + Next: {
+	//    Value: 2,
+	//    Next: {
+	//     Value: 3,
+	//     Next: <see #1>,
+	//    },
+	//   },
+	//  }
+}
diff --git a/vendor/github.com/kylelemons/godebug/pretty/public.go b/vendor/github.com/kylelemons/godebug/pretty/public.go
new file mode 100644
index 0000000..fbc5d7a
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/public.go
@@ -0,0 +1,188 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net"
+	"reflect"
+	"time"
+
+	"github.com/kylelemons/godebug/diff"
+)
+
+// A Config represents optional configuration parameters for formatting.
+//
+// Some options, notably ShortList, dramatically increase the overhead
+// of pretty-printing a value.
+type Config struct {
+	// Verbosity options
+	Compact  bool // One-line output. Overrides Diffable.
+	Diffable bool // Adds extra newlines for more easily diffable output.
+
+	// Field and value options
+	IncludeUnexported   bool // Include unexported fields in output
+	PrintStringers      bool // Call String on a fmt.Stringer
+	PrintTextMarshalers bool // Call MarshalText on an encoding.TextMarshaler
+	SkipZeroFields      bool // Skip struct fields that have a zero value.
+
+	// Output transforms
+	ShortList int // Maximum character length for short lists if nonzero.
+
+	// Type-specific overrides
+	//
+	// Formatter maps a type to a function that will provide a one-line string
+	// representation of the input value.  Conceptually:
+	//   Formatter[reflect.TypeOf(v)](v) = "v as a string"
+	//
+	// Note that the first argument need not explicitly match the type, it must
+	// merely be callable with it.
+	//
+	// When processing an input value, if its type exists as a key in Formatter:
+	//   1) If the value is nil, no stringification is performed.
+	//      This allows overriding of PrintStringers and PrintTextMarshalers.
+	//   2) The value will be called with the input as its only argument.
+	//      The function must return a string as its first return value.
+	//
+	// In addition to func literals, two common values for this will be:
+	//   fmt.Sprint        (function) func Sprint(...interface{}) string
+	//   Type.String         (method) func (Type) String() string
+	//
+	// Note that neither of these work if the String method is a pointer
+	// method and the input will be provided as a value.  In that case,
+	// use a function that calls .String on the formal value parameter.
+	Formatter map[reflect.Type]interface{}
+
+	// If TrackCycles is enabled, pretty will detect and track
+	// self-referential structures. If a self-referential structure (aka a
+	// "recursive" value) is detected, numbered placeholders will be emitted.
+	//
+	// Pointer tracking is disabled by default for performance reasons.
+	TrackCycles bool
+}
+
+// Default Config objects
+var (
+	// DefaultFormatter is the default set of overrides for stringification.
+	DefaultFormatter = map[reflect.Type]interface{}{
+		reflect.TypeOf(time.Time{}):          fmt.Sprint,
+		reflect.TypeOf(net.IP{}):             fmt.Sprint,
+		reflect.TypeOf((*error)(nil)).Elem(): fmt.Sprint,
+	}
+
+	// CompareConfig is the default configuration used for Compare.
+	CompareConfig = &Config{
+		Diffable:          true,
+		IncludeUnexported: true,
+		Formatter:         DefaultFormatter,
+	}
+
+	// DefaultConfig is the default configuration used for all other top-level functions.
+	DefaultConfig = &Config{
+		Formatter: DefaultFormatter,
+	}
+
+	// CycleTracker is a convenience config for formatting and comparing recursive structures.
+	CycleTracker = &Config{
+		Diffable:    true,
+		Formatter:   DefaultFormatter,
+		TrackCycles: true,
+	}
+)
+
+func (cfg *Config) fprint(buf *bytes.Buffer, vals ...interface{}) {
+	ref := &reflector{
+		Config: cfg,
+	}
+	if cfg.TrackCycles {
+		ref.pointerTracker = new(pointerTracker)
+	}
+	for i, val := range vals {
+		if i > 0 {
+			buf.WriteByte('\n')
+		}
+		newFormatter(cfg, buf).write(ref.val2node(reflect.ValueOf(val)))
+	}
+}
+
+// Print writes the DefaultConfig representation of the given values to standard output.
+func Print(vals ...interface{}) {
+	DefaultConfig.Print(vals...)
+}
+
+// Print writes the configured presentation of the given values to standard output.
+func (cfg *Config) Print(vals ...interface{}) {
+	fmt.Println(cfg.Sprint(vals...))
+}
+
+// Sprint returns a string representation of the given value according to the DefaultConfig.
+func Sprint(vals ...interface{}) string {
+	return DefaultConfig.Sprint(vals...)
+}
+
+// Sprint returns a string representation of the given value according to cfg.
+func (cfg *Config) Sprint(vals ...interface{}) string {
+	buf := new(bytes.Buffer)
+	cfg.fprint(buf, vals...)
+	return buf.String()
+}
+
+// Fprint writes the representation of the given value to the writer according to the DefaultConfig.
+func Fprint(w io.Writer, vals ...interface{}) (n int64, err error) {
+	return DefaultConfig.Fprint(w, vals...)
+}
+
+// Fprint writes the representation of the given value to the writer according to the cfg.
+func (cfg *Config) Fprint(w io.Writer, vals ...interface{}) (n int64, err error) {
+	buf := new(bytes.Buffer)
+	cfg.fprint(buf, vals...)
+	return buf.WriteTo(w)
+}
+
+// Compare returns a string containing a line-by-line unified diff of the
+// values in a and b, using the CompareConfig.
+//
+// Each line in the output is prefixed with '+', '-', or ' ' to indicate which
+// side it's from. Lines from the a side are marked with '-', lines from the
+// b side are marked with '+' and lines that are the same on both sides are
+// marked with ' '.
+//
+// The comparison is based on the intentionally-untyped output of Print, and as
+// such this comparison is pretty forviving.  In particular, if the types of or
+// types within in a and b are different but have the same representation,
+// Compare will not indicate any differences between them.
+func Compare(a, b interface{}) string {
+	return CompareConfig.Compare(a, b)
+}
+
+// Compare returns a string containing a line-by-line unified diff of the
+// values in got and want according to the cfg.
+//
+// Each line in the output is prefixed with '+', '-', or ' ' to indicate which
+// side it's from. Lines from the a side are marked with '-', lines from the
+// b side are marked with '+' and lines that are the same on both sides are
+// marked with ' '.
+//
+// The comparison is based on the intentionally-untyped output of Print, and as
+// such this comparison is pretty forviving.  In particular, if the types of or
+// types within in a and b are different but have the same representation,
+// Compare will not indicate any differences between them.
+func (cfg *Config) Compare(a, b interface{}) string {
+	diffCfg := *cfg
+	diffCfg.Diffable = true
+	return diff.Diff(cfg.Sprint(a), cfg.Sprint(b))
+}
diff --git a/vendor/github.com/kylelemons/godebug/pretty/public_test.go b/vendor/github.com/kylelemons/godebug/pretty/public_test.go
new file mode 100644
index 0000000..b25a9ca
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/public_test.go
@@ -0,0 +1,155 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty
+
+import (
+	"testing"
+	"time"
+)
+
+func TestDiff(t *testing.T) {
+	type example struct {
+		Name    string
+		Age     int
+		Friends []string
+	}
+
+	tests := []struct {
+		desc      string
+		got, want interface{}
+		diff      string
+	}{
+		{
+			desc: "basic struct",
+			got: example{
+				Name: "Zaphd",
+				Age:  42,
+				Friends: []string{
+					"Ford Prefect",
+					"Trillian",
+					"Marvin",
+				},
+			},
+			want: example{
+				Name: "Zaphod",
+				Age:  42,
+				Friends: []string{
+					"Ford Prefect",
+					"Trillian",
+				},
+			},
+			diff: ` {
+- Name: "Zaphd",
++ Name: "Zaphod",
+  Age: 42,
+  Friends: [
+   "Ford Prefect",
+   "Trillian",
+-  "Marvin",
+  ],
+ }`,
+		},
+	}
+
+	for _, test := range tests {
+		if got, want := Compare(test.got, test.want), test.diff; got != want {
+			t.Errorf("%s:", test.desc)
+			t.Errorf("  got:  %q", got)
+			t.Errorf("  want: %q", want)
+		}
+	}
+}
+
+func TestSkipZeroFields(t *testing.T) {
+	type example struct {
+		Name    string
+		Species string
+		Age     int
+		Friends []string
+	}
+
+	tests := []struct {
+		desc      string
+		got, want interface{}
+		diff      string
+	}{
+		{
+			desc: "basic struct",
+			got: example{
+				Name:    "Zaphd",
+				Species: "Betelgeusian",
+				Age:     42,
+			},
+			want: example{
+				Name:    "Zaphod",
+				Species: "Betelgeusian",
+				Age:     42,
+				Friends: []string{
+					"Ford Prefect",
+					"Trillian",
+					"",
+				},
+			},
+			diff: ` {
+- Name: "Zaphd",
++ Name: "Zaphod",
+  Species: "Betelgeusian",
+  Age: 42,
++ Friends: [
++  "Ford Prefect",
++  "Trillian",
++  "",
++ ],
+ }`,
+		},
+	}
+
+	cfg := *CompareConfig
+	cfg.SkipZeroFields = true
+
+	for _, test := range tests {
+		if got, want := cfg.Compare(test.got, test.want), test.diff; got != want {
+			t.Errorf("%s:", test.desc)
+			t.Errorf("  got:  %q", got)
+			t.Errorf("  want: %q", want)
+		}
+	}
+}
+
+func TestRegressions(t *testing.T) {
+	tests := []struct {
+		issue  string
+		config *Config
+		value  interface{}
+		want   string
+	}{
+		{
+			issue: "kylelemons/godebug#13",
+			config: &Config{
+				PrintStringers: true,
+			},
+			value: struct{ Day *time.Weekday }{},
+			want:  "{Day: nil}",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.issue, func(t *testing.T) {
+			if got, want := test.config.Sprint(test.value), test.want; got != want {
+				t.Errorf("%#v.Sprint(%#v) = %q, want %q", test.config, test.value, got, want)
+			}
+		})
+	}
+}
diff --git a/vendor/github.com/kylelemons/godebug/pretty/reflect.go b/vendor/github.com/kylelemons/godebug/pretty/reflect.go
new file mode 100644
index 0000000..214e622
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/reflect.go
@@ -0,0 +1,255 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty
+
+import (
+	"encoding"
+	"fmt"
+	"reflect"
+	"sort"
+)
+
+func isZeroVal(val reflect.Value) bool {
+	if !val.CanInterface() {
+		return false
+	}
+	z := reflect.Zero(val.Type()).Interface()
+	return reflect.DeepEqual(val.Interface(), z)
+}
+
+// pointerTracker is a helper for tracking pointer chasing to detect cycles.
+type pointerTracker struct {
+	addrs map[uintptr]int // addr[address] = seen count
+
+	lastID int
+	ids    map[uintptr]int // ids[address] = id
+}
+
+// track tracks following a reference (pointer, slice, map, etc).  Every call to
+// track should be paired with a call to untrack.
+func (p *pointerTracker) track(ptr uintptr) {
+	if p.addrs == nil {
+		p.addrs = make(map[uintptr]int)
+	}
+	p.addrs[ptr]++
+}
+
+// untrack registers that we have backtracked over the reference to the pointer.
+func (p *pointerTracker) untrack(ptr uintptr) {
+	p.addrs[ptr]--
+	if p.addrs[ptr] == 0 {
+		delete(p.addrs, ptr)
+	}
+}
+
+// seen returns whether the pointer was previously seen along this path.
+func (p *pointerTracker) seen(ptr uintptr) bool {
+	_, ok := p.addrs[ptr]
+	return ok
+}
+
+// keep allocates an ID for the given address and returns it.
+func (p *pointerTracker) keep(ptr uintptr) int {
+	if p.ids == nil {
+		p.ids = make(map[uintptr]int)
+	}
+	if _, ok := p.ids[ptr]; !ok {
+		p.lastID++
+		p.ids[ptr] = p.lastID
+	}
+	return p.ids[ptr]
+}
+
+// id returns the ID for the given address.
+func (p *pointerTracker) id(ptr uintptr) (int, bool) {
+	if p.ids == nil {
+		p.ids = make(map[uintptr]int)
+	}
+	id, ok := p.ids[ptr]
+	return id, ok
+}
+
+// reflector adds local state to the recursive reflection logic.
+type reflector struct {
+	*Config
+	*pointerTracker
+}
+
+// follow handles following a possiblly-recursive reference to the given value
+// from the given ptr address.
+func (r *reflector) follow(ptr uintptr, val reflect.Value) node {
+	if r.pointerTracker == nil {
+		// Tracking disabled
+		return r.val2node(val)
+	}
+
+	// If a parent already followed this, emit a reference marker
+	if r.seen(ptr) {
+		id := r.keep(ptr)
+		return ref{id}
+	}
+
+	// Track the pointer we're following while on this recursive branch
+	r.track(ptr)
+	defer r.untrack(ptr)
+	n := r.val2node(val)
+
+	// If the recursion used this ptr, wrap it with a target marker
+	if id, ok := r.id(ptr); ok {
+		return target{id, n}
+	}
+
+	// Otherwise, return the node unadulterated
+	return n
+}
+
+func (r *reflector) val2node(val reflect.Value) (ret node) {
+	if !val.IsValid() {
+		return rawVal("nil")
+	}
+
+	if val.CanInterface() {
+		// Detect panics in calling functions on nil pointers.
+		//
+		// We still want to call them, as it's possible that a nil value is
+		// valid for the particular type.
+		//
+		// If we detect a panic, just return raw nil.
+		if val.Kind() == reflect.Ptr && val.IsNil() {
+			defer func() {
+				if r := recover(); r != nil {
+					ret = rawVal("nil")
+				}
+			}()
+		}
+
+		v := val.Interface()
+		if formatter, ok := r.Formatter[val.Type()]; ok {
+			if formatter != nil {
+				res := reflect.ValueOf(formatter).Call([]reflect.Value{val})
+				return rawVal(res[0].Interface().(string))
+			}
+		} else {
+			if s, ok := v.(fmt.Stringer); ok && r.PrintStringers {
+				return stringVal(s.String())
+			}
+			if t, ok := v.(encoding.TextMarshaler); ok && r.PrintTextMarshalers {
+				if raw, err := t.MarshalText(); err == nil { // if NOT an error
+					return stringVal(string(raw))
+				}
+			}
+		}
+	}
+
+	switch kind := val.Kind(); kind {
+	case reflect.Ptr:
+		if val.IsNil() {
+			return rawVal("nil")
+		}
+		return r.follow(val.Pointer(), val.Elem())
+	case reflect.Interface:
+		if val.IsNil() {
+			return rawVal("nil")
+		}
+		return r.val2node(val.Elem())
+	case reflect.String:
+		return stringVal(val.String())
+	case reflect.Slice:
+		n := list{}
+		length := val.Len()
+		ptr := val.Pointer()
+		for i := 0; i < length; i++ {
+			n = append(n, r.follow(ptr, val.Index(i)))
+		}
+		return n
+	case reflect.Array:
+		n := list{}
+		length := val.Len()
+		for i := 0; i < length; i++ {
+			n = append(n, r.val2node(val.Index(i)))
+		}
+		return n
+	case reflect.Map:
+		// Extract the keys and sort them for stable iteration
+		keys := val.MapKeys()
+		pairs := make([]mapPair, 0, len(keys))
+		for _, key := range keys {
+			pairs = append(pairs, mapPair{
+				key:   new(formatter).compactString(r.val2node(key)), // can't be cyclic
+				value: val.MapIndex(key),
+			})
+		}
+		sort.Sort(byKey(pairs))
+
+		// Process the keys into the final representation
+		ptr, n := val.Pointer(), keyvals{}
+		for _, pair := range pairs {
+			n = append(n, keyval{
+				key: pair.key,
+				val: r.follow(ptr, pair.value),
+			})
+		}
+		return n
+	case reflect.Struct:
+		n := keyvals{}
+		typ := val.Type()
+		fields := typ.NumField()
+		for i := 0; i < fields; i++ {
+			sf := typ.Field(i)
+			if !r.IncludeUnexported && sf.PkgPath != "" {
+				continue
+			}
+			field := val.Field(i)
+			if r.SkipZeroFields && isZeroVal(field) {
+				continue
+			}
+			n = append(n, keyval{sf.Name, r.val2node(field)})
+		}
+		return n
+	case reflect.Bool:
+		if val.Bool() {
+			return rawVal("true")
+		}
+		return rawVal("false")
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return rawVal(fmt.Sprintf("%d", val.Int()))
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return rawVal(fmt.Sprintf("%d", val.Uint()))
+	case reflect.Uintptr:
+		return rawVal(fmt.Sprintf("0x%X", val.Uint()))
+	case reflect.Float32, reflect.Float64:
+		return rawVal(fmt.Sprintf("%v", val.Float()))
+	case reflect.Complex64, reflect.Complex128:
+		return rawVal(fmt.Sprintf("%v", val.Complex()))
+	}
+
+	// Fall back to the default %#v if we can
+	if val.CanInterface() {
+		return rawVal(fmt.Sprintf("%#v", val.Interface()))
+	}
+
+	return rawVal(val.String())
+}
+
+type mapPair struct {
+	key   string
+	value reflect.Value
+}
+
+type byKey []mapPair
+
+func (v byKey) Len() int           { return len(v) }
+func (v byKey) Swap(i, j int)      { v[i], v[j] = v[j], v[i] }
+func (v byKey) Less(i, j int) bool { return v[i].key < v[j].key }
diff --git a/vendor/github.com/kylelemons/godebug/pretty/reflect_test.go b/vendor/github.com/kylelemons/godebug/pretty/reflect_test.go
new file mode 100644
index 0000000..1a67d88
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/reflect_test.go
@@ -0,0 +1,456 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty
+
+import (
+	"fmt"
+	"net"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestVal2nodeDefault(t *testing.T) {
+	err := fmt.Errorf("err")
+	var errNil error
+
+	tests := []struct {
+		desc string
+		raw  interface{}
+		want node
+	}{
+		{
+			desc: "nil",
+			raw:  nil,
+			want: rawVal("nil"),
+		},
+		{
+			desc: "nil ptr",
+			raw:  (*int)(nil),
+			want: rawVal("nil"),
+		},
+		{
+			desc: "nil slice",
+			raw:  []string(nil),
+			want: list{},
+		},
+		{
+			desc: "nil map",
+			raw:  map[string]string(nil),
+			want: keyvals{},
+		},
+		{
+			desc: "string",
+			raw:  "zaphod",
+			want: stringVal("zaphod"),
+		},
+		{
+			desc: "slice",
+			raw:  []string{"a", "b"},
+			want: list{stringVal("a"), stringVal("b")},
+		},
+		{
+			desc: "map",
+			raw: map[string]string{
+				"zaphod": "beeblebrox",
+				"ford":   "prefect",
+			},
+			want: keyvals{
+				{"ford", stringVal("prefect")},
+				{"zaphod", stringVal("beeblebrox")},
+			},
+		},
+		{
+			desc: "map of [2]int",
+			raw: map[[2]int]string{
+				[2]int{-1, 2}: "school",
+				[2]int{0, 0}:  "origin",
+				[2]int{1, 3}:  "home",
+			},
+			want: keyvals{
+				{"[-1,2]", stringVal("school")},
+				{"[0,0]", stringVal("origin")},
+				{"[1,3]", stringVal("home")},
+			},
+		},
+		{
+			desc: "struct",
+			raw:  struct{ Zaphod, Ford string }{"beeblebrox", "prefect"},
+			want: keyvals{
+				{"Zaphod", stringVal("beeblebrox")},
+				{"Ford", stringVal("prefect")},
+			},
+		},
+		{
+			desc: "int",
+			raw:  3,
+			want: rawVal("3"),
+		},
+		{
+			desc: "time.Time",
+			raw:  time.Unix(1257894000, 0).UTC(),
+			want: rawVal("2009-11-10 23:00:00 +0000 UTC"),
+		},
+		{
+			desc: "net.IP",
+			raw:  net.IPv4(127, 0, 0, 1),
+			want: rawVal("127.0.0.1"),
+		},
+		{
+			desc: "error",
+			raw:  &err,
+			want: rawVal("err"),
+		},
+		{
+			desc: "nil error",
+			raw:  &errNil,
+			want: rawVal("<nil>"),
+		},
+	}
+
+	for _, test := range tests {
+		ref := &reflector{
+			Config: DefaultConfig,
+		}
+		if got, want := ref.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) {
+			t.Errorf("%s: got %#v, want %#v", test.desc, got, want)
+		}
+	}
+}
+
+func TestVal2node(t *testing.T) {
+	tests := []struct {
+		desc string
+		raw  interface{}
+		cfg  *Config
+		want node
+	}{
+		{
+			desc: "struct default",
+			raw:  struct{ Zaphod, Ford, foo string }{"beeblebrox", "prefect", "BAD"},
+			cfg:  DefaultConfig,
+			want: keyvals{
+				{"Zaphod", stringVal("beeblebrox")},
+				{"Ford", stringVal("prefect")},
+			},
+		},
+		{
+			desc: "struct with IncludeUnexported",
+			raw:  struct{ Zaphod, Ford, foo string }{"beeblebrox", "prefect", "GOOD"},
+			cfg: &Config{
+				IncludeUnexported: true,
+			},
+			want: keyvals{
+				{"Zaphod", stringVal("beeblebrox")},
+				{"Ford", stringVal("prefect")},
+				{"foo", stringVal("GOOD")},
+			},
+		},
+		{
+			desc: "time default",
+			raw:  struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()},
+			cfg:  DefaultConfig,
+			want: keyvals{
+				{"Date", rawVal("2009-02-13 23:31:30 +0000 UTC")},
+			},
+		},
+		{
+			desc: "time with nil Formatter",
+			raw:  struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()},
+			cfg: &Config{
+				PrintStringers: true,
+				Formatter: map[reflect.Type]interface{}{
+					reflect.TypeOf(time.Time{}): nil,
+				},
+			},
+			want: keyvals{
+				{"Date", keyvals{}},
+			},
+		},
+		{
+			desc: "time with PrintTextMarshalers",
+			raw:  struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()},
+			cfg: &Config{
+				PrintTextMarshalers: true,
+			},
+			want: keyvals{
+				{"Date", stringVal("2009-02-13T23:31:30Z")},
+			},
+		},
+		{
+			desc: "time with PrintStringers",
+			raw:  struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()},
+			cfg: &Config{
+				PrintStringers: true,
+			},
+			want: keyvals{
+				{"Date", stringVal("2009-02-13 23:31:30 +0000 UTC")},
+			},
+		},
+		{
+			desc: "nil with PrintStringers",
+			raw:  struct{ Date *time.Time }{},
+			cfg: &Config{
+				PrintStringers: true,
+			},
+			want: keyvals{
+				{"Date", rawVal("nil")},
+			},
+		},
+		{
+			desc: "nilIsFine with PrintStringers",
+			raw:  struct{ V *nilIsFine }{},
+			cfg: &Config{
+				PrintStringers: true,
+			},
+			want: keyvals{
+				{"V", stringVal("<nil is fine>")},
+			},
+		},
+		{
+			desc: "nilIsFine non-nil with PrintStringers",
+			raw:  struct{ V *nilIsFine }{V: new(nilIsFine)},
+			cfg: &Config{
+				PrintStringers: true,
+			},
+			want: keyvals{
+				{"V", stringVal("<not nil is fine>")},
+			},
+		},
+		{
+			desc: "circular list",
+			raw:  circular(3),
+			cfg:  CycleTracker,
+			want: target{1, keyvals{
+				{"Value", rawVal("1")},
+				{"Next", keyvals{
+					{"Value", rawVal("2")},
+					{"Next", keyvals{
+						{"Value", rawVal("3")},
+						{"Next", ref{1}},
+					}},
+				}},
+			}},
+		},
+		{
+			desc: "self referential maps",
+			raw:  selfRef(),
+			cfg:  CycleTracker,
+			want: target{1, keyvals{
+				{"ID", rawVal("1")},
+				{"Child", keyvals{
+					{"2", target{2, keyvals{
+						{"ID", rawVal("2")},
+						{"Child", keyvals{
+							{"3", target{3, keyvals{
+								{"ID", rawVal("3")},
+								{"Child", keyvals{
+									{"1", ref{1}},
+									{"2", ref{2}},
+									{"3", ref{3}},
+								}},
+							}}},
+						}},
+					}}},
+				}},
+			}},
+		},
+		{
+			desc: "maps of cycles",
+			raw: map[string]*ListNode{
+				"1. one":   circular(1),
+				"2. two":   circular(2),
+				"3. three": circular(1),
+			},
+			cfg: CycleTracker,
+			want: keyvals{
+				{"1. one", target{1, keyvals{
+					{"Value", rawVal("1")},
+					{"Next", ref{1}},
+				}}},
+				{"2. two", target{2, keyvals{
+					{"Value", rawVal("1")},
+					{"Next", keyvals{
+						{"Value", rawVal("2")},
+						{"Next", ref{2}},
+					}},
+				}}},
+				{"3. three", target{3, keyvals{
+					{"Value", rawVal("1")},
+					{"Next", ref{3}},
+				}}},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ref := &reflector{
+				Config: test.cfg,
+			}
+			if test.cfg.TrackCycles {
+				ref.pointerTracker = new(pointerTracker)
+			}
+			if got, want := ref.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) {
+				t.Errorf(" got %#v", got)
+				t.Errorf("want %#v", want)
+				t.Errorf("Diff: (-got +want)\n%s", Compare(got, want))
+			}
+		})
+	}
+}
+
+type ListNode struct {
+	Value int
+	Next  *ListNode
+}
+
+func circular(nodes int) *ListNode {
+	final := &ListNode{
+		Value: nodes,
+	}
+	final.Next = final
+
+	recent := final
+	for i := nodes - 1; i > 0; i-- {
+		n := &ListNode{
+			Value: i,
+			Next:  recent,
+		}
+		final.Next = n
+		recent = n
+	}
+	return recent
+}
+
+type SelfReferential struct {
+	ID    int
+	Child map[int]*SelfReferential
+}
+
+func selfRef() *SelfReferential {
+	sr1 := &SelfReferential{
+		ID:    1,
+		Child: make(map[int]*SelfReferential),
+	}
+	sr2 := &SelfReferential{
+		ID:    2,
+		Child: make(map[int]*SelfReferential),
+	}
+	sr3 := &SelfReferential{
+		ID:    3,
+		Child: make(map[int]*SelfReferential),
+	}
+
+	// Build a cycle
+	sr1.Child[2] = sr2
+	sr2.Child[3] = sr3
+	sr3.Child[1] = sr1
+
+	// Throw in some other stuff for funzies
+	sr3.Child[2] = sr2
+	sr3.Child[3] = sr3
+	return sr1
+}
+
+func BenchmarkVal2node(b *testing.B) {
+	benchmarks := []struct {
+		desc string
+		cfg  *Config
+		raw  interface{}
+	}{
+		{
+			desc: "struct",
+			cfg:  DefaultConfig,
+			raw:  struct{ Zaphod, Ford string }{"beeblebrox", "prefect"},
+		},
+		{
+			desc: "map",
+			cfg:  DefaultConfig,
+			raw: map[[2]int]string{
+				[2]int{-1, 2}: "school",
+				[2]int{0, 0}:  "origin",
+				[2]int{1, 3}:  "home",
+			},
+		},
+		{
+			desc: "track/struct",
+			cfg:  CycleTracker,
+			raw:  struct{ Zaphod, Ford string }{"beeblebrox", "prefect"},
+		},
+		{
+			desc: "track/map",
+			cfg:  CycleTracker,
+			raw: map[[2]int]string{
+				[2]int{-1, 2}: "school",
+				[2]int{0, 0}:  "origin",
+				[2]int{1, 3}:  "home",
+			},
+		},
+		{
+			desc: "circlist/small",
+			cfg:  CycleTracker,
+			raw:  circular(3),
+		},
+		{
+			desc: "circlist/med",
+			cfg:  CycleTracker,
+			raw:  circular(300),
+		},
+		{
+			desc: "circlist/large",
+			cfg:  CycleTracker,
+			raw:  circular(3000),
+		},
+		{
+			desc: "mapofcirc/small",
+			cfg:  CycleTracker,
+			raw: map[string]*ListNode{
+				"1. one":   circular(1),
+				"2. two":   circular(2),
+				"3. three": circular(1),
+			},
+		},
+		{
+			desc: "selfrefmap/small",
+			cfg:  CycleTracker,
+			raw:  selfRef,
+		},
+	}
+
+	for _, bench := range benchmarks {
+		b.Run(bench.desc, func(b *testing.B) {
+			b.ReportAllocs()
+			for i := 0; i < b.N; i++ {
+				ref := &reflector{
+					Config: bench.cfg,
+				}
+				if bench.cfg.TrackCycles {
+					ref.pointerTracker = new(pointerTracker)
+				}
+				ref.val2node(reflect.ValueOf(bench.raw))
+			}
+		})
+	}
+}
+
+type nilIsFine struct{}
+
+func (n *nilIsFine) String() string {
+	if n == nil {
+		return "<nil is fine>"
+	}
+	return "<not nil is fine>"
+}
diff --git a/vendor/github.com/kylelemons/godebug/pretty/structure.go b/vendor/github.com/kylelemons/godebug/pretty/structure.go
new file mode 100644
index 0000000..d876f60
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/structure.go
@@ -0,0 +1,223 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+)
+
+// a formatter stores stateful formatting information as well as being
+// an io.Writer for simplicity.
+type formatter struct {
+	*bufio.Writer
+	*Config
+
+	// Self-referential structure tracking
+	tagNumbers map[int]int // tagNumbers[id] = <#n>
+}
+
+// newFormatter creates a new buffered formatter.  For the output to be written
+// to the given writer, this must be accompanied by a call to write (or Flush).
+func newFormatter(cfg *Config, w io.Writer) *formatter {
+	return &formatter{
+		Writer:     bufio.NewWriter(w),
+		Config:     cfg,
+		tagNumbers: make(map[int]int),
+	}
+}
+
+func (f *formatter) write(n node) {
+	defer f.Flush()
+	n.format(f, "")
+}
+
+func (f *formatter) tagFor(id int) int {
+	if tag, ok := f.tagNumbers[id]; ok {
+		return tag
+	}
+	if f.tagNumbers == nil {
+		return 0
+	}
+	tag := len(f.tagNumbers) + 1
+	f.tagNumbers[id] = tag
+	return tag
+}
+
+type node interface {
+	format(f *formatter, indent string)
+}
+
+func (f *formatter) compactString(n node) string {
+	switch k := n.(type) {
+	case stringVal:
+		return string(k)
+	case rawVal:
+		return string(k)
+	}
+
+	buf := new(bytes.Buffer)
+	f2 := newFormatter(&Config{Compact: true}, buf)
+	f2.tagNumbers = f.tagNumbers // reuse tagNumbers just in case
+	f2.write(n)
+	return buf.String()
+}
+
+type stringVal string
+
+func (str stringVal) format(f *formatter, indent string) {
+	f.WriteString(strconv.Quote(string(str)))
+}
+
+type rawVal string
+
+func (r rawVal) format(f *formatter, indent string) {
+	f.WriteString(string(r))
+}
+
+type keyval struct {
+	key string
+	val node
+}
+
+type keyvals []keyval
+
+func (l keyvals) format(f *formatter, indent string) {
+	f.WriteByte('{')
+
+	switch {
+	case f.Compact:
+		// All on one line:
+		for i, kv := range l {
+			if i > 0 {
+				f.WriteByte(',')
+			}
+			f.WriteString(kv.key)
+			f.WriteByte(':')
+			kv.val.format(f, indent)
+		}
+	case f.Diffable:
+		f.WriteByte('\n')
+		inner := indent + " "
+		// Each value gets its own line:
+		for _, kv := range l {
+			f.WriteString(inner)
+			f.WriteString(kv.key)
+			f.WriteString(": ")
+			kv.val.format(f, inner)
+			f.WriteString(",\n")
+		}
+		f.WriteString(indent)
+	default:
+		keyWidth := 0
+		for _, kv := range l {
+			if kw := len(kv.key); kw > keyWidth {
+				keyWidth = kw
+			}
+		}
+		alignKey := indent + " "
+		alignValue := strings.Repeat(" ", keyWidth)
+		inner := alignKey + alignValue + "  "
+		// First and last line shared with bracket:
+		for i, kv := range l {
+			if i > 0 {
+				f.WriteString(",\n")
+				f.WriteString(alignKey)
+			}
+			f.WriteString(kv.key)
+			f.WriteString(": ")
+			f.WriteString(alignValue[len(kv.key):])
+			kv.val.format(f, inner)
+		}
+	}
+
+	f.WriteByte('}')
+}
+
+type list []node
+
+func (l list) format(f *formatter, indent string) {
+	if max := f.ShortList; max > 0 {
+		short := f.compactString(l)
+		if len(short) <= max {
+			f.WriteString(short)
+			return
+		}
+	}
+
+	f.WriteByte('[')
+
+	switch {
+	case f.Compact:
+		// All on one line:
+		for i, v := range l {
+			if i > 0 {
+				f.WriteByte(',')
+			}
+			v.format(f, indent)
+		}
+	case f.Diffable:
+		f.WriteByte('\n')
+		inner := indent + " "
+		// Each value gets its own line:
+		for _, v := range l {
+			f.WriteString(inner)
+			v.format(f, inner)
+			f.WriteString(",\n")
+		}
+		f.WriteString(indent)
+	default:
+		inner := indent + " "
+		// First and last line shared with bracket:
+		for i, v := range l {
+			if i > 0 {
+				f.WriteString(",\n")
+				f.WriteString(inner)
+			}
+			v.format(f, inner)
+		}
+	}
+
+	f.WriteByte(']')
+}
+
+type ref struct {
+	id int
+}
+
+func (r ref) format(f *formatter, indent string) {
+	fmt.Fprintf(f, "<see #%d>", f.tagFor(r.id))
+}
+
+type target struct {
+	id    int
+	value node
+}
+
+func (t target) format(f *formatter, indent string) {
+	tag := fmt.Sprintf("<#%d> ", f.tagFor(t.id))
+	switch {
+	case f.Diffable, f.Compact:
+		// no indent changes
+	default:
+		indent += strings.Repeat(" ", len(tag))
+	}
+	f.WriteString(tag)
+	t.value.format(f, indent)
+}
diff --git a/vendor/github.com/kylelemons/godebug/pretty/structure_test.go b/vendor/github.com/kylelemons/godebug/pretty/structure_test.go
new file mode 100644
index 0000000..7fb831c
--- /dev/null
+++ b/vendor/github.com/kylelemons/godebug/pretty/structure_test.go
@@ -0,0 +1,371 @@
+// Copyright 2013 Google Inc.  All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pretty
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+)
+
+func TestFormat(t *testing.T) {
+	tests := []struct {
+		desc string
+		node node
+
+		// All strings have a leading newline trimmed before comparison:
+		normal   string
+		diffable string
+	}{
+		{
+			desc:     "string",
+			node:     stringVal("zaphod"),
+			normal:   `"zaphod"`,
+			diffable: `"zaphod"`,
+		},
+		{
+			desc:     "raw",
+			node:     rawVal("42"),
+			normal:   `42`,
+			diffable: `42`,
+		},
+		{
+			desc: "keyvals",
+			node: keyvals{
+				{"name", stringVal("zaphod")},
+				{"age", rawVal("42")},
+			},
+			normal: `
+{name: "zaphod",
+ age:  42}`,
+			diffable: `
+{
+ name: "zaphod",
+ age: 42,
+}`,
+		},
+		{
+			desc: "empty list",
+			node: list{},
+			normal: `
+[]`,
+			diffable: `
+[
+]`,
+		},
+		{
+			desc: "empty nested list",
+			node: list{list{}},
+			normal: `
+[[]]`,
+			diffable: `
+[
+ [
+ ],
+]`,
+		},
+		{
+			desc: "list",
+			node: list{
+				stringVal("zaphod"),
+				rawVal("42"),
+			},
+			normal: `
+["zaphod",
+ 42]`,
+			diffable: `
+[
+ "zaphod",
+ 42,
+]`,
+		},
+		{
+			desc: "empty keyvals",
+			node: keyvals{},
+			normal: `
+{}`,
+			diffable: `
+{
+}`,
+		},
+		{
+			desc: "empty nested keyvals",
+			node: keyvals{{"k", keyvals{}}},
+			normal: `
+{k: {}}`,
+			diffable: `
+{
+ k: {
+ },
+}`,
+		},
+		{
+			desc: "nested",
+			node: list{
+				stringVal("first"),
+				list{rawVal("1"), rawVal("2"), rawVal("3")},
+				keyvals{
+					{"trillian", keyvals{
+						{"race", stringVal("human")},
+						{"age", rawVal("36")},
+					}},
+					{"zaphod", keyvals{
+						{"occupation", stringVal("president of the galaxy")},
+						{"features", stringVal("two heads")},
+					}},
+				},
+				keyvals{},
+			},
+			normal: `
+["first",
+ [1,
+  2,
+  3],
+ {trillian: {race: "human",
+             age:  36},
+  zaphod:   {occupation: "president of the galaxy",
+             features:   "two heads"}},
+ {}]`,
+			diffable: `
+[
+ "first",
+ [
+  1,
+  2,
+  3,
+ ],
+ {
+  trillian: {
+   race: "human",
+   age: 36,
+  },
+  zaphod: {
+   occupation: "president of the galaxy",
+   features: "two heads",
+  },
+ },
+ {
+ },
+]`,
+		},
+		{
+			desc: "recursive",
+			node: target{1, keyvals{
+				{"Value", rawVal("1")},
+				{"Next", keyvals{
+					{"Value", rawVal("2")},
+					{"Next", keyvals{
+						{"Value", rawVal("3")},
+						{"Next", ref{1}},
+					}},
+				}},
+			}},
+			normal: `
+<#1> {Value: 1,
+      Next:  {Value: 2,
+              Next:  {Value: 3,
+                      Next:  <see #1>}}}`,
+			diffable: `
+<#1> {
+ Value: 1,
+ Next: {
+  Value: 2,
+  Next: {
+   Value: 3,
+   Next: <see #1>,
+  },
+ },
+}`,
+		},
+		{
+			desc: "print in order",
+			node: list{
+				target{2, keyvals{
+					{"Next", ref{1}},
+				}},
+				target{1, keyvals{
+					{"Next", ref{2}},
+				}},
+			},
+			normal: `
+[<#1> {Next: <see #2>},
+ <#2> {Next: <see #1>}]`,
+			diffable: `
+[
+ <#1> {
+  Next: <see #2>,
+ },
+ <#2> {
+  Next: <see #1>,
+ },
+]`,
+		},
+	}
+
+	normal := &Config{}
+	diffable := &Config{Diffable: true}
+	for _, test := range tests {
+		// For readability, we have a newline that won't be there in the output
+		test.normal = strings.TrimPrefix(test.normal, "\n")
+		test.diffable = strings.TrimPrefix(test.diffable, "\n")
+
+		buf := new(bytes.Buffer)
+		newFormatter(normal, buf).write(test.node)
+		if got, want := buf.String(), test.normal; got != want {
+			t.Errorf("%s: normal rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want)
+		}
+		buf.Reset()
+
+		newFormatter(diffable, buf).write(test.node)
+		if got, want := buf.String(), test.diffable; got != want {
+			t.Errorf("%s: diffable rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want)
+		}
+	}
+}
+
+func TestCompactString(t *testing.T) {
+	tests := []struct {
+		node
+		compact string
+	}{
+		{
+			stringVal("abc"),
+			"abc",
+		},
+		{
+			rawVal("2"),
+			"2",
+		},
+		{
+			list{
+				rawVal("2"),
+				rawVal("3"),
+			},
+			"[2,3]",
+		},
+		{
+			keyvals{
+				{"name", stringVal("zaphod")},
+				{"age", rawVal("42")},
+			},
+			`{name:"zaphod",age:42}`,
+		},
+		{
+			list{
+				list{
+					rawVal("0"),
+					rawVal("1"),
+					rawVal("2"),
+					rawVal("3"),
+				},
+				list{
+					rawVal("1"),
+					rawVal("2"),
+					rawVal("3"),
+					rawVal("0"),
+				},
+				list{
+					rawVal("2"),
+					rawVal("3"),
+					rawVal("0"),
+					rawVal("1"),
+				},
+			},
+			`[[0,1,2,3],[1,2,3,0],[2,3,0,1]]`,
+		},
+	}
+
+	for _, test := range tests {
+		if got, want := new(formatter).compactString(test.node), test.compact; got != want {
+			t.Errorf("%#v: compact = %q, want %q", test.node, got, want)
+		}
+	}
+}
+
+func TestShortList(t *testing.T) {
+	cfg := &Config{
+		ShortList: 16,
+	}
+
+	tests := []struct {
+		node
+		want string
+	}{
+		{
+			list{
+				list{
+					rawVal("0"),
+					rawVal("1"),
+					rawVal("2"),
+					rawVal("3"),
+				},
+				list{
+					rawVal("1"),
+					rawVal("2"),
+					rawVal("3"),
+					rawVal("0"),
+				},
+				list{
+					rawVal("2"),
+					rawVal("3"),
+					rawVal("0"),
+					rawVal("1"),
+				},
+			},
+			`[[0,1,2,3],
+ [1,2,3,0],
+ [2,3,0,1]]`,
+		},
+	}
+
+	for _, test := range tests {
+		buf := new(bytes.Buffer)
+		newFormatter(cfg, buf).write(test.node)
+		if got, want := buf.String(), test.want; got != want {
+			t.Errorf("%#v:\ngot:\n%s\nwant:\n%s", test.node, got, want)
+		}
+	}
+}
+
+var benchNode = keyvals{
+	{"list", list{
+		rawVal("0"),
+		rawVal("1"),
+		rawVal("2"),
+		rawVal("3"),
+	}},
+	{"keyvals", keyvals{
+		{"a", stringVal("b")},
+		{"c", stringVal("e")},
+		{"d", stringVal("f")},
+	}},
+}
+
+func benchOpts(b *testing.B, cfg *Config) {
+	buf := new(bytes.Buffer)
+	newFormatter(cfg, buf).write(benchNode)
+	b.SetBytes(int64(buf.Len()))
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		buf.Reset()
+		newFormatter(cfg, buf).write(benchNode)
+	}
+}
+
+func BenchmarkWriteDefault(b *testing.B)   { benchOpts(b, DefaultConfig) }
+func BenchmarkWriteShortList(b *testing.B) { benchOpts(b, &Config{ShortList: 16}) }
+func BenchmarkWriteCompact(b *testing.B)   { benchOpts(b, &Config{Compact: true}) }
+func BenchmarkWriteDiffable(b *testing.B)  { benchOpts(b, &Config{Diffable: true}) }