From ac9ac23e8409ac390cb97ec5a000d24cfb53a420 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Dec 2022 15:50:29 -0500 Subject: [PATCH 1/2] listvalidator: Added UniqueValues validator Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/issues/67 --- .changelog/pending.txt | 3 + listvalidator/unique_values.go | 65 ++++++++++ listvalidator/unique_values_example_test.go | 24 ++++ listvalidator/unique_values_test.go | 127 ++++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 .changelog/pending.txt create mode 100644 listvalidator/unique_values.go create mode 100644 listvalidator/unique_values_example_test.go create mode 100644 listvalidator/unique_values_test.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 0000000..9784b25 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +listvalidator: Added `UniqueValues` validator +``` diff --git a/listvalidator/unique_values.go b/listvalidator/unique_values.go new file mode 100644 index 0000000..b6c6d05 --- /dev/null +++ b/listvalidator/unique_values.go @@ -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{} +} diff --git a/listvalidator/unique_values_example_test.go b/listvalidator/unique_values_example_test.go new file mode 100644 index 0000000..2274fb7 --- /dev/null +++ b/listvalidator/unique_values_example_test.go @@ -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(), + }, + }, + }, + } +} diff --git a/listvalidator/unique_values_test.go b/listvalidator/unique_values_test.go new file mode 100644 index 0000000..12b6d87 --- /dev/null +++ b/listvalidator/unique_values_test.go @@ -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-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) + } + }) + } +} From 53b58c2f27afecafb509f86af62011bb673bb69d Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Dec 2022 15:51:18 -0500 Subject: [PATCH 2/2] Update CHANGELOG for #88 --- .changelog/{pending.txt => 88.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 88.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/88.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/88.txt