Files
opentf/internal/command/jsonformat/differ/replace/replace.go
Liam Cervante f6d625103c Structured plan renderer: refactor replace paths logic (#32489)
* change -> diff, value -> change

* also update readme#

* structured plan renderer: refactor replace paths logic

* goimports

* goimports

* address comments

* fix compile error
2023-01-11 10:20:24 +01:00

115 lines
4.3 KiB
Go

package replace
import "encoding/json"
// ForcesReplacement encapsulates the ReplacePaths logic from the Terraform
// change object.
//
// It is possible for a change to a deeply nested attribute or block to result
// in an entire resource being replaced (deleted then recreated) instead of
// simply updated. In this case, we want to attach some additional context to
// say, this resource is being replaced because of these changes to its
// internal values.
//
// The ReplacePaths field is a slice of paths that point to the values causing
// the replace operation. It's a slice of paths because you can have multiple
// internal values causing a replacement.
//
// Each path is a slice of indices, where an index can be a string or an
// integer. We represent this a slice of generic interfaces: []interface{}. This
// is because we actually parse this field from JSON and have no way to easily
// represent a value that can be a string or an integer in Go. Luckily, this
// doesn't matter too much from an implementation point of view because we
// always know what type to expect as we know whether we are currently looking
// at a list type (which means an integer) or a map type (which means a string).
//
// The GetChildWithKey and GetChildWithIndex return additional but modified
// ForcesReplacement objects, where a path is simply dropped if the index
// doesn't match or included with the first entry removed if the index did
// match. These functions are called as the outside Change objects are being
// created for a complex change's children.
//
// The ForcesReplacement function actually tells you whether the current value
// is causing a replacement operation as one of the paths will be empty since
// we removed an entry every time the path matched, and the last entry will have
// been removed when the change was created.
type ForcesReplacement struct {
ReplacePaths [][]interface{}
}
// Parse accepts a json.RawMessage and outputs a formatted ForcesReplacement
// object.
//
// Parse expects the message to be a JSON array of JSON arrays containing
// strings and floats. This function happily accepts a null input representing
// none of the changes in this resource are causing a replacement.
func Parse(message json.RawMessage) ForcesReplacement {
replace := ForcesReplacement{}
if message == nil {
return replace
}
if err := json.Unmarshal(message, &replace.ReplacePaths); err != nil {
panic("failed to unmarshal replace paths: " + err.Error())
}
return replace
}
// ForcesReplacement returns true if this ForcesReplacement object represents
// a change that is causing the entire resource to be replaced.
func (replace ForcesReplacement) ForcesReplacement() bool {
for _, path := range replace.ReplacePaths {
if len(path) == 0 {
return true
}
}
return false
}
// GetChildWithKey steps through the paths in this ForcesReplacement and checks
// if any match the specified key.
//
// This function assumes the index will all be strings, so callers have to be
// sure they have navigated through previous paths accurately to this point or
// this function is liable to panic.
func (replace ForcesReplacement) GetChildWithKey(key string) ForcesReplacement {
child := ForcesReplacement{}
for _, path := range replace.ReplacePaths {
if len(path) == 0 {
// This means that the current value is causing a replacement but
// not its children, so we skip as we are returning the child's
// value.
continue
}
if path[0].(string) == key {
child.ReplacePaths = append(child.ReplacePaths, path[1:])
}
}
return child
}
// GetChildWithIndex steps through the paths in this ForcesReplacement and
// checks if any match the specified index.
//
// This function assumes the index will all be integers, so callers have to be
// sure they have navigated through previous paths accurately to this point or
// this function is liable to panic.
func (replace ForcesReplacement) GetChildWithIndex(index int) ForcesReplacement {
child := ForcesReplacement{}
for _, path := range replace.ReplacePaths {
if len(path) == 0 {
// This means that the current value is causing a replacement but
// not its children, so we skip as we are returning the child's
// value.
continue
}
if int(path[0].(float64)) == index {
child.ReplacePaths = append(child.ReplacePaths, path[1:])
}
}
return child
}