2024-02-08 03:48:59 -06:00
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
2023-05-02 10:33:06 -05:00
// SPDX-License-Identifier: MPL-2.0
2023-01-16 08:18:38 -06:00
package attribute_path
2023-07-05 04:11:02 -05:00
import (
"encoding/json"
"fmt"
"strconv"
)
2023-01-16 08:18:38 -06:00
// Matcher provides an interface for stepping through changes following an
// attribute path.
//
// GetChildWithKey and GetChildWithIndex will check if any of the internal paths
// match the provided key or index, and return a new Matcher that will match
// that children or potentially it's children.
//
// The caller of the above functions is required to know whether the next value
// in the path is a list type or an object type and call the relevant function,
// otherwise these functions will crash/panic.
//
// The Matches function returns true if the paths you have traversed until now
// ends.
type Matcher interface {
// Matches returns true if we have reached the end of a path and found an
// exact match.
Matches ( ) bool
// MatchesPartial returns true if the current attribute is part of a path
// but not necessarily at the end of the path.
MatchesPartial ( ) bool
GetChildWithKey ( key string ) Matcher
GetChildWithIndex ( index int ) Matcher
}
// Parse accepts a json.RawMessage and outputs a formatted Matcher 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. The propagate
// argument tells the matcher to propagate any matches to the matched attributes
// children.
//
// In general, this function is designed to accept messages that have been
// produced by the lossy cty.Paths conversion functions within the jsonplan
// package. There is nothing particularly special about that conversion process
// though, it just produces the nested JSON arrays described above.
func Parse ( message json . RawMessage , propagate bool ) Matcher {
matcher := & PathMatcher {
Propagate : propagate ,
}
if message == nil {
return matcher
}
if err := json . Unmarshal ( message , & matcher . Paths ) ; err != nil {
panic ( "failed to unmarshal attribute paths: " + err . Error ( ) )
}
return matcher
}
// Empty returns an empty PathMatcher that will by default match nothing.
//
// We give direct access to the PathMatcher struct so a matcher can be built
// in parts with the Append and AppendSingle functions.
func Empty ( propagate bool ) * PathMatcher {
return & PathMatcher {
Propagate : propagate ,
}
}
// Append accepts an existing PathMatcher and returns a new one that attaches
// all the paths from message with the existing paths.
//
// The new PathMatcher is created fresh, and the existing one is unchanged.
func Append ( matcher * PathMatcher , message json . RawMessage ) * PathMatcher {
var values [ ] [ ] interface { }
if err := json . Unmarshal ( message , & values ) ; err != nil {
panic ( "failed to unmarshal attribute paths: " + err . Error ( ) )
}
return & PathMatcher {
Propagate : matcher . Propagate ,
Paths : append ( matcher . Paths , values ... ) ,
}
}
// AppendSingle accepts an existing PathMatcher and returns a new one that
// attaches the single path from message with the existing paths.
//
// The new PathMatcher is created fresh, and the existing one is unchanged.
func AppendSingle ( matcher * PathMatcher , message json . RawMessage ) * PathMatcher {
var values [ ] interface { }
if err := json . Unmarshal ( message , & values ) ; err != nil {
panic ( "failed to unmarshal attribute paths: " + err . Error ( ) )
}
return & PathMatcher {
Propagate : matcher . Propagate ,
Paths : append ( matcher . Paths , values ) ,
}
}
// PathMatcher contains a slice of paths that represent paths through the values
// to relevant/tracked attributes.
type PathMatcher struct {
// We represent our internal paths as a [][]interface{} as the cty.Paths
// conversion process is lossy. Since the type information is lost there
// is no (easy) way to reproduce the original cty.Paths object. Instead,
// we simply rely on the external callers to know the type information and
// call the correct GetChild function.
Paths [ ] [ ] interface { }
// Propagate tells the matcher that it should propagate any matches it finds
// onto the children of that match.
Propagate bool
}
func ( p * PathMatcher ) Matches ( ) bool {
for _ , path := range p . Paths {
if len ( path ) == 0 {
return true
}
}
return false
}
func ( p * PathMatcher ) MatchesPartial ( ) bool {
return len ( p . Paths ) > 0
}
func ( p * PathMatcher ) GetChildWithKey ( key string ) Matcher {
child := & PathMatcher {
Propagate : p . Propagate ,
}
for _ , path := range p . Paths {
if len ( path ) == 0 {
// This means that the current value matched, but not necessarily
// it's child.
if p . Propagate {
// If propagate is true, then our child match our matches
child . Paths = append ( child . Paths , path )
}
// If not we would simply drop this path from our set of paths but
// either way we just continue.
continue
}
if path [ 0 ] . ( string ) == key {
child . Paths = append ( child . Paths , path [ 1 : ] )
}
}
return child
}
func ( p * PathMatcher ) GetChildWithIndex ( index int ) Matcher {
child := & PathMatcher {
Propagate : p . Propagate ,
}
for _ , path := range p . Paths {
if len ( path ) == 0 {
// This means that the current value matched, but not necessarily
// it's child.
if p . Propagate {
// If propagate is true, then our child match our matches
child . Paths = append ( child . Paths , path )
}
// If not we would simply drop this path from our set of paths but
// either way we just continue.
continue
}
2023-09-21 07:38:46 -05:00
// OpenTofu actually allows user to provide strings into indexes as
2023-07-05 04:11:02 -05:00
// long as the string can be interpreted into a number. For example, the
// following are equivalent and we need to support them.
// - test_resource.resource.list[0].attribute
// - test_resource.resource.list["0"].attribute
//
2023-09-21 07:38:46 -05:00
// Note, that OpenTofu will raise a validation error if the string
2023-07-05 04:11:02 -05:00
// can't be coerced into a number, so we will panic here if anything
// goes wrong safe in the knowledge the validation should stop this from
// happening.
switch val := path [ 0 ] . ( type ) {
case float64 :
if int ( path [ 0 ] . ( float64 ) ) == index {
child . Paths = append ( child . Paths , path [ 1 : ] )
}
case string :
f , err := strconv . ParseFloat ( val , 64 )
if err != nil {
2023-09-21 07:38:46 -05:00
panic ( fmt . Errorf ( "found invalid type within path (%v:%T), the validation shouldn't have allowed this to happen; this is a bug in OpenTofu, please report it" , val , val ) )
2023-07-05 04:11:02 -05:00
}
if int ( f ) == index {
child . Paths = append ( child . Paths , path [ 1 : ] )
}
default :
2023-09-21 07:38:46 -05:00
panic ( fmt . Errorf ( "found invalid type within path (%v:%T), the validation shouldn't have allowed this to happen; this is a bug in OpenTofu, please report it" , val , val ) )
2023-01-16 08:18:38 -06:00
}
}
return child
}
// AlwaysMatcher returns a matcher that will always match all paths.
func AlwaysMatcher ( ) Matcher {
return & alwaysMatcher { }
}
type alwaysMatcher struct { }
func ( a * alwaysMatcher ) Matches ( ) bool {
return true
}
func ( a * alwaysMatcher ) MatchesPartial ( ) bool {
return true
}
func ( a * alwaysMatcher ) GetChildWithKey ( _ string ) Matcher {
return a
}
func ( a * alwaysMatcher ) GetChildWithIndex ( _ int ) Matcher {
return a
}