From 8cb51f415b66163a1cad172b7df9f8748a900a8b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 4 Jan 2024 15:18:56 -0800 Subject: [PATCH] collections: go-cmp helpers for the collection types A downside of using custom collection types is that go-cmp doesn't support them automatically, and so we need to help it out a little by providing additional options. An additional awkwardness is that go-cmp does all of its work at runtime using reflection, at which point the generic types have all been erased and lowered to many separate concrete types. Therefore we need some helper functions to produce a dynamically representation using interface types, cautiously exposing some internal details of these types only for go-cmp's use. The dynamic adapter here unfortunately means that the compiler likely won't be able to optimize away CmpOptions in non-test builds, but we'll accept that for now since the cost of that inert data and code is relatively small. If it turns out to be a problem then we'll need to find a different strategy, but hopefully not. --- internal/collections/cmp.go | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 internal/collections/cmp.go diff --git a/internal/collections/cmp.go b/internal/collections/cmp.go new file mode 100644 index 0000000000..3d27a02ecb --- /dev/null +++ b/internal/collections/cmp.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import ( + "github.com/google/go-cmp/cmp" +) + +// CmpOptions is a set of options for use with the "go-cmp" module when +// comparing data structures that contain collection types from this package. +// +// Specifically, these options arrange for [Set] and [Map] values to be +// transformed into map[any]any to allow for element-based comparisons. +// +// [Set] of T values transform into a map whose keys have dynamic type +// UniqueKey[T] and whose values have dynamic type T. +// +// [Map] of K, V values transform into a map whose keys have dynamic type +// UniqueKey[K] and whose values have dynamic type MapElem[K, V]. +var CmpOptions cmp.Option + +func init() { + CmpOptions = cmp.Options([]cmp.Option{ + cmp.Transformer("collectionElementsRaw", func(v transformerForCmp) any { + return v.transformForCmp() + }), + }) +} + +// transformerForCmp is a helper interface implemented by both `Set` and `Map` +// types, to work around the fact that go-cmp does all its work with reflection +// and thus cannot rely on the static type information from the type +// parameters. +type transformerForCmp interface { + transformForCmp() any +} + +func (s Set[T]) transformForCmp() any { + ret := make(map[any]any, s.Len()) + // It's okay to access the keys here because this package is allowed to + // depend on its own implementation details. + for k, v := range s.Elems() { + ret[k] = v + } + return ret +} + +func (m Map[K, V]) transformForCmp() any { + ret := make(map[any]any, m.Len()) + // It's okay to access the keys here because this package is allowed to + // depend on its own implementation details. + for k, v := range m.Elems() { + ret[k] = v + } + return ret +}