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