1package ctydebug
2
3import (
4	"fmt"
5	"reflect"
6	"strings"
7
8	"github.com/google/go-cmp/cmp"
9	"github.com/zclconf/go-cty/cty"
10)
11
12// DiffValues returns a human-oriented description of the differences between
13// the two given values. It's guaranteed to return an empty string if the
14// two values are RawEqual.
15//
16// Don't depend on the exact formatting of the result. It is likely to change
17// in future releases.
18func DiffValues(want, got cty.Value) string {
19	// want and got are in the order they are here because that's how cmp
20	// seems to treat them, and we'd like to be consistent with cmp to
21	// minimize confusion in codebases that are using both cmp directly and
22	// indirectly via DiffValues.
23
24	if got.RawEquals(want) {
25		return "" // just to make sure
26	}
27
28	r := &diffValuesReporter{}
29	cmp.Equal(want, got, CmpOptions, cmp.Reporter(r))
30
31	return r.Result()
32}
33
34// This is a very simple reporter for now. Hopefully one day it can become
35// more sophisticated and produce output that looks more like the result
36// of ValueString.
37type diffValuesReporter struct {
38	path cmp.Path
39	sb   strings.Builder
40}
41
42func (r *diffValuesReporter) PushStep(step cmp.PathStep) {
43	r.path = append(r.path, step)
44}
45
46func (r *diffValuesReporter) PopStep() {
47	r.path = r.path[:len(r.path)-1]
48}
49
50func (r *diffValuesReporter) Report(result cmp.Result) {
51	if result.Equal() {
52		return
53	}
54
55	r.sb.WriteString(cmpPathString(r.path))
56	r.sb.WriteString("\n")
57	want, got := r.path.Last().Values()
58	fmt.Fprintf(&r.sb, "  got:  %s\n", resultValueString(got))
59	fmt.Fprintf(&r.sb, "  want: %s\n", resultValueString(want))
60	r.sb.WriteString("\n")
61}
62
63func (r *diffValuesReporter) Result() string {
64	return r.sb.String()
65}
66
67func resultValueString(rv reflect.Value) string {
68	if !rv.IsValid() {
69		return "(no value)"
70	}
71	if v, ok := rv.Interface().(cty.Value); ok && v == cty.NilVal {
72		return "cty.NilVal"
73	}
74	if ty, ok := rv.Interface().(cty.Type); ok && ty == cty.NilType {
75		return "cty.NilType"
76	}
77	return fmt.Sprintf("%#v", rv)
78}
79
80// cmpPathString returns the given path serialized using a compact syntax
81// that isn't in any language exactly but is hopefully intuitive.
82func cmpPathString(path cmp.Path) string {
83	var b strings.Builder
84	for _, step := range path {
85		switch step := step.(type) {
86		case cmp.Transform:
87			if step.Option() == transformValueOp || step.Option() == transformTypeOp {
88				continue // ignore; it's an implementation detail
89			}
90			b.WriteString(step.String())
91		case cmp.TypeAssertion:
92			// These show up on the results of the transforms we do to trick
93			// cmp into walking into our structural/collection values, but
94			// that's an implementation detail so we'll skip it.
95			continue
96		case cmp.Indirect:
97			continue
98		case cmp.MapIndex:
99			fmt.Fprintf(&b, "[%q]", step.Key())
100		case cmp.SliceIndex:
101			fmt.Fprintf(&b, "[%d]", step.Key())
102		case cmp.StructField:
103			// We don't expect to see any struct field traversals in our
104			// work because of our transformations, but if one shows up then
105			// we'll handle it somewhat gracefully...
106			fmt.Fprintf(&b, ".%s", step.Name())
107		default:
108			b.WriteString(step.String())
109		}
110	}
111	return b.String()
112}
113