|
| 1 | +package cty |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "iter" |
| 7 | + "maps" |
| 8 | + |
| 9 | + "github.com/zclconf/go-cty/cty/ctymarks" |
| 10 | +) |
| 11 | + |
| 12 | +// WrangleMarksDeep is a specialized variant of [Transform] that is focused |
| 13 | +// on interrogating and modifying any marks present throughout a data structure, |
| 14 | +// without modifying anything else about the value. |
| 15 | +// |
| 16 | +// Refer to the [WrangleFunc] documentation for more information. Each of |
| 17 | +// the provided functions is called in turn for each mark at each distinct path, |
| 18 | +// and the first function that returns a non-nil [ctymarks.WrangleAction] "wins" |
| 19 | +// and prevents any later ones from running for a particular mark/path pair. |
| 20 | +// |
| 21 | +// The implementation makes a best effort to avoid constructing new values |
| 22 | +// unless marks have actually changed, to keep this operation relatively cheap |
| 23 | +// in the presumed-common case where no marks are present at all. |
| 24 | +func (v Value) WrangleMarksDeep(wranglers ...WrangleFunc) (Value, error) { |
| 25 | + // This function is implemented in this package, rather than in the |
| 26 | + // separate "ctymarks", so that it can intrude into the unexported |
| 27 | + // internal details of [Value] to minimize overhead when no marks |
| 28 | + // are present at all. |
| 29 | + var path Path |
| 30 | + if v.IsKnown() && !v.Type().IsPrimitiveType() && !v.IsNull() { |
| 31 | + // If we have a known, non-null, non-primitive-typed value then we |
| 32 | + // can assume there will be at least a little nesting we need to |
| 33 | + // represent using our path, and so we'll preallocate some capacity |
| 34 | + // which we'll be able to share across all calls that are shallower |
| 35 | + // than this level of nesting. |
| 36 | + path = make(Path, 0, 4) |
| 37 | + } |
| 38 | + topMarks := make(ValueMarks) |
| 39 | + var errs []error |
| 40 | + new := wrangleMarksDeep(v, wranglers, path, topMarks, &errs) |
| 41 | + if new == NilVal { |
| 42 | + new = v // completely unchanged |
| 43 | + } |
| 44 | + var err error |
| 45 | + switch len(errs) { |
| 46 | + case 0: |
| 47 | + // nil err is fine, then |
| 48 | + case 1: |
| 49 | + err = errs[0] |
| 50 | + default: |
| 51 | + err = errors.Join(errs...) |
| 52 | + } |
| 53 | + return new.WithMarks(topMarks), err |
| 54 | +} |
| 55 | + |
| 56 | +// wrangleMarksDeep is the main implementation of [WrangleMarksDeep], which |
| 57 | +// calls itself recursively to handle nested data structures. |
| 58 | +// |
| 59 | +// If the returned value is [NilVal] then that means that no changes were |
| 60 | +// needed to anything at or beneath that nesting level and so the caller should |
| 61 | +// just keep the original value exactly. |
| 62 | +// |
| 63 | +// Modifies topMarks and errs during traversal to collect (respectively) any |
| 64 | +// marks that caused [ctymarks.WrangleExpand] and and errors returned by |
| 65 | +// wrangle functions. |
| 66 | +func wrangleMarksDeep(v Value, wranglers []WrangleFunc, path Path, topMarks ValueMarks, errs *[]error) Value { |
| 67 | + var givenMarks, newMarks ValueMarks |
| 68 | + makeNewValue := false |
| 69 | + // The following is the same idea as [Value.Unmark], but implemented inline |
| 70 | + // here so that we can skip copying any existing ValueMarks that might |
| 71 | + // already be present, since we know we're not going to try to mutate it. |
| 72 | + if marked, ok := v.v.(marker); ok { |
| 73 | + v = Value{ |
| 74 | + ty: v.ty, |
| 75 | + v: marked.realV, |
| 76 | + } |
| 77 | + givenMarks = marked.marks |
| 78 | + } |
| 79 | + |
| 80 | + // We call this whenever we know we're returning a new value, to perform |
| 81 | + // a one-time copy of the given marks into a new marks map we can modify |
| 82 | + // and to set a flag to force us to construct a newly-marked value when |
| 83 | + // we return below. |
| 84 | + needNewValue := func() { |
| 85 | + if newMarks == nil && len(givenMarks) != 0 { |
| 86 | + newMarks = make(ValueMarks, len(givenMarks)) |
| 87 | + maps.Copy(newMarks, givenMarks) |
| 88 | + } |
| 89 | + makeNewValue = true |
| 90 | + } |
| 91 | + |
| 92 | + for mark := range givenMarks { |
| 93 | + Wranglers: |
| 94 | + for _, wrangler := range wranglers { |
| 95 | + action, err := wrangler(mark, path) |
| 96 | + if err != nil { |
| 97 | + if len(path) != 0 { |
| 98 | + err = path.NewError(err) |
| 99 | + } |
| 100 | + *errs = append(*errs, err) |
| 101 | + } |
| 102 | + switch action { |
| 103 | + case nil: |
| 104 | + continue Wranglers |
| 105 | + case ctymarks.WrangleKeep: |
| 106 | + break Wranglers |
| 107 | + case ctymarks.WrangleExpand: |
| 108 | + topMarks[mark] = struct{}{} |
| 109 | + break Wranglers |
| 110 | + case ctymarks.WrangleDrop: |
| 111 | + needNewValue() |
| 112 | + delete(newMarks, mark) |
| 113 | + break Wranglers |
| 114 | + default: |
| 115 | + newMark := ctymarks.WrangleReplaceMark(action) |
| 116 | + if newMark == nil { |
| 117 | + // Should not get here because these cases should be |
| 118 | + // exhaustive for all possible WrangleAction values. |
| 119 | + panic(fmt.Sprintf("unhandled WrangleAction %#v", action)) |
| 120 | + } |
| 121 | + needNewValue() |
| 122 | + delete(newMarks, mark) |
| 123 | + newMarks[newMark] = struct{}{} |
| 124 | + break Wranglers |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + // We're not going to make any further changes to our _direct_ marks |
| 130 | + // after this, so if we didn't already make a copy of the given marks |
| 131 | + // we can now safely alias our original set to reuse when we return. |
| 132 | + // (We might still construct a new value though, if we recurse into |
| 133 | + // a nested value that needs its own changes.) |
| 134 | + if newMarks == nil { |
| 135 | + newMarks = givenMarks // might still be nil if we didn't have any marks on entry |
| 136 | + } |
| 137 | + |
| 138 | + // Now we'll visit nested values recursively, if any. |
| 139 | + // The cases below intentionally don't cover primitive types, set types, |
| 140 | + // or capsule types, because none of them can possibly have nested marks |
| 141 | + // inside. (For set types in particular, any marks on inner values get |
| 142 | + // aggregated on the top-level set itself during construction.) |
| 143 | + ty := v.Type() |
| 144 | + switch { |
| 145 | + case v.IsNull() || !v.IsKnown(): |
| 146 | + // Can't recurse into null or unknown values, regardless of type, |
| 147 | + // so nothing to do here. |
| 148 | + |
| 149 | + case ty.IsListType() || ty.IsTupleType(): |
| 150 | + // These types both have the same internal representation, and we |
| 151 | + // know we're not going to change anything about the type, so we |
| 152 | + // can share the same implementation for both. |
| 153 | + l := v.LengthInt() |
| 154 | + if l == 0 { |
| 155 | + break // nothing to do for an empty container |
| 156 | + } |
| 157 | + |
| 158 | + // We'll avoid allocating a new slice until we know we're going |
| 159 | + // to make a change. |
| 160 | + var newElems []any // as would appear in Value.v for all three of these types |
| 161 | + for i, innerV := range replaceKWithIdx(v.Elements()) { |
| 162 | + path := append(path, IndexStep{Key: NumberIntVal(int64(i))}) |
| 163 | + newInnerV := wrangleMarksDeep(innerV, wranglers, path, topMarks, errs) |
| 164 | + if newInnerV != NilVal { |
| 165 | + needNewValue() |
| 166 | + if newElems == nil { |
| 167 | + // If this is the first change we've found then we need to |
| 168 | + // allocate our new elems array and retroactively copy |
| 169 | + // anything we previously skipped because it was unchanged. |
| 170 | + newElems = make([]any, i, l) |
| 171 | + copy(newElems, v.v.([]any)) |
| 172 | + } |
| 173 | + newElems = append(newElems, newInnerV.v) |
| 174 | + } else if newElems != nil { |
| 175 | + // Once we've started building a new value we need to append |
| 176 | + // everything to it whether it's changed or not, but we can |
| 177 | + // reuse the unchanged element's internal value. |
| 178 | + newElems = append(newElems, innerV.v) |
| 179 | + } |
| 180 | + } |
| 181 | + if newElems != nil { |
| 182 | + // if we built a new array of elements then it should replace |
| 183 | + // the one from our input value. |
| 184 | + v.v = newElems |
| 185 | + } |
| 186 | + |
| 187 | + case ty.IsMapType() || ty.IsObjectType(): |
| 188 | + // These types both have the same internal representation, and we |
| 189 | + // know we're not going to change anything about the type, so we |
| 190 | + // can share the same implementation for both. |
| 191 | + l := v.LengthInt() |
| 192 | + if l == 0 { |
| 193 | + break // nothing to do for an empty container |
| 194 | + } |
| 195 | + |
| 196 | + // We'll avoid allocating a new map until we know we're going to |
| 197 | + // make a change. |
| 198 | + var newElems map[string]any |
| 199 | + for keyV, innerV := range v.Elements() { |
| 200 | + var pathStep PathStep |
| 201 | + if ty.IsObjectType() { |
| 202 | + pathStep = GetAttrStep{Name: keyV.AsString()} |
| 203 | + } else { |
| 204 | + pathStep = IndexStep{Key: keyV} |
| 205 | + } |
| 206 | + path := append(path, pathStep) |
| 207 | + newInnerV := wrangleMarksDeep(innerV, wranglers, path, topMarks, errs) |
| 208 | + if newInnerV != NilVal { |
| 209 | + needNewValue() |
| 210 | + if newElems == nil { |
| 211 | + // If this is the first change we've found then we need to |
| 212 | + // allocate our new elems map and retroactively copy |
| 213 | + // everything from the original map before we overwrite |
| 214 | + // the elements that need to change. |
| 215 | + newElems = make(map[string]any, l) |
| 216 | + maps.Copy(newElems, v.v.(map[string]any)) |
| 217 | + } |
| 218 | + newElems[keyV.AsString()] = newInnerV.v |
| 219 | + } |
| 220 | + } |
| 221 | + if newElems != nil { |
| 222 | + // if we built a new map of elements then it should replace |
| 223 | + // the one from our input value. |
| 224 | + v.v = newElems |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + if !makeNewValue { |
| 229 | + // We didn't make any changes to the marks, so we don't need to |
| 230 | + // construct a new value. |
| 231 | + return NilVal |
| 232 | + } |
| 233 | + return v.WithMarks(newMarks) |
| 234 | +} |
| 235 | + |
| 236 | +// WrangleFunc is the signature of a callback function used to visit a |
| 237 | +// particular mark associated with a particular path within a value. |
| 238 | +// |
| 239 | +// [Path] values passed to successive calls to a [WrangleFunc] may share a |
| 240 | +// backing array, and so if the function wishes to retain a particular path |
| 241 | +// after it returns it must use [Path.Copy] to produce a copy in an unaliased |
| 242 | +// backing array. |
| 243 | +// |
| 244 | +// A function of this type must decide what change to make, if any, to the |
| 245 | +// presence of this mark at this location. Returning nil means to take no |
| 246 | +// action at all and to potentially allow other later functions of this |
| 247 | +// type to decide what to do instead. |
| 248 | +// |
| 249 | +// If the function returns an error then [Value.WrangleMarksDeep] collects it |
| 250 | +// and any other errors returned during traversal, automatically wraps in a |
| 251 | +// [cty.PathError] if not at the root, and returns an [`errors.Join`] of all |
| 252 | +// of the errors if there are more than one. The indicated action is still taken |
| 253 | +// and continued "wrangling" occurs as normal to visit other marks and other |
| 254 | +// paths. |
| 255 | +// |
| 256 | +// [cty.Value.WrangleMarksDeep], and this callback signature used with it, |
| 257 | +// are together designed with some assumptions that don't always hold but |
| 258 | +// have been common enough to make it seem worth supporting with a first-class |
| 259 | +// feature: |
| 260 | +// |
| 261 | +// - All of the different marks on any specific value are orthogonal to one |
| 262 | +// another, and so it's possible to decide an action for each one in |
| 263 | +// isolation. |
| 264 | +// - Marks within a data structure are orthogonal to the specific values they |
| 265 | +// are associated with, and so it's possible to decide an action without |
| 266 | +// knowning the value it's associated with. (Though it's possible in |
| 267 | +// principle to use the given path to retrieve more information when needed, |
| 268 | +// at the expense of some additional traversal overhead.) |
| 269 | +// - Most values have no marks at all and when marks are present there are |
| 270 | +// relatively few of them, and so it's worth making some extra effort to |
| 271 | +// handle the no-marks case cheaply even if it makes the marks-present case |
| 272 | +// a little more expensive. |
| 273 | +// |
| 274 | +// If any of these assumptions don't apply to your situation then this may not |
| 275 | +// be an appropriate solution. [cty.Transform] or [cty.TransformWithTransformer] |
| 276 | +// might serve as a more general alternative if you need more control. |
| 277 | +type WrangleFunc func(mark any, path Path) (ctymarks.WrangleAction, error) |
| 278 | + |
| 279 | +func replaceKWithIdx[K any, V any](in iter.Seq2[K, V]) iter.Seq2[int, V] { |
| 280 | + return func(yield func(int, V) bool) { |
| 281 | + i := 0 |
| 282 | + for _, v := range in { |
| 283 | + if !yield(i, v) { |
| 284 | + break |
| 285 | + } |
| 286 | + i++ |
| 287 | + } |
| 288 | + } |
| 289 | +} |
0 commit comments