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
73 changes: 73 additions & 0 deletions customtypes/encodedstring/base64_input_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package encodedstring

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 = (*Base64InputType)(nil)
)

// 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
// custom semantic equality defined in the Value type, which decodes the string and compares it with the response after create / update.
type Base64InputType struct {
basetypes.StringType
}

// String returns a human readable string of the type name.
func (t Base64InputType) String() string {
return "customtypes.encodedstring.Base64InputType"
}

// ValueType returns the Value type.
func (t Base64InputType) ValueType(ctx context.Context) attr.Value {
return Base64Input{}
}

// Equal returns true if the given type is equivalent.
func (t Base64InputType) Equal(o attr.Type) bool {
other, ok := o.(Base64InputType)

if !ok {
return false
}

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

// ValueFromString returns a StringValuable type given a StringValue.
func (t Base64InputType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
return Base64Input{
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 Base64InputType) 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
}
108 changes: 108 additions & 0 deletions customtypes/encodedstring/base64_input_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package encodedstring

import (
"context"
"encoding/base64"
"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.StringValuableWithSemanticEquals = (*Base64Input)(nil)
)

// Base64Input represents a valid base64 encoded string. Custom semantic equality
// logic is defined for Base64Input, where the encoded value is decoded, and then compared to the value received in response from Read / Create / Update.
type Base64Input struct {
basetypes.StringValue
}

// Type returns an Base64InputType.
func (v Base64Input) Type(_ context.Context) attr.Type {
return Base64InputType{}
}

// Equal returns true if the given value is equivalent.
func (v Base64Input) Equal(o attr.Value) bool {
other, ok := o.(Base64Input)

if !ok {
return false
}

return v.StringValue.Equal(other.StringValue)
}

// NewBase64InputNull creates an Base64Input with a null value. Determine whether the value is null via IsNull method.
func NewBase64InputNull() Base64Input {
return Base64Input{
StringValue: basetypes.NewStringNull(),
}
}

// NewBase64InputUnknown creates an Base64Input with an unknown value. Determine whether the value is unknown via IsUnknown method.
func NewBase64InputUnknown() Base64Input {
return Base64Input{
StringValue: basetypes.NewStringUnknown(),
}
}

// NewBase64InputValue creates an Base64Input with a known value. Access the value via ValueString method.
func NewBase64InputValue(value string) Base64Input {
return Base64Input{
StringValue: basetypes.NewStringValue(value),
}
}

// NewBase64InputPointerValue creates an Base64Input with a null value if nil or a known value. Access the value via ValueStringPointer method.
func NewBase64InputPointerValue(value *string) Base64Input {
return Base64Input{
StringValue: basetypes.NewStringPointerValue(value),
}
}

// StringSemanticEquals decodes givenValuable, compares it with the receiver string value and returns whether they are inconsequentially equal.
// Semantic equality is checked during planning phase, and after receiving response in apply phase. In planning phase, givenValuable comes from the state, and v
// 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.
func (v Base64Input) StringSemanticEquals(ctx context.Context, givenValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
// The framework should always pass the correct value type, but always check
_, ok := givenValuable.(Base64Input)

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", givenValuable),
)

return false, diags
}

givenStringValue, err := givenValuable.ToStringValue(ctx)
if err != nil {
// Not a StringValue type.
diags.AddError(
"Custom String Conversion Error",
"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",
)
return false, diags
}

decodedGivenStringBytes, errors := base64.StdEncoding.DecodeString(givenStringValue.ValueString())
if errors != nil {
// Not base64 encoded, return false so value from response is used in plan to compare with config.
diags.AddError(
"Custom String Decode Error",
"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",
)
return false, diags
}

return string(decodedGivenStringBytes) == v.ValueString(), diags
}
46 changes: 46 additions & 0 deletions customtypes/encodedstring/base64_input_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package encodedstring

import (
"context"
"testing"
)

func TestBase64InputValue(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
stateOrConfigValue string
currentValue string
expectedSemanticEqualityOutput bool
}{
{
name: "semantically equal values",
stateOrConfigValue: "c2FtcGxlIHZhbHVl",
currentValue: "sample value",
expectedSemanticEqualityOutput: true,
},
{
name: "semantically unequal values",
stateOrConfigValue: "c2FtcGxlIHZhbHVl",
currentValue: "not sample value",
expectedSemanticEqualityOutput: false,
},
}

ctx := context.Background()

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
givenValue := NewBase64InputValue(testCase.stateOrConfigValue)
currentValue := NewBase64InputValue(testCase.currentValue)

areEqual, _ := currentValue.StringSemanticEquals(ctx, givenValue)

if areEqual != testCase.expectedSemanticEqualityOutput {
t.Errorf("Unexpected difference in Base64Input semantic equality, got: %t, expected: %t", areEqual, testCase.expectedSemanticEqualityOutput)
}
})
}
}
73 changes: 73 additions & 0 deletions customtypes/encodedstring/base64_or_plain_input_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package encodedstring

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 = (*Base64OrPlainInputType)(nil)
)

// 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
// 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.
type Base64OrPlainInputType struct {
basetypes.StringType
}

// String returns a human readable string of the type name.
func (t Base64OrPlainInputType) String() string {
return "customtypes.encodedstring.Base64OrPlainInputType"
}

// ValueType returns the Value type.
func (t Base64OrPlainInputType) ValueType(ctx context.Context) attr.Value {
return Base64OrPlainInput{}
}

// Equal returns true if the given type is equivalent.
func (t Base64OrPlainInputType) Equal(o attr.Type) bool {
other, ok := o.(Base64OrPlainInputType)

if !ok {
return false
}

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

// ValueFromString returns a StringValuable type given a StringValue.
func (t Base64OrPlainInputType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
return Base64OrPlainInput{
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 Base64OrPlainInputType) 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
}
Loading
Loading