command/format: show deep diffs for old/new JSON strings

Because so far we've not supported dynamically-typed complex data
structures, several providers have used strings containing JSON to stand
in for these.

In order to get a readable diff in those cases, we'll recognize situations
where old and new are both JSON and present a diff of the effective value
of the JSON, using a faux call to the jsonencode(...) function to indicate
when we've done so.

This is a bit of a "cute" heuristic, but is important at least for now
until we can migrate away from that practice of passing large JSON strings
to providers and use dynamically-typed attributes instead.
This commit is contained in:
Martin Atkins 2018-08-29 19:18:44 -07:00
parent 9706a00b3a
commit ce157c7f23
1 changed files with 50 additions and 1 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/mitchellh/colorstring"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
@ -422,6 +423,24 @@ func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, in
case ty.IsPrimitiveType():
switch ty {
case cty.String:
{
// Special behavior for JSON strings
src := []byte(val.AsString())
ty, err := ctyjson.ImpliedType(src)
if err == nil {
jv, err := ctyjson.Unmarshal(src, ty)
if err == nil {
p.buf.WriteString("jsonencode(")
p.buf.WriteByte('\n')
p.buf.WriteString(strings.Repeat(" ", indent+4))
p.writeValue(jv, action, indent+4)
p.buf.WriteByte('\n')
p.buf.WriteString(strings.Repeat(" ", indent))
p.buf.WriteByte(')')
break // don't *also* do the normal behavior below
}
}
}
fmt.Fprintf(p.buf, "%q", val.AsString())
case cty.Bool:
if val.True() {
@ -516,12 +535,42 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() {
switch {
// TODO: list diffs using longest-common-subsequence matching algorithm
// TODO: object diffs that behave a bit like the map diffs, including if the two object types don't exactly match
// TODO: multi-line string diffs showing lines added/removed using longest-common-subsequence
case ty == cty.String:
// We only have special behavior for multi-line strings here
// We have special behavior for both multi-line strings in general
// and for strings that can parse as JSON. For the JSON handling
// to apply, both old and new must be valid JSON.
// For single-line strings that don't parse as JSON we just fall
// out of this switch block and do the default old -> new rendering.
oldS := old.AsString()
newS := new.AsString()
{
// Special behavior for JSON strings
oldBytes := []byte(oldS)
newBytes := []byte(newS)
oldType, oldErr := ctyjson.ImpliedType(oldBytes)
newType, newErr := ctyjson.ImpliedType(newBytes)
if oldErr == nil && newErr == nil {
oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType)
newJV, newErr := ctyjson.Unmarshal(newBytes, newType)
if oldErr == nil && newErr == nil {
if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace
p.buf.WriteString("jsonencode(")
p.buf.WriteByte('\n')
p.buf.WriteString(strings.Repeat(" ", indent+4))
p.writeValueDiff(oldJV, newJV, indent+4, path)
p.buf.WriteByte('\n')
p.buf.WriteString(strings.Repeat(" ", indent))
p.buf.WriteByte(')')
return
}
}
}
}
if strings.Index(oldS, "\n") < 0 && strings.Index(newS, "\n") < 0 {
break
}