| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package collections | |
| import ( | |
| "reflect" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/computed" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/plans/objchange" | |
| ) | |
| type TransformIndices func(before, after int) computed.Diff | |
| type ProcessIndices func(before, after int) | |
| type IsObjType[Input any] func(input Input) bool | |
| func TransformSlice[Input any](before, after []Input, process TransformIndices, isObjType IsObjType[Input]) ([]computed.Diff, plans.Action) { | |
| current := plans.NoOp | |
| if before != nil && after == nil { | |
| current = plans.Delete | |
| } | |
| if before == nil && after != nil { | |
| current = plans.Create | |
| } | |
| var elements []computed.Diff | |
| ProcessSlice(before, after, func(before, after int) { | |
| element := process(before, after) | |
| elements = append(elements, element) | |
| current = CompareActions(current, element.Action) | |
| }, isObjType) | |
| return elements, current | |
| } | |
| func ProcessSlice[Input any](before, after []Input, process ProcessIndices, isObjType IsObjType[Input]) { | |
| // If before and after are the same length and is not a reordering | |
| // we want to compare elements on an individual basis | |
| if len(before) == len(after) && !isReorder(before, after) { | |
| for ix := range before { | |
| process(ix, ix) | |
| } | |
| return | |
| } | |
| lcs := objchange.LongestCommonSubsequence(before, after, func(before, after Input) bool { | |
| return reflect.DeepEqual(before, after) | |
| }) | |
| var beforeIx, afterIx, lcsIx int | |
| for beforeIx < len(before) || afterIx < len(after) || lcsIx < len(lcs) { | |
| // Step through all the before values until we hit the next item in the | |
| // longest common subsequence. We are going to just say that all of | |
| // these have been deleted. | |
| for beforeIx < len(before) && (lcsIx >= len(lcs) || !reflect.DeepEqual(before[beforeIx], lcs[lcsIx])) { | |
| isObjectDiff := isObjType(before[beforeIx]) && afterIx < len(after) && isObjType(after[afterIx]) && (lcsIx >= len(lcs) || !reflect.DeepEqual(after[afterIx], lcs[lcsIx])) | |
| if isObjectDiff { | |
| process(beforeIx, afterIx) | |
| beforeIx++ | |
| afterIx++ | |
| continue | |
| } | |
| process(beforeIx, len(after)) | |
| beforeIx++ | |
| } | |
| // Now, step through all the after values until hit the next item in the | |
| // LCS. We are going to say that all of these have been created. | |
| for afterIx < len(after) && (lcsIx >= len(lcs) || !reflect.DeepEqual(after[afterIx], lcs[lcsIx])) { | |
| process(len(before), afterIx) | |
| afterIx++ | |
| } | |
| // Finally, add the item in common as unchanged. | |
| if lcsIx < len(lcs) { | |
| process(beforeIx, afterIx) | |
| beforeIx++ | |
| afterIx++ | |
| lcsIx++ | |
| } | |
| } | |
| } | |
| // isReorder returns true if every item of before can be found in after | |
| func isReorder[Input any](before, after []Input) bool { | |
| // To be a reorder the length needs to be the same | |
| if len(before) != len(after) { | |
| return false | |
| } | |
| for _, b := range before { | |
| hasMatch := false | |
| for _, a := range after { | |
| if reflect.DeepEqual(b, a) { | |
| // Match found, no need to search anymore | |
| hasMatch = true | |
| break | |
| } | |
| } | |
| if !hasMatch { | |
| return false | |
| } | |
| } | |
| return true | |
| } |