Skip to content

Commit

Permalink
listvalidator: Added UniqueValues validator (#88)
Browse files Browse the repository at this point in the history
Reference: #67
  • Loading branch information
bflad authored Dec 20, 2022
1 parent 692fbd3 commit 8e6bfb1
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/88.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
listvalidator: Added `UniqueValues` validator
```
65 changes: 65 additions & 0 deletions listvalidator/unique_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package listvalidator

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

var _ validator.List = uniqueValuesValidator{}

// uniqueValuesValidator implements the validator.
type uniqueValuesValidator struct{}

// Description returns the plaintext description of the validator.
func (v uniqueValuesValidator) Description(_ context.Context) string {
return "all values must be unique"
}

// MarkdownDescription returns the Markdown description of the validator.
func (v uniqueValuesValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

// ValidateList implements the validation logic.
func (v uniqueValuesValidator) ValidateList(_ context.Context, req validator.ListRequest, resp *validator.ListResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}

elements := req.ConfigValue.Elements()

for indexOuter, elementOuter := range elements {
// Only evaluate known values for duplicates.
if elementOuter.IsUnknown() {
continue
}

for indexInner := indexOuter + 1; indexInner < len(elements); indexInner++ {
elementInner := elements[indexInner]

if elementInner.IsUnknown() {
continue
}

if !elementInner.Equal(elementOuter) {
continue
}

resp.Diagnostics.AddAttributeError(
req.Path,
"Duplicate List Value",
fmt.Sprintf("This attribute contains duplicate values of: %s", elementInner),
)
}
}
}

// UniqueValues returns a validator which ensures that any configured list
// only contains unique values. This is similar to using a set attribute type
// which inherently validates unique values, but with list ordering semantics.
// Null (unconfigured) and unknown (known after apply) values are skipped.
func UniqueValues() validator.List {
return uniqueValuesValidator{}
}
24 changes: 24 additions & 0 deletions listvalidator/unique_values_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package listvalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func ExampleUniqueValues() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attr": schema.ListAttribute{
ElementType: types.StringType,
Required: true,
Validators: []validator.List{
// Validate this list must contain only unique values.
listvalidator.UniqueValues(),
},
},
},
}
}
127 changes: 127 additions & 0 deletions listvalidator/unique_values_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package listvalidator_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

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

testCases := map[string]struct {
list types.List
expectedDiagnostics diag.Diagnostics
}{
"null-list": {
list: types.ListNull(types.StringType),
expectedDiagnostics: nil,
},
"unknown-list": {
list: types.ListUnknown(types.StringType),
expectedDiagnostics: nil,
},
"null-value": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringNull()},
),
expectedDiagnostics: nil,
},
"null-values-duplicate": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringNull(), types.StringNull()},
),
expectedDiagnostics: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("test"),
"Duplicate List Value",
"This attribute contains duplicate values of: <null>",
),
},
},
"null-values-valid": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringNull(), types.StringValue("test")},
),
expectedDiagnostics: nil,
},
"unknown-value": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringUnknown()},
),
expectedDiagnostics: nil,
},
"unknown-values-duplicate": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringUnknown(), types.StringUnknown()},
),
expectedDiagnostics: nil,
},
"unknown-values-valid": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringUnknown(), types.StringValue("test")},
),
expectedDiagnostics: nil,
},
"known-value": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringValue("test")},
),
expectedDiagnostics: nil,
},
"known-values-duplicate": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringValue("test"), types.StringValue("test")},
),
expectedDiagnostics: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("test"),
"Duplicate List Value",
"This attribute contains duplicate values of: \"test\"",
),
},
},
"known-values-valid": {
list: types.ListValueMust(
types.StringType,
[]attr.Value{types.StringValue("test1"), types.StringValue("test2")},
),
expectedDiagnostics: nil,
},
}

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

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

request := validator.ListRequest{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: testCase.list,
}
response := validator.ListResponse{}
listvalidator.UniqueValues().ValidateList(context.Background(), request, &response)

if diff := cmp.Diff(response.Diagnostics, testCase.expectedDiagnostics); diff != "" {
t.Errorf("unexpected diagnostics difference: %s", diff)
}
})
}
}

0 comments on commit 8e6bfb1

Please sign in to comment.