blob: 99bc4137c8ec25f604e643d471f3eac684e722d7 [file] [log] [blame]
package jsondiff
import (
"reflect"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/command/jsonformat/collections"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/structured"
"github.com/hashicorp/terraform/internal/plans"
)
type TransformPrimitiveJson func(before, after interface{}, ctype cty.Type, action plans.Action) computed.Diff
type TransformObjectJson func(map[string]computed.Diff, plans.Action) computed.Diff
type TransformArrayJson func([]computed.Diff, plans.Action) computed.Diff
type TransformUnknownJson func(computed.Diff, plans.Action) computed.Diff
type TransformSensitiveJson func(computed.Diff, bool, bool, plans.Action) computed.Diff
type TransformTypeChangeJson func(before, after computed.Diff, action plans.Action) computed.Diff
// JsonOpts defines the external callback functions that callers should
// implement to process the supplied diffs.
type JsonOpts struct {
Primitive TransformPrimitiveJson
Object TransformObjectJson
Array TransformArrayJson
Unknown TransformUnknownJson
Sensitive TransformSensitiveJson
TypeChange TransformTypeChangeJson
}
// Transform accepts a generic before and after value that is assumed to be JSON
// formatted and transforms it into a computed.Diff, using the callbacks
// supplied in the JsonOpts class.
func (opts JsonOpts) Transform(change structured.Change) computed.Diff {
if sensitive, ok := opts.processSensitive(change); ok {
return sensitive
}
if unknown, ok := opts.processUnknown(change); ok {
return unknown
}
beforeType := GetType(change.Before)
afterType := GetType(change.After)
deleted := afterType == Null && !change.AfterExplicit
created := beforeType == Null && !change.BeforeExplicit
if beforeType == afterType || (created || deleted) {
targetType := beforeType
if targetType == Null {
targetType = afterType
}
return opts.processUpdate(change, targetType)
}
b := opts.processUpdate(change.AsDelete(), beforeType)
a := opts.processUpdate(change.AsCreate(), afterType)
return opts.TypeChange(b, a, plans.Update)
}
func (opts JsonOpts) processUpdate(change structured.Change, jtype Type) computed.Diff {
switch jtype {
case Null:
return opts.processPrimitive(change, cty.NilType)
case Bool:
return opts.processPrimitive(change, cty.Bool)
case String:
return opts.processPrimitive(change, cty.String)
case Number:
return opts.processPrimitive(change, cty.Number)
case Object:
return opts.processObject(change.AsMap())
case Array:
return opts.processArray(change.AsSlice())
default:
panic("unrecognized json type: " + jtype)
}
}
func (opts JsonOpts) processPrimitive(change structured.Change, ctype cty.Type) computed.Diff {
beforeMissing := change.Before == nil && !change.BeforeExplicit
afterMissing := change.After == nil && !change.AfterExplicit
var action plans.Action
switch {
case beforeMissing && !afterMissing:
action = plans.Create
case !beforeMissing && afterMissing:
action = plans.Delete
case reflect.DeepEqual(change.Before, change.After):
action = plans.NoOp
default:
action = plans.Update
}
return opts.Primitive(change.Before, change.After, ctype, action)
}
func (opts JsonOpts) processArray(change structured.ChangeSlice) computed.Diff {
processIndices := func(beforeIx, afterIx int) computed.Diff {
// It's actually really difficult to render the diffs when some indices
// within a list are relevant and others aren't. To make this simpler
// we just treat all children of a relevant list as also relevant, so we
// ignore the relevant attributes field.
//
// Interestingly the terraform plan builder also agrees with this, and
// never sets relevant attributes beneath lists or sets. We're just
// going to enforce this logic here as well. If the list is relevant
// (decided elsewhere), then every element in the list is also relevant.
return opts.Transform(change.GetChild(beforeIx, afterIx))
}
isObjType := func(value interface{}) bool {
return GetType(value) == Object
}
return opts.Array(collections.TransformSlice(change.Before, change.After, processIndices, isObjType))
}
func (opts JsonOpts) processObject(change structured.ChangeMap) computed.Diff {
return opts.Object(collections.TransformMap(change.Before, change.After, change.AllKeys(), func(key string) computed.Diff {
child := change.GetChild(key)
if !child.RelevantAttributes.MatchesPartial() {
child = child.AsNoOp()
}
return opts.Transform(child)
}))
}
func (opts JsonOpts) processUnknown(change structured.Change) (computed.Diff, bool) {
return change.CheckForUnknown(
false,
func(current structured.Change) computed.Diff {
return opts.Unknown(computed.Diff{}, plans.Create)
}, func(current structured.Change, before structured.Change) computed.Diff {
return opts.Unknown(opts.Transform(before), plans.Update)
},
)
}
func (opts JsonOpts) processSensitive(change structured.Change) (computed.Diff, bool) {
return change.CheckForSensitive(opts.Transform, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff {
return opts.Sensitive(inner, beforeSensitive, afterSensitive, action)
})
}