internal/utils/tfsdk.go (222 lines of code) (raw):

package utils import ( "context" "encoding/json" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) type ListMeta struct { Context context.Context Index int Path path.Path Diags *diag.Diagnostics } type MapMeta struct { Context context.Context Key string Path path.Path Diags *diag.Diagnostics } type ObjectMeta struct { Context context.Context Path path.Path Diags *diag.Diagnostics } // ====================== // ===== Primitives ===== // ====================== // ValueStringPointer returns nil if unknown, otherwise the same as value.ValueStringPointer(). // Useful for computed optional fields without a default value, as these unknown values // return a pointer to an empty string. func ValueStringPointer(value types.String) *string { if value.IsUnknown() { return nil } return value.ValueStringPointer() } // ================ // ===== Maps ===== // ================ // MapToNormalizedType marshals a map[string]T into a jsontypes.Normalized. func MapToNormalizedType[T any](value map[string]T, p path.Path, diags *diag.Diagnostics) jsontypes.Normalized { if value == nil { return jsontypes.NewNormalizedNull() } bytes, err := json.Marshal(value) if err != nil { diags.AddAttributeError(p, "marshal failure", err.Error()) } return jsontypes.NewNormalizedValue(string(bytes)) } // NormalizedTypeToMap unmarshals a jsontypes.Normalized to a map[string]T. func NormalizedTypeToMap[T any](value jsontypes.Normalized, p path.Path, diags *diag.Diagnostics) map[string]T { if !IsKnown(value) { return nil } var dest map[string]T d := value.Unmarshal(&dest) diags.Append(ConvertToAttrDiags(d, p)...) return dest } // MapToMapType converts a tfsdk naive map[string]T1 into an types.Map of map[string]T2. // This handles both structs and simple types to attr.Values. func MapToMapType[T1 any, T2 any](ctx context.Context, value map[string]T1, elemType attr.Type, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta MapMeta) T2) types.Map { if value == nil { return types.MapNull(elemType) } elems := TransformMap(ctx, value, p, diags, iteratee) mapping, d := types.MapValueFrom(ctx, elemType, elems) diags.Append(ConvertToAttrDiags(d, p)...) return mapping } // MapTypeToMap converts a types.Map first into a tfsdk aware map[string]T1 and transforms // the result into a map[string]T2. func MapTypeToMap[T1 any, T2 any](ctx context.Context, value types.Map, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta MapMeta) T2) map[string]T2 { if !IsKnown(value) { return nil } elems := MapTypeAs[T1](ctx, value, p, diags) if diags.HasError() { return nil } return TransformMap(ctx, elems, p, diags, iteratee) } // MapTypeAs converts a types.Map into a tfsdk aware map[string]T. func MapTypeAs[T any](ctx context.Context, value types.Map, p path.Path, diags *diag.Diagnostics) map[string]T { if !IsKnown(value) { return nil } var items map[string]T d := value.ElementsAs(ctx, &items, false) diags.Append(ConvertToAttrDiags(d, p)...) return items } // MapValueFrom converts a tfsdk aware map[string]T to a types.Map. func MapValueFrom[T any](ctx context.Context, value map[string]T, elemType attr.Type, p path.Path, diags *diag.Diagnostics) types.Map { mapping, d := types.MapValueFrom(ctx, elemType, value) diags.Append(ConvertToAttrDiags(d, p)...) return mapping } // ================= // ===== Lists ===== // ================= // SliceToListType converts a tfsdk naive []T1 into an types.List of []T2. // This handles both structs and simple types to attr.Values. func SliceToListType[T1 any, T2 any](ctx context.Context, value []T1, elemType attr.Type, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) types.List { if value == nil { return types.ListNull(elemType) } elems := TransformSlice(ctx, value, p, diags, iteratee) list, nd := types.ListValueFrom(ctx, elemType, elems) diags.Append(ConvertToAttrDiags(nd, p)...) return list } // SliceToListType_String converts a tfsdk naive []string into a types.List. // This is a shorthand SliceToListType helper for strings. func SliceToListType_String(ctx context.Context, value []string, p path.Path, diags *diag.Diagnostics) types.List { return SliceToListType(ctx, value, types.StringType, p, diags, func(item string, meta ListMeta) types.String { return types.StringValue(item) }) } // ListTypeToMap converts a types.List first into a tfsdk aware map[string]T1 // and transforms the result into a map[string]T2. func ListTypeToMap[T1 any, T2 any](ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) (key string, elem T2)) map[string]T2 { if !IsKnown(value) { return nil } items := ListTypeAs[T1](ctx, value, p, diags) if diags.HasError() { return nil } return TransformSliceToMap(ctx, items, p, diags, iteratee) } // ListTypeToSlice converts a types.List first into a tfsdk aware []T1 and transforms // the result into a []T2. func ListTypeToSlice[T1 any, T2 any](ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) []T2 { if !IsKnown(value) { return nil } elems := ListTypeAs[T1](ctx, value, p, diags) if diags.HasError() { return nil } return TransformSlice(ctx, elems, p, diags, iteratee) } // ListTypeToSlice_String converts a types.List into a []string. // This is a shorthand ListTypeToSlice helper for strings. func ListTypeToSlice_String(ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics) []string { return ListTypeToSlice(ctx, value, p, diags, func(item types.String, meta ListMeta) string { return item.ValueString() }) } // ListTypeAs converts a types.List into a tfsdk aware []T. func ListTypeAs[T any](ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics) []T { if !IsKnown(value) { return nil } var items []T nd := value.ElementsAs(ctx, &items, false) diags.Append(ConvertToAttrDiags(nd, p)...) return items } // ListValueFrom converts a tfsdk aware []T to a types.List. func ListValueFrom[T any](ctx context.Context, value []T, elemType attr.Type, p path.Path, diags *diag.Diagnostics) types.List { list, d := types.ListValueFrom(ctx, elemType, value) diags.Append(ConvertToAttrDiags(d, p)...) return list } // =================== // ===== Objects ===== // =================== // StructToObjectType converts a tfsdk naive T1 into an types.Object of T2. func StructToObjectType[T1 any, T2 any](ctx context.Context, value *T1, attrTypes map[string]attr.Type, p path.Path, diags *diag.Diagnostics, transformee func(item T1, meta ObjectMeta) T2) types.Object { if value == nil { return types.ObjectNull(attrTypes) } item := TransformObject(ctx, value, p, diags, transformee) obj, d := types.ObjectValueFrom(ctx, attrTypes, item) diags.Append(ConvertToAttrDiags(d, p)...) return obj } // ObjectTypeToStruct converts a types.Object first into a tfsdk aware T1 and transforms // the result into a T2. func ObjectTypeToStruct[T1 any, T2 any](ctx context.Context, value types.Object, p path.Path, diags *diag.Diagnostics, transformee func(item T1, meta ObjectMeta) T2) *T2 { if !IsKnown(value) { return nil } item := ObjectTypeAs[T1](ctx, value, p, diags) if diags.HasError() { return nil } return TransformObject(ctx, item, p, diags, transformee) } // ObjectTypeAs converts a types.Object into a tfsdk aware T. func ObjectTypeAs[T any](ctx context.Context, value types.Object, p path.Path, diags *diag.Diagnostics) *T { if !IsKnown(value) { return nil } var item T d := value.As(ctx, &item, basetypes.ObjectAsOptions{}) diags.Append(ConvertToAttrDiags(d, p)...) return &item } // ObjectValueFrom converts a tfsdk aware T to a types.Object. func ObjectValueFrom[T any](ctx context.Context, value *T, attrTypes map[string]attr.Type, p path.Path, diags *diag.Diagnostics) types.Object { obj, d := types.ObjectValueFrom(ctx, attrTypes, value) diags.Append(ConvertToAttrDiags(d, p)...) return obj } // ====================== // ===== Transforms ===== // ====================== // TransformObject converts T1 to T2 via the transformee. func TransformObject[T1 any, T2 any](ctx context.Context, value *T1, p path.Path, diags *diag.Diagnostics, transformee func(item T1, meta ObjectMeta) T2) *T2 { if value == nil { return nil } result := transformee(*value, ObjectMeta{Context: ctx, Path: p, Diags: diags}) return &result } // TransformMap converts map[string]T1 to map[string]T2 via the iteratee. func TransformMap[T1 any, T2 any](ctx context.Context, value map[string]T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta MapMeta) T2) map[string]T2 { if value == nil { return nil } elems := make(map[string]T2, len(value)) for k, v := range value { elems[k] = iteratee(v, MapMeta{Context: ctx, Key: k, Path: p.AtMapKey(k), Diags: diags}) } return elems } // TransformSlice converts []T1 to []T2 via the iteratee. func TransformSlice[T1 any, T2 any](ctx context.Context, value []T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) []T2 { if value == nil { return nil } elems := make([]T2, len(value)) for i, v := range value { elems[i] = iteratee(v, ListMeta{Context: ctx, Index: i, Path: p.AtListIndex(i), Diags: diags}) } return elems } // TransformSliceToMap converts []T1 to map[string]]T2 via the iteratee. func TransformSliceToMap[T1 any, T2 any](ctx context.Context, value []T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) (key string, elem T2)) map[string]T2 { if value == nil { return nil } elems := make(map[string]T2, len(value)) for i, v := range value { k, v := iteratee(v, ListMeta{Context: ctx, Index: i, Path: p.AtListIndex(i), Diags: diags}) elems[k] = v } return elems } // TransformSliceToMap converts []T1 to map[string]]T2 via the iteratee. func TransformMapToSlice[T1 any, T2 any](ctx context.Context, value map[string]T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta MapMeta) T2) []T2 { if value == nil { return nil } elems := make([]T2, 0, len(value)) for k, v := range value { v := iteratee(v, MapMeta{Context: ctx, Key: k, Path: p.AtMapKey(k), Diags: diags}) elems = append(elems, v) } return elems }