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 }