Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/common/autogen/customtypes/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ type ListValueInterface interface {
Elements() []attr.Value
}

func (v ListValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.ListValue.ElementType(ctx) == nil {
// ListValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.ListValue = NewListValueNull[T](ctx).ListValue
}
return v.ListValue.ToTerraformValue(ctx)
}

func (v ListValue[T]) NewListValue(ctx context.Context, value []attr.Value) ListValueInterface {
return NewListValue[T](ctx, value)
}
Expand Down
172 changes: 172 additions & 0 deletions internal/common/autogen/customtypes/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package customtypes

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

/*
Custom Map type used in auto-generated code to enable the generic marshal/unmarshal operations to access the elements' type during conversion.
Custom types docs: https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/custom

Usage:
- Schema definition:
"sample_string_map": schema.MapAttribute{
...
CustomType: customtypes.NewMapType[basetypes.StringValue](ctx),
ElementType: types.StringType,
}

- TF Models:
type TFModel struct {
SampleStringMap customtypes.MapValue[basetypes.StringValue] `tfsdk:"sample_string_map"`
...
}
*/

var (
_ basetypes.MapTypable = MapType[basetypes.StringValue]{}
_ basetypes.MapValuable = MapValue[basetypes.StringValue]{}
_ MapValueInterface = MapValue[basetypes.StringValue]{}
)

type MapType[T attr.Value] struct {
basetypes.MapType
}

func NewMapType[T attr.Value](ctx context.Context) MapType[T] {
elemType := getElemType[T](ctx)
return MapType[T]{
MapType: basetypes.MapType{ElemType: elemType},
}
}

func (t MapType[T]) Equal(o attr.Type) bool {
other, ok := o.(MapType[T])
if !ok {
return false
}

return t.MapType.Equal(other.MapType)
}

func (MapType[T]) String() string {
var t T
return fmt.Sprintf("MapType[%T]", t)
}

func (t MapType[T]) ValueFromMap(ctx context.Context, in basetypes.MapValue) (basetypes.MapValuable, diag.Diagnostics) {
if in.IsNull() {
return NewMapValueNull[T](ctx), nil
}

if in.IsUnknown() {
return NewMapValueUnknown[T](ctx), nil
}

elemType := getElemType[T](ctx)
baseMapValue, diags := basetypes.NewMapValue(elemType, in.Elements())
if diags.HasError() {
return nil, diags
}

return MapValue[T]{MapValue: baseMapValue}, nil
}

func (t MapType[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.MapType.ValueFromTerraform(ctx, in)

if err != nil {
return nil, err
}

mapValue, ok := attrValue.(basetypes.MapValue)
if !ok {
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}

mapValuable, diags := t.ValueFromMap(ctx, mapValue)
if diags.HasError() {
return nil, fmt.Errorf("unexpected error converting MapValue to MapValuable: %v", diags)
}

return mapValuable, nil
}

func (t MapType[T]) ValueType(_ context.Context) attr.Value {
return MapValue[T]{}
}

type MapValue[T attr.Value] struct {
basetypes.MapValue
}

type MapValueInterface interface {
basetypes.MapValuable
NewMapValue(ctx context.Context, value map[string]attr.Value) MapValueInterface
NewMapValueNull(ctx context.Context) MapValueInterface
ElementType(ctx context.Context) attr.Type
Elements() map[string]attr.Value
}

func (v MapValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.MapValue.ElementType(ctx) == nil {
// MapValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.MapValue = NewMapValueNull[T](ctx).MapValue
}
return v.MapValue.ToTerraformValue(ctx)
}

func (v MapValue[T]) NewMapValue(ctx context.Context, value map[string]attr.Value) MapValueInterface {
return NewMapValue[T](ctx, value)
}

func NewMapValue[T attr.Value](ctx context.Context, value map[string]attr.Value) MapValue[T] {
elemType := getElemType[T](ctx)

mapValue, diags := basetypes.NewMapValue(elemType, value)
if diags.HasError() {
return NewMapValueUnknown[T](ctx)
}

return MapValue[T]{MapValue: mapValue}
}

func (v MapValue[T]) NewMapValueNull(ctx context.Context) MapValueInterface {
return NewMapValueNull[T](ctx)
}

func NewMapValueNull[T attr.Value](ctx context.Context) MapValue[T] {
elemType := getElemType[T](ctx)
return MapValue[T]{MapValue: basetypes.NewMapNull(elemType)}
}

func NewMapValueUnknown[T attr.Value](ctx context.Context) MapValue[T] {
elemType := getElemType[T](ctx)
return MapValue[T]{MapValue: basetypes.NewMapUnknown(elemType)}
}

func (v MapValue[T]) Equal(o attr.Value) bool {
other, ok := o.(MapValue[T])
if !ok {
return false
}
return v.MapValue.Equal(other.MapValue)
}

func (v MapValue[T]) Type(ctx context.Context) attr.Type {
return NewMapType[T](ctx)
}

func (v MapValue[T]) ElementType(ctx context.Context) attr.Type {
return getElemType[T](ctx)
}

func (v MapValue[T]) Elements() map[string]attr.Value {
return v.MapValue.Elements()
}
8 changes: 8 additions & 0 deletions internal/common/autogen/customtypes/nested_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ type NestedListValueInterface interface {
Len() int
}

func (v NestedListValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.ElementType(ctx) == nil {
// NestedListValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.ListValue = NewNestedListValueNull[T](ctx).ListValue
}
return v.ListValue.ToTerraformValue(ctx)
}

func (v NestedListValue[T]) NewNestedListValue(ctx context.Context, value any) NestedListValueInterface {
return NewNestedListValue[T](ctx, value)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/common/autogen/customtypes/nested_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ type NestedMapValueInterface interface {
NewEmptyMapPtr() any
}

func (v NestedMapValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.ElementType(ctx) == nil {
// NestedMapValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.MapValue = NewNestedMapValueNull[T](ctx).MapValue
}
return v.MapValue.ToTerraformValue(ctx)
}

func (v NestedMapValue[T]) NewNestedMapValue(ctx context.Context, value any) NestedMapValueInterface {
return NewNestedMapValue[T](ctx, value)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/common/autogen/customtypes/nested_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ type NestedSetValueInterface interface {
Len() int
}

func (v NestedSetValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.ElementType(ctx) == nil {
// NestedSetValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.SetValue = NewNestedSetValueNull[T](ctx).SetValue
}
return v.SetValue.ToTerraformValue(ctx)
}

func (v NestedSetValue[T]) NewNestedSetValue(ctx context.Context, value any) NestedSetValueInterface {
return NewNestedSetValue[T](ctx, value)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/common/autogen/customtypes/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ type ObjectValueInterface interface {
ValuePtrAsAny(ctx context.Context) (any, diag.Diagnostics)
}

func (v ObjectValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.ObjectValue.Equal(basetypes.ObjectValue{}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Is there a reason why this implementation deviates from other implementations which check for null?

if v.ElementType(ctx) == nil {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike other types, which just return the underlying elementType, the basetypes.ObjectValue.AttributeTypes() implementation creates and returns a map:

// AttributeTypes returns a copy of the mapping of attribute types for the Object.
func (o ObjectValue) AttributeTypes(_ context.Context) map[string]attr.Type {
	// Ensure callers cannot mutate the internal attribute types
	result := make(map[string]attr.Type, len(o.attributeTypes))

	for name, typ := range o.attributeTypes {
		result[name] = typ
	}

	return result
}

At which point, its better to compare against a zero-value.

// ObjectValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.ObjectValue = NewObjectValueNull[T](ctx).ObjectValue
}
return v.ObjectValue.ToTerraformValue(ctx)
}

func (v ObjectValue[T]) NewObjectValue(ctx context.Context, value any) ObjectValueInterface {
return NewObjectValue[T](ctx, value)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/common/autogen/customtypes/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ type SetValueInterface interface {
Elements() []attr.Value
}

func (v SetValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if v.SetValue.ElementType(ctx) == nil {
// SetValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
v.SetValue = NewSetValueNull[T](ctx).SetValue
}
return v.SetValue.ToTerraformValue(ctx)
}

func (v SetValue[T]) NewSetValue(ctx context.Context, value []attr.Value) SetValueInterface {
return NewSetValue[T](ctx, value)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/common/autogen/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func getModelAttr(val attr.Value, isUpdate bool) (any, error) {
return getMapAttr(v.Attributes(), false, isUpdate)
case types.Map:
return getMapAttr(v.Elements(), true, isUpdate)
case customtypes.MapValueInterface:
return getMapAttr(v.Elements(), true, isUpdate)
case types.List:
return getListAttr(v.Elements(), isUpdate)
case customtypes.ListValueInterface:
Expand Down
13 changes: 13 additions & 0 deletions internal/common/autogen/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func TestMarshalNestedAllTypes(t *testing.T) {
AttrCustomSet customtypes.SetValue[types.String] `tfsdk:"attr_custom_set"`
AttrSetObj types.Set `tfsdk:"attr_set_obj"`
AttrMapSimple types.Map `tfsdk:"attr_map_simple"`
AttrCustomMap customtypes.MapValue[types.String] `tfsdk:"attr_custom_map"`
AttrMapObj types.Map `tfsdk:"attr_map_obj"`
}{
AttrString: types.StringValue("val"),
Expand All @@ -143,6 +144,10 @@ func TestMarshalNestedAllTypes(t *testing.T) {
"keyOne": types.StringValue("val1"),
"KeyTwo": types.StringValue("val2"), // don't change the key case when it's a map
}),
AttrCustomMap: customtypes.NewMapValue[types.String](t.Context(), map[string]attr.Value{
"keyOne": types.StringValue("val1"),
"KeyTwo": types.StringValue("val2"),
}),
AttrMapObj: attrMapObj,
}
const expectedJSON = `
Expand All @@ -164,6 +169,10 @@ func TestMarshalNestedAllTypes(t *testing.T) {
"keyOne": "val1",
"KeyTwo": "val2"
},
"attrCustomMap": {
"keyOne": "val1",
"KeyTwo": "val2"
},
"attrMapObj": {
"keyOne": { "attrString": "str1", "attrInt": 1 },
"KeyTwo": { "attrString": "str2", "attrInt": 2 }
Expand Down Expand Up @@ -268,6 +277,8 @@ func TestMarshalUpdateNull(t *testing.T) {
AttrCustomList customtypes.ListValue[types.String] `tfsdk:"attr_custom_list"`
AttrSet types.Set `tfsdk:"attr_set"`
AttrCustomSet customtypes.SetValue[types.String] `tfsdk:"attr_custom_set"`
AttrMap types.Map `tfsdk:"attr_map"`
AttrCustomMap customtypes.MapValue[types.String] `tfsdk:"attr_custom_map"`
AttrString types.String `tfsdk:"attr_string"`
AttrObj types.Object `tfsdk:"attr_obj"`
AttrIncludeString types.String `tfsdk:"attr_include_update" autogen:"includenullonupdate"`
Expand All @@ -277,6 +288,8 @@ func TestMarshalUpdateNull(t *testing.T) {
AttrCustomList: customtypes.NewListValueNull[types.String](t.Context()),
AttrSet: types.SetNull(types.StringType),
AttrCustomSet: customtypes.NewSetValueNull[types.String](t.Context()),
AttrMap: types.MapNull(types.StringType),
AttrCustomMap: customtypes.NewMapValueNull[types.String](t.Context()),
AttrString: types.StringNull(),
AttrObj: types.ObjectNull(objTypeTest.AttrTypes),
AttrIncludeString: types.StringNull(),
Expand Down
6 changes: 6 additions & 0 deletions internal/common/autogen/unknown.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ func prepareAttr(value attr.Value) (attr.Value, error) {
}
// If known, no need to process each set item since unmarshal does not generate unknown attributes.
return v, nil
case customtypes.MapValueInterface:
if v.IsUnknown() {
return v.NewMapValueNull(ctx), nil
}
// If known, no need to process each map entry since unmarshal does not generate unknown attributes.
return v, nil
case customtypes.NestedMapValueInterface:
if v.IsUnknown() {
return v.NewNestedMapValueNull(ctx), nil
Expand Down
3 changes: 3 additions & 0 deletions internal/common/autogen/unknown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestResolveUnknowns(t *testing.T) {
AttrCustomObject customtypes.ObjectValue[modelCustomTypeTest] `tfsdk:"attr_custom_object"`
AttrCustomListUnknown customtypes.ListValue[types.String] `tfsdk:"attr_custom_list_string"`
AttrCustomSetUnknown customtypes.SetValue[types.String] `tfsdk:"attr_custom_set_string"`
AttrCustomMapUnknown customtypes.MapValue[types.String] `tfsdk:"attr_custom_map_string"`
AttrCustomNestedListUnknown customtypes.NestedListValue[modelEmptyTest] `tfsdk:"attr_custom_nested_list_unknown"`
AttrCustomNestedSetUnknown customtypes.NestedSetValue[modelEmptyTest] `tfsdk:"attr_custom_nested_set_unknown"`
AttrCustomNestedMapUnknown customtypes.NestedMapValue[modelEmptyTest] `tfsdk:"attr_custom_nested_map_unknown"`
Expand Down Expand Up @@ -99,6 +100,7 @@ func TestResolveUnknowns(t *testing.T) {
AttrCustomNestedMapUnknown: customtypes.NewNestedMapValueUnknown[modelEmptyTest](ctx),
AttrCustomListUnknown: customtypes.NewListValueUnknown[types.String](ctx),
AttrCustomSetUnknown: customtypes.NewSetValueUnknown[types.String](ctx),
AttrCustomMapUnknown: customtypes.NewMapValueUnknown[types.String](ctx),
AttrCustomNestedList: customtypes.NewNestedListValue[modelCustomTypeTest](ctx, []modelCustomTypeTest{
{
AttrKnownString: types.StringValue("val1"),
Expand Down Expand Up @@ -176,6 +178,7 @@ func TestResolveUnknowns(t *testing.T) {
}),
AttrCustomListUnknown: customtypes.NewListValueNull[types.String](ctx),
AttrCustomSetUnknown: customtypes.NewSetValueNull[types.String](ctx),
AttrCustomMapUnknown: customtypes.NewMapValueNull[types.String](ctx),
AttrCustomNestedListUnknown: customtypes.NewNestedListValueNull[modelEmptyTest](ctx),
AttrCustomNestedSetUnknown: customtypes.NewNestedSetValueNull[modelEmptyTest](ctx),
AttrCustomNestedMapUnknown: customtypes.NewNestedMapValueNull[modelEmptyTest](ctx),
Expand Down
Loading