github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonformat/structured/attribute_path/matcher.go (about)

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