Skip to content

Commit

Permalink
[WIP] Type-Based Semantic Equality
Browse files Browse the repository at this point in the history
Reference: #70
  • Loading branch information
bflad committed May 26, 2023
1 parent 99f2844 commit 5d89e26
Show file tree
Hide file tree
Showing 55 changed files with 9,647 additions and 27 deletions.
84 changes: 84 additions & 0 deletions internal/fwschemadata/value_semantic_equality.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package fwschemadata

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

// ValueSemanticEqualityRequest represents a request for the provider to
// perform semantic equality logic on a value.
type ValueSemanticEqualityRequest struct {
// Path is the schema-based path of the value.
Path path.Path

// PriorValue is the prior value.
PriorValue attr.Value

// ProposedNewValue is the proposed new value. NewValue in the response
// contains the results of semantic equality logic.
ProposedNewValue attr.Value
}

// ValueSemanticEqualityResponse represents a response to a
// ValueSemanticEqualityRequest.
type ValueSemanticEqualityResponse struct {
// NewValue contains the new value based on the semantic equality logic.
NewValue attr.Value

// Diagnostics contains any errors and warnings for the logic.
Diagnostics diag.Diagnostics
}

// ValueSemanticEquality runs all semantic equality logic for a value, including
// recursive checking against collection and structural types.
func ValueSemanticEquality(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) {
ctx = logging.FrameworkWithAttributePath(ctx, req.Path.String())

// Ensure the response NewValue always starts with the proposed new value.
// This is purely defensive coding to prevent subtle data handling bugs.
resp.NewValue = req.ProposedNewValue

// If the prior value is null or unknown, no need to check semantic equality
// as the proposed new value is always correct. There is also no need to
// descend further into any nesting.
if req.PriorValue.IsNull() || req.PriorValue.IsUnknown() {
return
}

// If the proposed new value is null or unknown, no need to check semantic
// equality as it should never be changed back to the prior value. There is
// also no need to descend further into any nesting.
if req.ProposedNewValue.IsNull() || req.ProposedNewValue.IsUnknown() {
return
}

switch req.ProposedNewValue.(type) {
case basetypes.BoolValuable:
ValueSemanticEqualityBool(ctx, req, resp)
case basetypes.Float64Valuable:
ValueSemanticEqualityFloat64(ctx, req, resp)
case basetypes.Int64Valuable:
ValueSemanticEqualityInt64(ctx, req, resp)
case basetypes.ListValuable:
ValueSemanticEqualityList(ctx, req, resp)
case basetypes.MapValuable:
ValueSemanticEqualityMap(ctx, req, resp)
case basetypes.NumberValuable:
ValueSemanticEqualityNumber(ctx, req, resp)
case basetypes.ObjectValuable:
ValueSemanticEqualityObject(ctx, req, resp)
case basetypes.SetValuable:
ValueSemanticEqualitySet(ctx, req, resp)
case basetypes.StringValuable:
ValueSemanticEqualityString(ctx, req, resp)
}

if resp.NewValue.Equal(req.PriorValue) {
logging.FrameworkDebug(ctx, "Value switched to prior value due to semantic equality logic")
}
}
51 changes: 51 additions & 0 deletions internal/fwschemadata/value_semantic_equality_bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package fwschemadata

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

// ValueSemanticEqualityBool performs bool type semantic equality.
func ValueSemanticEqualityBool(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) {
priorValuable, ok := req.PriorValue.(basetypes.BoolValuableWithSemanticEquals)

// No changes required if the interface is not implemented.
if !ok {
return
}

proposedNewValuable, ok := req.ProposedNewValue.(basetypes.BoolValuableWithSemanticEquals)

// No changes required if the interface is not implemented.
if !ok {
return
}

logging.FrameworkTrace(
ctx,
"Calling provider defined type-based SemanticEquals",
map[string]interface{}{
logging.KeyValueType: proposedNewValuable.String(),
},
)

usePriorValue, diags := proposedNewValuable.BoolSemanticEquals(ctx, priorValuable)

logging.FrameworkTrace(
ctx,
"Called provider defined type-based SemanticEquals",
map[string]interface{}{
logging.KeyValueType: proposedNewValuable.String(),
},
)

resp.Diagnostics.Append(diags...)

if !usePriorValue {
return
}

resp.NewValue = priorValuable
}
124 changes: 124 additions & 0 deletions internal/fwschemadata/value_semantic_equality_bool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package fwschemadata_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata"
testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/types"
)

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

testCases := map[string]struct {
request fwschemadata.ValueSemanticEqualityRequest
expected *fwschemadata.ValueSemanticEqualityResponse
}{
"BoolValue": {
request: fwschemadata.ValueSemanticEqualityRequest{
Path: path.Root("test"),
PriorValue: types.BoolValue(false),
ProposedNewValue: types.BoolValue(true),
},
expected: &fwschemadata.ValueSemanticEqualityResponse{
NewValue: types.BoolValue(true),
},
},
"BoolValuableWithSemanticEquals-true": {
request: fwschemadata.ValueSemanticEqualityRequest{
Path: path.Root("test"),
PriorValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(false),
SemanticEquals: true,
},
ProposedNewValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(true),
SemanticEquals: true,
},
},
expected: &fwschemadata.ValueSemanticEqualityResponse{
NewValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(false),
SemanticEquals: true,
},
},
},
"BoolValuableWithSemanticEquals-false": {
request: fwschemadata.ValueSemanticEqualityRequest{
Path: path.Root("test"),
PriorValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(false),
SemanticEquals: false,
},
ProposedNewValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(true),
SemanticEquals: false,
},
},
expected: &fwschemadata.ValueSemanticEqualityResponse{
NewValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(true),
SemanticEquals: false,
},
},
},
"BoolValuableWithSemanticEquals-diagnostics": {
request: fwschemadata.ValueSemanticEqualityRequest{
Path: path.Root("test"),
PriorValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(false),
SemanticEquals: false,
SemanticEqualsDiagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
},
},
ProposedNewValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(true),
SemanticEquals: false,
SemanticEqualsDiagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
},
},
},
expected: &fwschemadata.ValueSemanticEqualityResponse{
NewValue: testtypes.BoolValueWithSemanticEquals{
BoolValue: types.BoolValue(true),
SemanticEquals: false,
SemanticEqualsDiagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
},
},
Diagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
},
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got := &fwschemadata.ValueSemanticEqualityResponse{
NewValue: testCase.request.ProposedNewValue,
}

fwschemadata.ValueSemanticEqualityBool(context.Background(), testCase.request, got)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
51 changes: 51 additions & 0 deletions internal/fwschemadata/value_semantic_equality_float64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package fwschemadata

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

// ValueSemanticEqualityFloat64 performs float64 type semantic equality.
func ValueSemanticEqualityFloat64(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) {
priorValuable, ok := req.PriorValue.(basetypes.Float64ValuableWithSemanticEquals)

// No changes required if the interface is not implemented.
if !ok {
return
}

proposedNewValuable, ok := req.ProposedNewValue.(basetypes.Float64ValuableWithSemanticEquals)

// No changes required if the interface is not implemented.
if !ok {
return
}

logging.FrameworkTrace(
ctx,
"Calling provider defined type-based SemanticEquals",
map[string]interface{}{
logging.KeyValueType: proposedNewValuable.String(),
},
)

usePriorValue, diags := proposedNewValuable.Float64SemanticEquals(ctx, priorValuable)

logging.FrameworkTrace(
ctx,
"Called provider defined type-based SemanticEquals",
map[string]interface{}{
logging.KeyValueType: proposedNewValuable.String(),
},
)

resp.Diagnostics.Append(diags...)

if !usePriorValue {
return
}

resp.NewValue = priorValuable
}

0 comments on commit 5d89e26

Please sign in to comment.