Skip to content

Commit dd5c84c

Browse files
committed
Add custom types for encoded strings and unit tests
1 parent 77603c9 commit dd5c84c

File tree

8 files changed

+539
-0
lines changed

8 files changed

+539
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package encodedstring
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
)
12+
13+
var (
14+
_ basetypes.StringTypable = (*Base64InputType)(nil)
15+
)
16+
17+
// Base64InputType is an attribute type that represents a string that is base64 encoded, but only in configuration and state, not in the response. It has
18+
// custom semantic equality defined in the Value type, which decodes the string and compares it with the response after create / update.
19+
type Base64InputType struct {
20+
basetypes.StringType
21+
}
22+
23+
// String returns a human readable string of the type name.
24+
func (t Base64InputType) String() string {
25+
return "customtypes.encodedstring.Base64InputType"
26+
}
27+
28+
// ValueType returns the Value type.
29+
func (t Base64InputType) ValueType(ctx context.Context) attr.Value {
30+
return Base64Input{}
31+
}
32+
33+
// Equal returns true if the given type is equivalent.
34+
func (t Base64InputType) Equal(o attr.Type) bool {
35+
other, ok := o.(Base64InputType)
36+
37+
if !ok {
38+
return false
39+
}
40+
41+
return t.StringType.Equal(other.StringType)
42+
}
43+
44+
// ValueFromString returns a StringValuable type given a StringValue.
45+
func (t Base64InputType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
46+
return Base64Input{
47+
StringValue: in,
48+
}, nil
49+
}
50+
51+
// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to convert the tftypes.Value into a more convenient Go type
52+
// for the provider to consume the data with.
53+
func (t Base64InputType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
54+
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
55+
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
stringValue, ok := attrValue.(basetypes.StringValue)
61+
62+
if !ok {
63+
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
64+
}
65+
66+
stringValuable, diags := t.ValueFromString(ctx, stringValue)
67+
68+
if diags.HasError() {
69+
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
70+
}
71+
72+
return stringValuable, nil
73+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package encodedstring
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
11+
)
12+
13+
var (
14+
_ basetypes.StringValuableWithSemanticEquals = (*Base64Input)(nil)
15+
)
16+
17+
// Base64Input represents a valid base64 encoded string. Custom semantic equality
18+
// logic is defined for Base64Input, where the encoded value is decoded, and then compared to the value received in response from Read / Create / Update.
19+
type Base64Input struct {
20+
basetypes.StringValue
21+
}
22+
23+
// Type returns an Base64InputType.
24+
func (v Base64Input) Type(_ context.Context) attr.Type {
25+
return Base64InputType{}
26+
}
27+
28+
// Equal returns true if the given value is equivalent.
29+
func (v Base64Input) Equal(o attr.Value) bool {
30+
other, ok := o.(Base64Input)
31+
32+
if !ok {
33+
return false
34+
}
35+
36+
return v.StringValue.Equal(other.StringValue)
37+
}
38+
39+
// NewBase64InputNull creates an Base64Input with a null value. Determine whether the value is null via IsNull method.
40+
func NewBase64InputNull() Base64Input {
41+
return Base64Input{
42+
StringValue: basetypes.NewStringNull(),
43+
}
44+
}
45+
46+
// NewBase64InputUnknown creates an Base64Input with an unknown value. Determine whether the value is unknown via IsUnknown method.
47+
func NewBase64InputUnknown() Base64Input {
48+
return Base64Input{
49+
StringValue: basetypes.NewStringUnknown(),
50+
}
51+
}
52+
53+
// NewBase64InputValue creates an Base64Input with a known value. Access the value via ValueString method.
54+
func NewBase64InputValue(value string) Base64Input {
55+
return Base64Input{
56+
StringValue: basetypes.NewStringValue(value),
57+
}
58+
}
59+
60+
// NewBase64InputPointerValue creates an Base64Input with a null value if nil or a known value. Access the value via ValueStringPointer method.
61+
func NewBase64InputPointerValue(value *string) Base64Input {
62+
return Base64Input{
63+
StringValue: basetypes.NewStringPointerValue(value),
64+
}
65+
}
66+
67+
// StringSemanticEquals decodes givenValuable, compares it with the receiver string value and returns whether they are inconsequentially equal.
68+
// Semantic equality is checked during planning phase, and after receiving response in apply phase. In planning phase, givenValuable comes from the state, and v
69+
// is the current value - from Read method response. In the apply phase, givenValuable is from the plan, and v is from response of Create / Update.
70+
func (v Base64Input) StringSemanticEquals(ctx context.Context, givenValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
71+
var diags diag.Diagnostics
72+
// The framework should always pass the correct value type, but always check
73+
_, ok := givenValuable.(Base64Input)
74+
75+
if !ok {
76+
diags.AddError(
77+
"Semantic Equality Check Error",
78+
"An unexpected value type was received while performing semantic equality checks. "+
79+
"Please report this to the provider developers.\n\n"+
80+
"Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+
81+
"Got Value Type: "+fmt.Sprintf("%T", givenValuable),
82+
)
83+
84+
return false, diags
85+
}
86+
87+
givenStringValue, err := givenValuable.ToStringValue(ctx)
88+
if err != nil {
89+
// Not a StringValue type.
90+
diags.AddError(
91+
"Custom String Conversion Error",
92+
"An unexpected error was encountered trying to convert a custom string. This is always an error in the provider. Please report the following to the provider developer:\n\n",
93+
)
94+
return false, diags
95+
}
96+
97+
decodedGivenStringBytes, errors := base64.StdEncoding.DecodeString(givenStringValue.ValueString())
98+
if errors != nil {
99+
// Not base64 encoded, return false so value from response is used in plan to compare with config.
100+
diags.AddError(
101+
"Custom String Decode Error",
102+
"An unexpected error was encountered trying to decode a custom string. This is always an error in the provider. Please report the following to the provider developer:\n\n",
103+
)
104+
return false, diags
105+
}
106+
107+
return string(decodedGivenStringBytes) == v.ValueString(), diags
108+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package encodedstring
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func TestBase64InputValue(t *testing.T) {
9+
t.Parallel()
10+
11+
testCases := []struct {
12+
name string
13+
stateOrConfigValue string
14+
currentValue string
15+
expectedSemanticEqualityOutput bool
16+
}{
17+
{
18+
name: "semantically equal values",
19+
stateOrConfigValue: "c2FtcGxlIHZhbHVl",
20+
currentValue: "sample value",
21+
expectedSemanticEqualityOutput: true,
22+
},
23+
{
24+
name: "semantically unequal values",
25+
stateOrConfigValue: "c2FtcGxlIHZhbHVl",
26+
currentValue: "not sample value",
27+
expectedSemanticEqualityOutput: false,
28+
},
29+
}
30+
31+
ctx := context.Background()
32+
33+
for _, testCase := range testCases {
34+
35+
t.Run(testCase.name, func(t *testing.T) {
36+
t.Parallel()
37+
givenValue := NewBase64InputValue(testCase.stateOrConfigValue)
38+
currentValue := NewBase64InputValue(testCase.currentValue)
39+
40+
areEqual, _ := currentValue.StringSemanticEquals(ctx, givenValue)
41+
42+
if areEqual != testCase.expectedSemanticEqualityOutput {
43+
t.Errorf("Unexpected difference in Base64Input semantic equality, got: %t, expected: %t", areEqual, testCase.expectedSemanticEqualityOutput)
44+
}
45+
})
46+
}
47+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package encodedstring
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
)
12+
13+
var (
14+
_ basetypes.StringTypable = (*Base64OrPlainInputType)(nil)
15+
)
16+
17+
// Base64OrPlainInputType is an attribute type that represents a string that is optionally base64 encoded, but only in configuration and state, not in the response. It has
18+
// custom semantic equality defined in the Value type, which does a double comparison with the value in response of Read/Create/Update - with and without decoding.
19+
type Base64OrPlainInputType struct {
20+
basetypes.StringType
21+
}
22+
23+
// String returns a human readable string of the type name.
24+
func (t Base64OrPlainInputType) String() string {
25+
return "customtypes.encodedstring.Base64OrPlainInputType"
26+
}
27+
28+
// ValueType returns the Value type.
29+
func (t Base64OrPlainInputType) ValueType(ctx context.Context) attr.Value {
30+
return Base64OrPlainInput{}
31+
}
32+
33+
// Equal returns true if the given type is equivalent.
34+
func (t Base64OrPlainInputType) Equal(o attr.Type) bool {
35+
other, ok := o.(Base64OrPlainInputType)
36+
37+
if !ok {
38+
return false
39+
}
40+
41+
return t.StringType.Equal(other.StringType)
42+
}
43+
44+
// ValueFromString returns a StringValuable type given a StringValue.
45+
func (t Base64OrPlainInputType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
46+
return Base64OrPlainInput{
47+
StringValue: in,
48+
}, nil
49+
}
50+
51+
// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to convert the tftypes.Value into a more convenient Go type
52+
// for the provider to consume the data with.
53+
func (t Base64OrPlainInputType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
54+
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
55+
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
stringValue, ok := attrValue.(basetypes.StringValue)
61+
62+
if !ok {
63+
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
64+
}
65+
66+
stringValuable, diags := t.ValueFromString(ctx, stringValue)
67+
68+
if diags.HasError() {
69+
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
70+
}
71+
72+
return stringValuable, nil
73+
}

0 commit comments

Comments
 (0)