Skip to content

Commit b793fd3

Browse files
authored
ephemeralvalidator: Introduce new package for common ephemeral resource configuration validators (#242)
* use WIP framework branch * implementation of the shared config validators for ephemeral resources * update go mod * go mod tidy * changelog
1 parent 761f545 commit b793fd3

33 files changed

+1941
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'ephemeralvalidator: Introduce new package with declarative validators for ephemeral
3+
resource configurations'
4+
time: 2024-10-30T16:46:18.935223-04:00
5+
custom:
6+
Issue: "242"

ephemeralvalidator/all.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralvalidator
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
12+
)
13+
14+
// All returns a validator which ensures that any configured attribute value
15+
// validates against all the given validators.
16+
//
17+
// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings
18+
// as the Validators field automatically applies a logical AND.
19+
func All(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator {
20+
return allValidator{
21+
validators: validators,
22+
}
23+
}
24+
25+
var _ ephemeral.ConfigValidator = allValidator{}
26+
27+
// allValidator implements the validator.
28+
type allValidator struct {
29+
validators []ephemeral.ConfigValidator
30+
}
31+
32+
// Description describes the validation in plain text formatting.
33+
func (v allValidator) Description(ctx context.Context) string {
34+
var descriptions []string
35+
36+
for _, subValidator := range v.validators {
37+
descriptions = append(descriptions, subValidator.Description(ctx))
38+
}
39+
40+
return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + "))
41+
}
42+
43+
// MarkdownDescription describes the validation in Markdown formatting.
44+
func (v allValidator) MarkdownDescription(ctx context.Context) string {
45+
return v.Description(ctx)
46+
}
47+
48+
// ValidateEphemeralResource performs the validation.
49+
func (v allValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
50+
for _, subValidator := range v.validators {
51+
validateResp := &ephemeral.ValidateConfigResponse{}
52+
53+
subValidator.ValidateEphemeralResource(ctx, req, validateResp)
54+
55+
resp.Diagnostics.Append(validateResp.Diagnostics...)
56+
}
57+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
8+
9+
"github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator"
10+
)
11+
12+
func ExampleAll() {
13+
// Used inside a ephemeral.EphemeralResource type ConfigValidators method
14+
_ = []ephemeral.ConfigValidator{
15+
// The configuration must satisfy either All validator.
16+
ephemeralvalidator.Any(
17+
ephemeralvalidator.All( /* ... */ ),
18+
ephemeralvalidator.All( /* ... */ ),
19+
),
20+
}
21+
}

ephemeralvalidator/all_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralvalidator_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
13+
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
14+
"github.com/hashicorp/terraform-plugin-framework/path"
15+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
16+
"github.com/hashicorp/terraform-plugin-go/tftypes"
17+
18+
"github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator"
19+
)
20+
21+
func TestAllValidatorValidateEphemeralResource(t *testing.T) {
22+
t.Parallel()
23+
24+
testCases := map[string]struct {
25+
validators []ephemeral.ConfigValidator
26+
req ephemeral.ValidateConfigRequest
27+
expected *ephemeral.ValidateConfigResponse
28+
}{
29+
"no-diagnostics": {
30+
validators: []ephemeral.ConfigValidator{
31+
ephemeralvalidator.ExactlyOneOf(
32+
path.MatchRoot("test1"),
33+
path.MatchRoot("test2"),
34+
),
35+
ephemeralvalidator.All(
36+
ephemeralvalidator.AtLeastOneOf(
37+
path.MatchRoot("test3"),
38+
path.MatchRoot("test4"),
39+
),
40+
ephemeralvalidator.Conflicting(
41+
path.MatchRoot("test3"),
42+
path.MatchRoot("test5"),
43+
),
44+
),
45+
},
46+
req: ephemeral.ValidateConfigRequest{
47+
Config: tfsdk.Config{
48+
Schema: schema.Schema{
49+
Attributes: map[string]schema.Attribute{
50+
"test1": schema.StringAttribute{
51+
Optional: true,
52+
},
53+
"test2": schema.StringAttribute{
54+
Optional: true,
55+
},
56+
"test3": schema.StringAttribute{
57+
Optional: true,
58+
},
59+
"test4": schema.StringAttribute{
60+
Optional: true,
61+
},
62+
"test5": schema.StringAttribute{
63+
Optional: true,
64+
},
65+
},
66+
},
67+
Raw: tftypes.NewValue(
68+
tftypes.Object{
69+
AttributeTypes: map[string]tftypes.Type{
70+
"test1": tftypes.String,
71+
"test2": tftypes.String,
72+
"test3": tftypes.String,
73+
"test4": tftypes.String,
74+
"test5": tftypes.String,
75+
},
76+
},
77+
map[string]tftypes.Value{
78+
"test1": tftypes.NewValue(tftypes.String, nil),
79+
"test2": tftypes.NewValue(tftypes.String, nil),
80+
"test3": tftypes.NewValue(tftypes.String, "test-value"),
81+
"test4": tftypes.NewValue(tftypes.String, nil),
82+
"test5": tftypes.NewValue(tftypes.String, nil),
83+
},
84+
),
85+
},
86+
},
87+
expected: &ephemeral.ValidateConfigResponse{},
88+
},
89+
"diagnostics": {
90+
validators: []ephemeral.ConfigValidator{
91+
ephemeralvalidator.ExactlyOneOf(
92+
path.MatchRoot("test1"),
93+
path.MatchRoot("test2"),
94+
),
95+
ephemeralvalidator.All(
96+
ephemeralvalidator.AtLeastOneOf(
97+
path.MatchRoot("test3"),
98+
path.MatchRoot("test4"),
99+
),
100+
ephemeralvalidator.Conflicting(
101+
path.MatchRoot("test3"),
102+
path.MatchRoot("test5"),
103+
),
104+
),
105+
},
106+
req: ephemeral.ValidateConfigRequest{
107+
Config: tfsdk.Config{
108+
Schema: schema.Schema{
109+
Attributes: map[string]schema.Attribute{
110+
"test1": schema.StringAttribute{
111+
Optional: true,
112+
},
113+
"test2": schema.StringAttribute{
114+
Optional: true,
115+
},
116+
"test3": schema.StringAttribute{
117+
Optional: true,
118+
},
119+
"test4": schema.StringAttribute{
120+
Optional: true,
121+
},
122+
"test5": schema.StringAttribute{
123+
Optional: true,
124+
},
125+
},
126+
},
127+
Raw: tftypes.NewValue(
128+
tftypes.Object{
129+
AttributeTypes: map[string]tftypes.Type{
130+
"test1": tftypes.String,
131+
"test2": tftypes.String,
132+
"test3": tftypes.String,
133+
"test4": tftypes.String,
134+
"test5": tftypes.String,
135+
},
136+
},
137+
map[string]tftypes.Value{
138+
"test1": tftypes.NewValue(tftypes.String, nil),
139+
"test2": tftypes.NewValue(tftypes.String, nil),
140+
"test3": tftypes.NewValue(tftypes.String, "test-value"),
141+
"test4": tftypes.NewValue(tftypes.String, nil),
142+
"test5": tftypes.NewValue(tftypes.String, "test-value"),
143+
},
144+
),
145+
},
146+
},
147+
expected: &ephemeral.ValidateConfigResponse{
148+
Diagnostics: diag.Diagnostics{
149+
diag.NewErrorDiagnostic(
150+
"Missing Attribute Configuration",
151+
"Exactly one of these attributes must be configured: [test1,test2]",
152+
),
153+
diag.WithPath(path.Root("test3"),
154+
diag.NewErrorDiagnostic(
155+
"Invalid Attribute Combination",
156+
"These attributes cannot be configured together: [test3,test5]",
157+
)),
158+
},
159+
},
160+
},
161+
}
162+
163+
for name, testCase := range testCases {
164+
name, testCase := name, testCase
165+
166+
t.Run(name, func(t *testing.T) {
167+
t.Parallel()
168+
169+
got := &ephemeral.ValidateConfigResponse{}
170+
171+
ephemeralvalidator.Any(testCase.validators...).ValidateEphemeralResource(context.Background(), testCase.req, got)
172+
173+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
174+
t.Errorf("unexpected difference: %s", diff)
175+
}
176+
})
177+
}
178+
}

ephemeralvalidator/any.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralvalidator
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
12+
)
13+
14+
// Any returns a validator which ensures that any configured attribute value
15+
// passes at least one of the given validators.
16+
//
17+
// To prevent practitioner confusion should non-passing validators have
18+
// conflicting logic, only warnings from the passing validator are returned.
19+
// Use AnyWithAllWarnings() to return warnings from non-passing validators
20+
// as well.
21+
func Any(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator {
22+
return anyValidator{
23+
validators: validators,
24+
}
25+
}
26+
27+
var _ ephemeral.ConfigValidator = anyValidator{}
28+
29+
// anyValidator implements the validator.
30+
type anyValidator struct {
31+
validators []ephemeral.ConfigValidator
32+
}
33+
34+
// Description describes the validation in plain text formatting.
35+
func (v anyValidator) Description(ctx context.Context) string {
36+
var descriptions []string
37+
38+
for _, subValidator := range v.validators {
39+
descriptions = append(descriptions, subValidator.Description(ctx))
40+
}
41+
42+
return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + "))
43+
}
44+
45+
// MarkdownDescription describes the validation in Markdown formatting.
46+
func (v anyValidator) MarkdownDescription(ctx context.Context) string {
47+
return v.Description(ctx)
48+
}
49+
50+
// ValidateEphemeralResource performs the validation.
51+
func (v anyValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
52+
for _, subValidator := range v.validators {
53+
validateResp := &ephemeral.ValidateConfigResponse{}
54+
55+
subValidator.ValidateEphemeralResource(ctx, req, validateResp)
56+
57+
if !validateResp.Diagnostics.HasError() {
58+
resp.Diagnostics = validateResp.Diagnostics
59+
60+
return
61+
}
62+
63+
resp.Diagnostics.Append(validateResp.Diagnostics...)
64+
}
65+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ephemeralvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
8+
9+
"github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator"
10+
)
11+
12+
func ExampleAny() {
13+
// Used inside a ephemeral.EphemeralResource type ConfigValidators method
14+
_ = []ephemeral.ConfigValidator{
15+
ephemeralvalidator.Any( /* ... */ ),
16+
}
17+
}

0 commit comments

Comments
 (0)