Skip to content

Commit 5558e71

Browse files
committed
Type-Based Semantic Equality
Reference: #70 This change set includes an initial implemention of type-based semantic equality functionality for the framework. Semantic equality functionality enables provider developers to prevent drift detection and certain cases of Terraform data consistency errors, by writing logic that defines when a prior value should be preserved when compared to a new value with inconsequential differences. The definition of inconsequential depends on the context of the value, but typically it refers to when a values have equivalent meaning with differing syntax. For example, the JSON specification defines whitespace as optional and object properties as unordered. Remote systems or code libraries may normalize JSON encoded strings into a differing byte ordering while remaining exactly equivalent in terms of the JSON specification. Type-based refers to the logic being baked into the extensible custom type system. This design is preferable over being schema-based, which refers to logic being repetitively coded across multiple attribute definitions. Being type-based means the provider developer can state these intentions by nature of pointing to a custom type which bakes in this logic, rather than needing to remember all the details to duplicate. For example, proper JSON string handling in the prior terraform-plugin-sdk required implementing an attribute as a string type with an appropriate JSON validation reference and JSON normalization reference. With these changes, a bespoke and reusable JSON string type with automatic validation and equivalency handling can be created. Developers can implement semantic equality logic by implementing a new type-specific interface, which has one method that receives the new value and should return whether the prior value should be preserved or any diagnostics. The website documentation for custom types has been rewritten to remove details more pertinent to the original design of the framework and instead focus on how developers can either use existing custom types or create custom types based on the `basetypes` package `Typable` and `Valuable` interfaces.
1 parent 99f2844 commit 5558e71

File tree

56 files changed

+9920
-241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+9920
-241
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package fwschemadata
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/attr"
7+
"github.com/hashicorp/terraform-plugin-framework/diag"
8+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
9+
"github.com/hashicorp/terraform-plugin-framework/path"
10+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
11+
)
12+
13+
// ValueSemanticEqualityRequest represents a request for the provider to
14+
// perform semantic equality logic on a value.
15+
type ValueSemanticEqualityRequest struct {
16+
// Path is the schema-based path of the value.
17+
Path path.Path
18+
19+
// PriorValue is the prior value.
20+
PriorValue attr.Value
21+
22+
// ProposedNewValue is the proposed new value. NewValue in the response
23+
// contains the results of semantic equality logic.
24+
ProposedNewValue attr.Value
25+
}
26+
27+
// ValueSemanticEqualityResponse represents a response to a
28+
// ValueSemanticEqualityRequest.
29+
type ValueSemanticEqualityResponse struct {
30+
// NewValue contains the new value based on the semantic equality logic.
31+
NewValue attr.Value
32+
33+
// Diagnostics contains any errors and warnings for the logic.
34+
Diagnostics diag.Diagnostics
35+
}
36+
37+
// ValueSemanticEquality runs all semantic equality logic for a value, including
38+
// recursive checking against collection and structural types.
39+
func ValueSemanticEquality(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) {
40+
ctx = logging.FrameworkWithAttributePath(ctx, req.Path.String())
41+
42+
// Ensure the response NewValue always starts with the proposed new value.
43+
// This is purely defensive coding to prevent subtle data handling bugs.
44+
resp.NewValue = req.ProposedNewValue
45+
46+
// If the prior value is null or unknown, no need to check semantic equality
47+
// as the proposed new value is always correct. There is also no need to
48+
// descend further into any nesting.
49+
if req.PriorValue.IsNull() || req.PriorValue.IsUnknown() {
50+
return
51+
}
52+
53+
// If the proposed new value is null or unknown, no need to check semantic
54+
// equality as it should never be changed back to the prior value. There is
55+
// also no need to descend further into any nesting.
56+
if req.ProposedNewValue.IsNull() || req.ProposedNewValue.IsUnknown() {
57+
return
58+
}
59+
60+
switch req.ProposedNewValue.(type) {
61+
case basetypes.BoolValuable:
62+
ValueSemanticEqualityBool(ctx, req, resp)
63+
case basetypes.Float64Valuable:
64+
ValueSemanticEqualityFloat64(ctx, req, resp)
65+
case basetypes.Int64Valuable:
66+
ValueSemanticEqualityInt64(ctx, req, resp)
67+
case basetypes.ListValuable:
68+
ValueSemanticEqualityList(ctx, req, resp)
69+
case basetypes.MapValuable:
70+
ValueSemanticEqualityMap(ctx, req, resp)
71+
case basetypes.NumberValuable:
72+
ValueSemanticEqualityNumber(ctx, req, resp)
73+
case basetypes.ObjectValuable:
74+
ValueSemanticEqualityObject(ctx, req, resp)
75+
case basetypes.SetValuable:
76+
ValueSemanticEqualitySet(ctx, req, resp)
77+
case basetypes.StringValuable:
78+
ValueSemanticEqualityString(ctx, req, resp)
79+
}
80+
81+
if resp.NewValue.Equal(req.PriorValue) {
82+
logging.FrameworkDebug(ctx, "Value switched to prior value due to semantic equality logic")
83+
}
84+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package fwschemadata
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
7+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
8+
)
9+
10+
// ValueSemanticEqualityBool performs bool type semantic equality.
11+
func ValueSemanticEqualityBool(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) {
12+
priorValuable, ok := req.PriorValue.(basetypes.BoolValuableWithSemanticEquals)
13+
14+
// No changes required if the interface is not implemented.
15+
if !ok {
16+
return
17+
}
18+
19+
proposedNewValuable, ok := req.ProposedNewValue.(basetypes.BoolValuableWithSemanticEquals)
20+
21+
// No changes required if the interface is not implemented.
22+
if !ok {
23+
return
24+
}
25+
26+
logging.FrameworkTrace(
27+
ctx,
28+
"Calling provider defined type-based SemanticEquals",
29+
map[string]interface{}{
30+
logging.KeyValueType: proposedNewValuable.String(),
31+
},
32+
)
33+
34+
usePriorValue, diags := proposedNewValuable.BoolSemanticEquals(ctx, priorValuable)
35+
36+
logging.FrameworkTrace(
37+
ctx,
38+
"Called provider defined type-based SemanticEquals",
39+
map[string]interface{}{
40+
logging.KeyValueType: proposedNewValuable.String(),
41+
},
42+
)
43+
44+
resp.Diagnostics.Append(diags...)
45+
46+
if !usePriorValue {
47+
return
48+
}
49+
50+
resp.NewValue = priorValuable
51+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package fwschemadata_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata"
10+
testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
)
14+
15+
func TestValueSemanticEqualityBool(t *testing.T) {
16+
t.Parallel()
17+
18+
testCases := map[string]struct {
19+
request fwschemadata.ValueSemanticEqualityRequest
20+
expected *fwschemadata.ValueSemanticEqualityResponse
21+
}{
22+
"BoolValue": {
23+
request: fwschemadata.ValueSemanticEqualityRequest{
24+
Path: path.Root("test"),
25+
PriorValue: types.BoolValue(false),
26+
ProposedNewValue: types.BoolValue(true),
27+
},
28+
expected: &fwschemadata.ValueSemanticEqualityResponse{
29+
NewValue: types.BoolValue(true),
30+
},
31+
},
32+
"BoolValuableWithSemanticEquals-true": {
33+
request: fwschemadata.ValueSemanticEqualityRequest{
34+
Path: path.Root("test"),
35+
PriorValue: testtypes.BoolValueWithSemanticEquals{
36+
BoolValue: types.BoolValue(false),
37+
SemanticEquals: true,
38+
},
39+
ProposedNewValue: testtypes.BoolValueWithSemanticEquals{
40+
BoolValue: types.BoolValue(true),
41+
SemanticEquals: true,
42+
},
43+
},
44+
expected: &fwschemadata.ValueSemanticEqualityResponse{
45+
NewValue: testtypes.BoolValueWithSemanticEquals{
46+
BoolValue: types.BoolValue(false),
47+
SemanticEquals: true,
48+
},
49+
},
50+
},
51+
"BoolValuableWithSemanticEquals-false": {
52+
request: fwschemadata.ValueSemanticEqualityRequest{
53+
Path: path.Root("test"),
54+
PriorValue: testtypes.BoolValueWithSemanticEquals{
55+
BoolValue: types.BoolValue(false),
56+
SemanticEquals: false,
57+
},
58+
ProposedNewValue: testtypes.BoolValueWithSemanticEquals{
59+
BoolValue: types.BoolValue(true),
60+
SemanticEquals: false,
61+
},
62+
},
63+
expected: &fwschemadata.ValueSemanticEqualityResponse{
64+
NewValue: testtypes.BoolValueWithSemanticEquals{
65+
BoolValue: types.BoolValue(true),
66+
SemanticEquals: false,
67+
},
68+
},
69+
},
70+
"BoolValuableWithSemanticEquals-diagnostics": {
71+
request: fwschemadata.ValueSemanticEqualityRequest{
72+
Path: path.Root("test"),
73+
PriorValue: testtypes.BoolValueWithSemanticEquals{
74+
BoolValue: types.BoolValue(false),
75+
SemanticEquals: false,
76+
SemanticEqualsDiagnostics: diag.Diagnostics{
77+
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
78+
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
79+
},
80+
},
81+
ProposedNewValue: testtypes.BoolValueWithSemanticEquals{
82+
BoolValue: types.BoolValue(true),
83+
SemanticEquals: false,
84+
SemanticEqualsDiagnostics: diag.Diagnostics{
85+
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
86+
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
87+
},
88+
},
89+
},
90+
expected: &fwschemadata.ValueSemanticEqualityResponse{
91+
NewValue: testtypes.BoolValueWithSemanticEquals{
92+
BoolValue: types.BoolValue(true),
93+
SemanticEquals: false,
94+
SemanticEqualsDiagnostics: diag.Diagnostics{
95+
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
96+
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
97+
},
98+
},
99+
Diagnostics: diag.Diagnostics{
100+
diag.NewErrorDiagnostic("test summary 1", "test detail 1"),
101+
diag.NewErrorDiagnostic("test summary 2", "test detail 2"),
102+
},
103+
},
104+
},
105+
}
106+
107+
for name, testCase := range testCases {
108+
name, testCase := name, testCase
109+
110+
t.Run(name, func(t *testing.T) {
111+
t.Parallel()
112+
113+
got := &fwschemadata.ValueSemanticEqualityResponse{
114+
NewValue: testCase.request.ProposedNewValue,
115+
}
116+
117+
fwschemadata.ValueSemanticEqualityBool(context.Background(), testCase.request, got)
118+
119+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
120+
t.Errorf("unexpected difference: %s", diff)
121+
}
122+
})
123+
}
124+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package fwschemadata
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
7+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
8+
)
9+
10+
// ValueSemanticEqualityFloat64 performs float64 type semantic equality.
11+
func ValueSemanticEqualityFloat64(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) {
12+
priorValuable, ok := req.PriorValue.(basetypes.Float64ValuableWithSemanticEquals)
13+
14+
// No changes required if the interface is not implemented.
15+
if !ok {
16+
return
17+
}
18+
19+
proposedNewValuable, ok := req.ProposedNewValue.(basetypes.Float64ValuableWithSemanticEquals)
20+
21+
// No changes required if the interface is not implemented.
22+
if !ok {
23+
return
24+
}
25+
26+
logging.FrameworkTrace(
27+
ctx,
28+
"Calling provider defined type-based SemanticEquals",
29+
map[string]interface{}{
30+
logging.KeyValueType: proposedNewValuable.String(),
31+
},
32+
)
33+
34+
usePriorValue, diags := proposedNewValuable.Float64SemanticEquals(ctx, priorValuable)
35+
36+
logging.FrameworkTrace(
37+
ctx,
38+
"Called provider defined type-based SemanticEquals",
39+
map[string]interface{}{
40+
logging.KeyValueType: proposedNewValuable.String(),
41+
},
42+
)
43+
44+
resp.Diagnostics.Append(diags...)
45+
46+
if !usePriorValue {
47+
return
48+
}
49+
50+
resp.NewValue = priorValuable
51+
}

0 commit comments

Comments
 (0)