Skip to content

Commit

Permalink
Merge pull request #991 from Juniper/990-introduce-custom-type-string…
Browse files Browse the repository at this point in the history
…withaltvalues

Introduce custom type `StringWithAltValues`
  • Loading branch information
chrismarget-j authored Dec 20, 2024
2 parents 5b05612 + fac9ee9 commit bd03e0f
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 4 deletions.
4 changes: 1 addition & 3 deletions apstra/custom_types/ipv46_address_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 0 additions & 1 deletion apstra/custom_types/ipv46_address_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
69 changes: 69 additions & 0 deletions apstra/custom_types/string_with_alt_values_type.go
Original file line number Diff line number Diff line change
@@ -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
}
56 changes: 56 additions & 0 deletions apstra/custom_types/string_with_alt_values_type_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
98 changes: 98 additions & 0 deletions apstra/custom_types/string_with_alt_values_value.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
56 changes: 56 additions & 0 deletions apstra/custom_types/string_with_alt_values_value_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}

0 comments on commit bd03e0f

Please sign in to comment.