github.com/opentofu/opentofu@v1.7.1/internal/command/jsonformat/structured/attribute_path/matcher.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package attribute_path
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"strconv"
    12  )
    13  
    14  // Matcher provides an interface for stepping through changes following an
    15  // attribute path.
    16  //
    17  // GetChildWithKey and GetChildWithIndex will check if any of the internal paths
    18  // match the provided key or index, and return a new Matcher that will match
    19  // that children or potentially it's children.
    20  //
    21  // The caller of the above functions is required to know whether the next value
    22  // in the path is a list type or an object type and call the relevant function,
    23  // otherwise these functions will crash/panic.
    24  //
    25  // The Matches function returns true if the paths you have traversed until now
    26  // ends.
    27  type Matcher interface {
    28  	// Matches returns true if we have reached the end of a path and found an
    29  	// exact match.
    30  	Matches() bool
    31  
    32  	// MatchesPartial returns true if the current attribute is part of a path
    33  	// but not necessarily at the end of the path.
    34  	MatchesPartial() bool
    35  
    36  	GetChildWithKey(key string) Matcher
    37  	GetChildWithIndex(index int) Matcher
    38  }
    39  
    40  // Parse accepts a json.RawMessage and outputs a formatted Matcher object.
    41  //
    42  // Parse expects the message to be a JSON array of JSON arrays containing
    43  // strings and floats. This function happily accepts a null input representing
    44  // none of the changes in this resource are causing a replacement. The propagate
    45  // argument tells the matcher to propagate any matches to the matched attributes
    46  // children.
    47  //
    48  // In general, this function is designed to accept messages that have been
    49  // produced by the lossy cty.Paths conversion functions within the jsonplan
    50  // package. There is nothing particularly special about that conversion process
    51  // though, it just produces the nested JSON arrays described above.
    52  func Parse(message json.RawMessage, propagate bool) Matcher {
    53  	matcher := &PathMatcher{
    54  		Propagate: propagate,
    55  	}
    56  	if message == nil {
    57  		return matcher
    58  	}
    59  
    60  	if err := json.Unmarshal(message, &matcher.Paths); err != nil {
    61  		panic("failed to unmarshal attribute paths: " + err.Error())
    62  	}
    63  
    64  	return matcher
    65  }
    66  
    67  // Empty returns an empty PathMatcher that will by default match nothing.
    68  //
    69  // We give direct access to the PathMatcher struct so a matcher can be built
    70  // in parts with the Append and AppendSingle functions.
    71  func Empty(propagate bool) *PathMatcher {
    72  	return &PathMatcher{
    73  		Propagate: propagate,
    74  	}
    75  }
    76  
    77  // Append accepts an existing PathMatcher and returns a new one that attaches
    78  // all the paths from message with the existing paths.
    79  //
    80  // The new PathMatcher is created fresh, and the existing one is unchanged.
    81  func Append(matcher *PathMatcher, message json.RawMessage) *PathMatcher {
    82  	var values [][]interface{}
    83  	if err := json.Unmarshal(message, &values); err != nil {
    84  		panic("failed to unmarshal attribute paths: " + err.Error())
    85  	}
    86  
    87  	return &PathMatcher{
    88  		Propagate: matcher.Propagate,
    89  		Paths:     append(matcher.Paths, values...),
    90  	}
    91  }
    92  
    93  // AppendSingle accepts an existing PathMatcher and returns a new one that
    94  // attaches the single path from message with the existing paths.
    95  //
    96  // The new PathMatcher is created fresh, and the existing one is unchanged.
    97  func AppendSingle(matcher *PathMatcher, message json.RawMessage) *PathMatcher {
    98  	var values []interface{}
    99  	if err := json.Unmarshal(message, &values); err != nil {
   100  		panic("failed to unmarshal attribute paths: " + err.Error())
   101  	}
   102  
   103  	return &PathMatcher{
   104  		Propagate: matcher.Propagate,
   105  		Paths:     append(matcher.Paths, values),
   106  	}
   107  }
   108  
   109  // PathMatcher contains a slice of paths that represent paths through the values
   110  // to relevant/tracked attributes.
   111  type PathMatcher struct {
   112  	// We represent our internal paths as a [][]interface{} as the cty.Paths
   113  	// conversion process is lossy. Since the type information is lost there
   114  	// is no (easy) way to reproduce the original cty.Paths object. Instead,
   115  	// we simply rely on the external callers to know the type information and
   116  	// call the correct GetChild function.
   117  	Paths [][]interface{}
   118  
   119  	// Propagate tells the matcher that it should propagate any matches it finds
   120  	// onto the children of that match.
   121  	Propagate bool
   122  }
   123  
   124  func (p *PathMatcher) Matches() bool {
   125  	for _, path := range p.Paths {
   126  		if len(path) == 0 {
   127  			return true
   128  		}
   129  	}
   130  	return false
   131  }
   132  
   133  func (p *PathMatcher) MatchesPartial() bool {
   134  	return len(p.Paths) > 0
   135  }
   136  
   137  func (p *PathMatcher) GetChildWithKey(key string) Matcher {
   138  	child := &PathMatcher{
   139  		Propagate: p.Propagate,
   140  	}
   141  	for _, path := range p.Paths {
   142  		if len(path) == 0 {
   143  			// This means that the current value matched, but not necessarily
   144  			// it's child.
   145  
   146  			if p.Propagate {
   147  				// If propagate is true, then our child match our matches
   148  				child.Paths = append(child.Paths, path)
   149  			}
   150  
   151  			// If not we would simply drop this path from our set of paths but
   152  			// either way we just continue.
   153  			continue
   154  		}
   155  
   156  		if path[0].(string) == key {
   157  			child.Paths = append(child.Paths, path[1:])
   158  		}
   159  	}
   160  	return child
   161  }
   162  
   163  func (p *PathMatcher) GetChildWithIndex(index int) Matcher {
   164  	child := &PathMatcher{
   165  		Propagate: p.Propagate,
   166  	}
   167  	for _, path := range p.Paths {
   168  		if len(path) == 0 {
   169  			// This means that the current value matched, but not necessarily
   170  			// it's child.
   171  
   172  			if p.Propagate {
   173  				// If propagate is true, then our child match our matches
   174  				child.Paths = append(child.Paths, path)
   175  			}
   176  
   177  			// If not we would simply drop this path from our set of paths but
   178  			// either way we just continue.
   179  			continue
   180  		}
   181  
   182  		// OpenTofu actually allows user to provide strings into indexes as
   183  		// long as the string can be interpreted into a number. For example, the
   184  		// following are equivalent and we need to support them.
   185  		//    - test_resource.resource.list[0].attribute
   186  		//    - test_resource.resource.list["0"].attribute
   187  		//
   188  		// Note, that OpenTofu will raise a validation error if the string
   189  		// can't be coerced into a number, so we will panic here if anything
   190  		// goes wrong safe in the knowledge the validation should stop this from
   191  		// happening.
   192  
   193  		switch val := path[0].(type) {
   194  		case float64:
   195  			if int(path[0].(float64)) == index {
   196  				child.Paths = append(child.Paths, path[1:])
   197  			}
   198  		case string:
   199  			f, err := strconv.ParseFloat(val, 64)
   200  			if err != nil {
   201  				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))
   202  			}
   203  			if int(f) == index {
   204  				child.Paths = append(child.Paths, path[1:])
   205  			}
   206  		default:
   207  			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))
   208  		}
   209  	}
   210  	return child
   211  }
   212  
   213  // AlwaysMatcher returns a matcher that will always match all paths.
   214  func AlwaysMatcher() Matcher {
   215  	return &alwaysMatcher{}
   216  }
   217  
   218  type alwaysMatcher struct{}
   219  
   220  func (a *alwaysMatcher) Matches() bool {
   221  	return true
   222  }
   223  
   224  func (a *alwaysMatcher) MatchesPartial() bool {
   225  	return true
   226  }
   227  
   228  func (a *alwaysMatcher) GetChildWithKey(_ string) Matcher {
   229  	return a
   230  }
   231  
   232  func (a *alwaysMatcher) GetChildWithIndex(_ int) Matcher {
   233  	return a
   234  }