Skip to content

Commit

Permalink
Add NoNullValues validators (#246)
Browse files Browse the repository at this point in the history
* listvalidator: add `NoNullValues` validator

This validator will iterate over elements in the list, returning an error diagnostic if any null elements are detected.

* setvalidator: add `NoNullValues` validator

This validator will iterate over elements in the set, returning an error diagnostic if any null elements are detected.

* mapvalidator: add `NoNullValues` validator

This validator will iterate over elements in the map, returning an error diagnostic if any null elements are detected.

* Documentation adjustments

* changelogs

---------
  • Loading branch information
jar-b authored Dec 12, 2024
1 parent e16013a commit 7d19faa
Show file tree
Hide file tree
Showing 12 changed files with 702 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20241212-140548.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'listvalidator: Added `NoNullValues` validator'
time: 2024-12-12T14:05:48.064529-05:00
custom:
Issue: "245"
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20241212-140613.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'mapvalidator: Added `NoNullValues` validator'
time: 2024-12-12T14:06:13.229216-05:00
custom:
Issue: "245"
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20241212-140624.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'setvalidator: Added `NoNullValues` validator'
time: 2024-12-12T14:06:24.967598-05:00
custom:
Issue: "245"
78 changes: 78 additions & 0 deletions listvalidator/no_null_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package listvalidator

import (
"context"

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

var _ validator.List = noNullValuesValidator{}
var _ function.ListParameterValidator = noNullValuesValidator{}

type noNullValuesValidator struct{}

func (v noNullValuesValidator) Description(_ context.Context) string {
return "All values in the list must be configured"
}

func (v noNullValuesValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

func (v noNullValuesValidator) ValidateList(_ context.Context, req validator.ListRequest, resp *validator.ListResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}

elements := req.ConfigValue.Elements()

for _, e := range elements {
// Only evaluate known values for null
if e.IsUnknown() {
continue
}

if e.IsNull() {
resp.Diagnostics.AddAttributeError(
req.Path,
"Null List Value",
"This attribute contains a null value.",
)
}
}
}

func (v noNullValuesValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) {
if req.Value.IsNull() || req.Value.IsUnknown() {
return
}

elements := req.Value.Elements()

for _, e := range elements {
// Only evaluate known values for null
if e.IsUnknown() {
continue
}

if e.IsNull() {
resp.Error = function.ConcatFuncErrors(
resp.Error,
function.NewArgumentFuncError(
req.ArgumentPosition,
"Null List Value: This attribute contains a null value.",
),
)
}
}
}

// NoNullValues returns a validator which ensures that any configured list
// only contains non-null values.
func NoNullValues() noNullValuesValidator {
return noNullValuesValidator{}
}
42 changes: 42 additions & 0 deletions listvalidator/no_null_values_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

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/function"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func ExampleNoNullValues() {
// 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 no null values.
listvalidator.NoNullValues(),
},
},
},
}
}

func ExampleNoNullValues_function() {
_ = function.Definition{
Parameters: []function.Parameter{
function.ListParameter{
Name: "example_param",
Validators: []function.ListParameterValidator{
// Validate this list must contain no null values.
listvalidator.NoNullValues(),
},
},
},
}
}
109 changes: 109 additions & 0 deletions listvalidator/no_null_values_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package listvalidator_test

import (
"context"
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

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

type testCase struct {
val types.List
expectError bool
}
tests := map[string]testCase{
"List unknown": {
val: types.ListUnknown(
types.StringType,
),
expectError: false,
},
"List null": {
val: types.ListNull(
types.StringType,
),
expectError: false,
},
"No null values": {
val: types.ListValueMust(
types.StringType,
[]attr.Value{
types.StringValue("first"),
types.StringValue("second"),
},
),
expectError: false,
},
"Unknown value": {
val: types.ListValueMust(
types.StringType,
[]attr.Value{
types.StringValue("first"),
types.StringUnknown(),
},
),
expectError: false,
},
"Null value": {
val: types.ListValueMust(
types.StringType,
[]attr.Value{
types.StringValue("first"),
types.StringNull(),
},
),
expectError: true,
},
}

for name, test := range tests {
t.Run(fmt.Sprintf("ValidateList - %s", name), func(t *testing.T) {
t.Parallel()
request := validator.ListRequest{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: test.val,
}
response := validator.ListResponse{}
listvalidator.NoNullValues().ValidateList(context.TODO(), request, &response)

if !response.Diagnostics.HasError() && test.expectError {
t.Fatal("expected error, got no error")
}

if response.Diagnostics.HasError() && !test.expectError {
t.Fatalf("got unexpected error: %s", response.Diagnostics)
}
})

t.Run(fmt.Sprintf("ValidateParameterList - %s", name), func(t *testing.T) {
t.Parallel()
request := function.ListParameterValidatorRequest{
ArgumentPosition: 0,
Value: test.val,
}
response := function.ListParameterValidatorResponse{}
listvalidator.NoNullValues().ValidateParameterList(context.TODO(), request, &response)

if response.Error == nil && test.expectError {
t.Fatal("expected error, got no error")
}

if response.Error != nil && !test.expectError {
t.Fatalf("got unexpected error: %s", response.Error)
}
})
}
}
78 changes: 78 additions & 0 deletions mapvalidator/no_null_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package mapvalidator

import (
"context"

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

var _ validator.Map = noNullValuesValidator{}
var _ function.MapParameterValidator = noNullValuesValidator{}

type noNullValuesValidator struct{}

func (v noNullValuesValidator) Description(_ context.Context) string {
return "All values in the map must be configured"
}

func (v noNullValuesValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

func (v noNullValuesValidator) ValidateMap(_ context.Context, req validator.MapRequest, resp *validator.MapResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}

elements := req.ConfigValue.Elements()

for _, e := range elements {
// Only evaluate known values for null
if e.IsUnknown() {
continue
}

if e.IsNull() {
resp.Diagnostics.AddAttributeError(
req.Path,
"Null Map Value",
"This attribute contains a null value.",
)
}
}
}

func (v noNullValuesValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) {
if req.Value.IsNull() || req.Value.IsUnknown() {
return
}

elements := req.Value.Elements()

for _, e := range elements {
// Only evaluate known values for null
if e.IsUnknown() {
continue
}

if e.IsNull() {
resp.Error = function.ConcatFuncErrors(
resp.Error,
function.NewArgumentFuncError(
req.ArgumentPosition,
"Null Map Value: This attribute contains a null value.",
),
)
}
}
}

// NoNullValues returns a validator which ensures that any configured map
// only contains non-null values.
func NoNullValues() noNullValuesValidator {
return noNullValuesValidator{}
}
42 changes: 42 additions & 0 deletions mapvalidator/no_null_values_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package mapvalidator_test

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

func ExampleNoNullValues() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attr": schema.MapAttribute{
ElementType: types.StringType,
Required: true,
Validators: []validator.Map{
// Validate this map must contain no null values.
mapvalidator.NoNullValues(),
},
},
},
}
}

func ExampleNoNullValues_function() {
_ = function.Definition{
Parameters: []function.Parameter{
function.MapParameter{
Name: "example_param",
Validators: []function.MapParameterValidator{
// Validate this map must contain no null values.
mapvalidator.NoNullValues(),
},
},
},
}
}
Loading

0 comments on commit 7d19faa

Please sign in to comment.