Skip to content

Commit 8e6bfb1

Browse files
authored
listvalidator: Added UniqueValues validator (#88)
Reference: #67
1 parent 692fbd3 commit 8e6bfb1

File tree

4 files changed

+219
-0
lines changed

4 files changed

+219
-0
lines changed

.changelog/88.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
listvalidator: Added `UniqueValues` validator
3+
```

listvalidator/unique_values.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package listvalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
8+
)
9+
10+
var _ validator.List = uniqueValuesValidator{}
11+
12+
// uniqueValuesValidator implements the validator.
13+
type uniqueValuesValidator struct{}
14+
15+
// Description returns the plaintext description of the validator.
16+
func (v uniqueValuesValidator) Description(_ context.Context) string {
17+
return "all values must be unique"
18+
}
19+
20+
// MarkdownDescription returns the Markdown description of the validator.
21+
func (v uniqueValuesValidator) MarkdownDescription(ctx context.Context) string {
22+
return v.Description(ctx)
23+
}
24+
25+
// ValidateList implements the validation logic.
26+
func (v uniqueValuesValidator) ValidateList(_ context.Context, req validator.ListRequest, resp *validator.ListResponse) {
27+
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
28+
return
29+
}
30+
31+
elements := req.ConfigValue.Elements()
32+
33+
for indexOuter, elementOuter := range elements {
34+
// Only evaluate known values for duplicates.
35+
if elementOuter.IsUnknown() {
36+
continue
37+
}
38+
39+
for indexInner := indexOuter + 1; indexInner < len(elements); indexInner++ {
40+
elementInner := elements[indexInner]
41+
42+
if elementInner.IsUnknown() {
43+
continue
44+
}
45+
46+
if !elementInner.Equal(elementOuter) {
47+
continue
48+
}
49+
50+
resp.Diagnostics.AddAttributeError(
51+
req.Path,
52+
"Duplicate List Value",
53+
fmt.Sprintf("This attribute contains duplicate values of: %s", elementInner),
54+
)
55+
}
56+
}
57+
}
58+
59+
// UniqueValues returns a validator which ensures that any configured list
60+
// only contains unique values. This is similar to using a set attribute type
61+
// which inherently validates unique values, but with list ordering semantics.
62+
// Null (unconfigured) and unknown (known after apply) values are skipped.
63+
func UniqueValues() validator.List {
64+
return uniqueValuesValidator{}
65+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package listvalidator_test
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
6+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
7+
"github.com/hashicorp/terraform-plugin-framework/types"
8+
)
9+
10+
func ExampleUniqueValues() {
11+
// Used within a Schema method of a DataSource, Provider, or Resource
12+
_ = schema.Schema{
13+
Attributes: map[string]schema.Attribute{
14+
"example_attr": schema.ListAttribute{
15+
ElementType: types.StringType,
16+
Required: true,
17+
Validators: []validator.List{
18+
// Validate this list must contain only unique values.
19+
listvalidator.UniqueValues(),
20+
},
21+
},
22+
},
23+
}
24+
}

listvalidator/unique_values_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package listvalidator_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
9+
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
12+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
)
15+
16+
func TestUniqueValues(t *testing.T) {
17+
t.Parallel()
18+
19+
testCases := map[string]struct {
20+
list types.List
21+
expectedDiagnostics diag.Diagnostics
22+
}{
23+
"null-list": {
24+
list: types.ListNull(types.StringType),
25+
expectedDiagnostics: nil,
26+
},
27+
"unknown-list": {
28+
list: types.ListUnknown(types.StringType),
29+
expectedDiagnostics: nil,
30+
},
31+
"null-value": {
32+
list: types.ListValueMust(
33+
types.StringType,
34+
[]attr.Value{types.StringNull()},
35+
),
36+
expectedDiagnostics: nil,
37+
},
38+
"null-values-duplicate": {
39+
list: types.ListValueMust(
40+
types.StringType,
41+
[]attr.Value{types.StringNull(), types.StringNull()},
42+
),
43+
expectedDiagnostics: diag.Diagnostics{
44+
diag.NewAttributeErrorDiagnostic(
45+
path.Root("test"),
46+
"Duplicate List Value",
47+
"This attribute contains duplicate values of: <null>",
48+
),
49+
},
50+
},
51+
"null-values-valid": {
52+
list: types.ListValueMust(
53+
types.StringType,
54+
[]attr.Value{types.StringNull(), types.StringValue("test")},
55+
),
56+
expectedDiagnostics: nil,
57+
},
58+
"unknown-value": {
59+
list: types.ListValueMust(
60+
types.StringType,
61+
[]attr.Value{types.StringUnknown()},
62+
),
63+
expectedDiagnostics: nil,
64+
},
65+
"unknown-values-duplicate": {
66+
list: types.ListValueMust(
67+
types.StringType,
68+
[]attr.Value{types.StringUnknown(), types.StringUnknown()},
69+
),
70+
expectedDiagnostics: nil,
71+
},
72+
"unknown-values-valid": {
73+
list: types.ListValueMust(
74+
types.StringType,
75+
[]attr.Value{types.StringUnknown(), types.StringValue("test")},
76+
),
77+
expectedDiagnostics: nil,
78+
},
79+
"known-value": {
80+
list: types.ListValueMust(
81+
types.StringType,
82+
[]attr.Value{types.StringValue("test")},
83+
),
84+
expectedDiagnostics: nil,
85+
},
86+
"known-values-duplicate": {
87+
list: types.ListValueMust(
88+
types.StringType,
89+
[]attr.Value{types.StringValue("test"), types.StringValue("test")},
90+
),
91+
expectedDiagnostics: diag.Diagnostics{
92+
diag.NewAttributeErrorDiagnostic(
93+
path.Root("test"),
94+
"Duplicate List Value",
95+
"This attribute contains duplicate values of: \"test\"",
96+
),
97+
},
98+
},
99+
"known-values-valid": {
100+
list: types.ListValueMust(
101+
types.StringType,
102+
[]attr.Value{types.StringValue("test1"), types.StringValue("test2")},
103+
),
104+
expectedDiagnostics: nil,
105+
},
106+
}
107+
108+
for name, testCase := range testCases {
109+
name, testCase := name, testCase
110+
111+
t.Run(name, func(t *testing.T) {
112+
t.Parallel()
113+
114+
request := validator.ListRequest{
115+
Path: path.Root("test"),
116+
PathExpression: path.MatchRoot("test"),
117+
ConfigValue: testCase.list,
118+
}
119+
response := validator.ListResponse{}
120+
listvalidator.UniqueValues().ValidateList(context.Background(), request, &response)
121+
122+
if diff := cmp.Diff(response.Diagnostics, testCase.expectedDiagnostics); diff != "" {
123+
t.Errorf("unexpected diagnostics difference: %s", diff)
124+
}
125+
})
126+
}
127+
}

0 commit comments

Comments
 (0)