diff --git a/apstra/custom_types/ipv46_address_type.go b/apstra/custom_types/ipv46_address_type.go index 9ea1581e..7b80c028 100644 --- a/apstra/custom_types/ipv46_address_type.go +++ b/apstra/custom_types/ipv46_address_type.go @@ -3,6 +3,7 @@ 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" @@ -50,19 +51,16 @@ func (t IPv46AddressType) ValueFromString(_ context.Context, in basetypes.String // for the provider to consume the data with. func (t IPv46AddressType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { attrValue, err := t.StringType.ValueFromTerraform(ctx, in) - if err != nil { return nil, err } stringValue, ok := attrValue.(basetypes.StringValue) - if !ok { return nil, fmt.Errorf("unexpected value type of %T", attrValue) } stringValuable, diags := t.ValueFromString(ctx, stringValue) - if diags.HasError() { return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) } diff --git a/apstra/custom_types/ipv46_address_value.go b/apstra/custom_types/ipv46_address_value.go index 1f69713d..7172f3a9 100644 --- a/apstra/custom_types/ipv46_address_value.go +++ b/apstra/custom_types/ipv46_address_value.go @@ -71,7 +71,6 @@ func (v IPv46Address) Type(_ context.Context) attr.Type { func (v IPv46Address) Equal(o attr.Value) bool { other, ok := o.(IPv46Address) - if !ok { return false } diff --git a/apstra/custom_types/string_with_alt_values_type.go b/apstra/custom_types/string_with_alt_values_type.go new file mode 100644 index 00000000..9d354270 --- /dev/null +++ b/apstra/custom_types/string_with_alt_values_type.go @@ -0,0 +1,69 @@ +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" +) + +var ( + _ basetypes.StringTypable = (*StringWithAltValuesType)(nil) + _ attr.Type = (*StringWithAltValuesType)(nil) +) + +type StringWithAltValuesType struct { + basetypes.StringType +} + +// String returns a human readable string of the type name. +func (t StringWithAltValuesType) String() string { + return "customtypes.StringWithAltValues" +} + +// ValueType returns the Value type. +func (t StringWithAltValuesType) ValueType(_ context.Context) attr.Value { + return StringWithAltValues{} +} + +// Equal returns true if the given type is equivalent. +func (t StringWithAltValuesType) Equal(o attr.Type) bool { + other, ok := o.(StringWithAltValuesType) + + if !ok { + return false + } + + return t.StringType.Equal(other.StringType) +} + +// ValueFromString returns a StringValuable type given a StringValue. +func (t StringWithAltValuesType) ValueFromString(_ context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { + return StringWithAltValues{ + StringValue: in, + }, nil +} + +// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to convert the tftypes.Value into a more convenient Go type +// for the provider to consume the data with. +func (t StringWithAltValuesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.StringType.ValueFromTerraform(ctx, in) + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.StringValue) + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromString(ctx, stringValue) + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) + } + + return stringValuable, nil +} diff --git a/apstra/custom_types/string_with_alt_values_type_test.go b/apstra/custom_types/string_with_alt_values_type_test.go new file mode 100644 index 00000000..b7f9e5bf --- /dev/null +++ b/apstra/custom_types/string_with_alt_values_type_test.go @@ -0,0 +1,56 @@ +package customtypes_test + +import ( + "context" + "testing" + + customtypes "github.com/Juniper/terraform-provider-apstra/apstra/custom_types" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/stretchr/testify/require" +) + +func TestStringWithAltValuesType_ValueFromTerraform(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in tftypes.Value + expectation attr.Value + expectedErr string + }{ + "true": { + in: tftypes.NewValue(tftypes.String, "foo"), + expectation: customtypes.NewStringWithAltValuesValue("foo"), + }, + "unknown": { + in: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + expectation: customtypes.NewStringWithAltValuesUnknown(), + }, + "null": { + in: tftypes.NewValue(tftypes.String, nil), + expectation: customtypes.NewStringWithAltValuesNull(), + }, + "wrongType": { + in: tftypes.NewValue(tftypes.Number, 123), + expectedErr: "can't unmarshal tftypes.Number into *string, expected string", + }, + } + + for tName, tCase := range testCases { + t.Run(tName, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + got, err := customtypes.StringWithAltValuesType{}.ValueFromTerraform(ctx, tCase.in) + if tCase.expectedErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Equal(t, tCase.expectedErr, err.Error()) + return + } + + require.Truef(t, got.Equal(tCase.expectation), "values not equal %s, %s", tCase.expectation, got) + }) + } +} diff --git a/apstra/custom_types/string_with_alt_values_value.go b/apstra/custom_types/string_with_alt_values_value.go new file mode 100644 index 00000000..d985e0d9 --- /dev/null +++ b/apstra/custom_types/string_with_alt_values_value.go @@ -0,0 +1,98 @@ +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" +) + +var ( + _ basetypes.StringValuable = (*StringWithAltValues)(nil) + _ basetypes.StringValuableWithSemanticEquals = (*StringWithAltValues)(nil) +) + +type StringWithAltValues struct { + basetypes.StringValue + altValues []attr.Value +} + +func (v StringWithAltValues) Type(_ context.Context) attr.Type { + return StringWithAltValuesType{} +} + +func (v StringWithAltValues) Equal(o attr.Value) bool { + other, ok := o.(StringWithAltValues) + if !ok { + return false + } + + return v.StringValue.Equal(other.StringValue) +} + +// StringSemanticEquals implements the semantic equality check. According to this +// (https://discuss.hashicorp.com/t/can-semantic-equality-check-in-custom-types-be-asymmetrical/60644/2?u=hqnvylrx) +// semantic equality checks on custom types are always implementeed as oldValue.SemanticEquals(ctx, newValue) +func (v StringWithAltValues) StringSemanticEquals(_ context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(StringWithAltValues) + if !ok { + diags.AddError( + "Semantic Equality Check Error", + "An unexpected value type was received while performing semantic equality checks. "+ + "Please report this to the provider developers.\n\n"+ + "Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+ + "Got Value Type: "+fmt.Sprintf("%T", newValuable), + ) + + return false, diags + } + + // check new value against our "main" value + if v.Equal(newValue) { + return true, diags + } + + // check new value against our "alt" values + for _, a := range v.altValues { + if a.Equal(newValue) { + return true, diags + } + } + + // check old value against new "alt" values + for _, a := range newValue.altValues { + if a.Equal(v) { + return true, diags + } + } + + return false, diags +} + +func NewStringWithAltValuesNull() StringWithAltValues { + return StringWithAltValues{ + StringValue: basetypes.NewStringNull(), + } +} + +func NewStringWithAltValuesUnknown() StringWithAltValues { + return StringWithAltValues{ + StringValue: basetypes.NewStringUnknown(), + } +} + +func NewStringWithAltValuesValue(value string, alt ...string) StringWithAltValues { + altValues := make([]attr.Value, len(alt)) + for i, a := range alt { + altValues[i] = StringWithAltValues{StringValue: basetypes.NewStringValue(a)} + } + + return StringWithAltValues{ + StringValue: basetypes.NewStringValue(value), + altValues: altValues, + } +} diff --git a/apstra/custom_types/string_with_alt_values_value_test.go b/apstra/custom_types/string_with_alt_values_value_test.go new file mode 100644 index 00000000..006509f0 --- /dev/null +++ b/apstra/custom_types/string_with_alt_values_value_test.go @@ -0,0 +1,56 @@ +package customtypes_test + +import ( + "context" + "testing" + + customtypes "github.com/Juniper/terraform-provider-apstra/apstra/custom_types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/stretchr/testify/require" +) + +func TestStringWithAltValues_StringSemanticEquals(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + currentValue customtypes.StringWithAltValues + givenValue basetypes.StringValuable + expectedMatch bool + }{ + "equal - no alt values": { + currentValue: customtypes.NewStringWithAltValuesValue("foo"), + givenValue: customtypes.NewStringWithAltValuesValue("foo"), + expectedMatch: true, + }, + "equal - with alt values": { + currentValue: customtypes.NewStringWithAltValuesValue("foo", "bar", "baz"), + givenValue: customtypes.NewStringWithAltValuesValue("foo"), + expectedMatch: true, + }, + "semantically equal - given matches an alt value": { + currentValue: customtypes.NewStringWithAltValuesValue("foo", "bar", "baz", "bang"), + givenValue: customtypes.NewStringWithAltValuesValue("baz"), + expectedMatch: true, + }, + "semantically equal - current matches an alt value": { + currentValue: customtypes.NewStringWithAltValuesValue("baz"), + givenValue: customtypes.NewStringWithAltValuesValue("foo", "bar", "baz", "bang"), + expectedMatch: true, + }, + "not equal": { + currentValue: customtypes.NewStringWithAltValuesValue("foo", "bar", "baz", "bang"), + givenValue: customtypes.NewStringWithAltValuesValue("FOO"), + expectedMatch: false, + }, + } + + for tName, tCase := range testCases { + t.Run(tName, func(t *testing.T) { + t.Parallel() + + match, diags := tCase.currentValue.StringSemanticEquals(context.Background(), tCase.givenValue) + require.Equalf(t, tCase.expectedMatch, match, "Expected StringSemanticEquals to return: %t, but got: %t", tCase.expectedMatch, match) + require.Nil(t, diags) + }) + } +}