-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
listvalidator: Added UniqueValues validator (#88)
Reference: #67
- Loading branch information
Showing
4 changed files
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:enhancement | ||
listvalidator: Added `UniqueValues` validator | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |