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 }