Skip to content

Commit c53ddcf

Browse files
committed
mapvalidator: add NoNullValues validator
This validator will iterate over elements in the map, returning an error diagnostic if any null elements are detected.
1 parent daff222 commit c53ddcf

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed

mapvalidator/no_null_values.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapvalidator
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/function"
10+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
11+
)
12+
13+
var _ validator.Map = noNullValuesValidator{}
14+
var _ function.MapParameterValidator = noNullValuesValidator{}
15+
16+
type noNullValuesValidator struct{}
17+
18+
func (v noNullValuesValidator) Description(_ context.Context) string {
19+
return "null values are not permitted"
20+
}
21+
22+
func (v noNullValuesValidator) MarkdownDescription(ctx context.Context) string {
23+
return v.Description(ctx)
24+
}
25+
26+
func (v noNullValuesValidator) ValidateMap(_ context.Context, req validator.MapRequest, resp *validator.MapResponse) {
27+
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
28+
return
29+
}
30+
31+
elements := req.ConfigValue.Elements()
32+
33+
for _, e := range elements {
34+
// Only evaluate known values for null
35+
if e.IsUnknown() {
36+
continue
37+
}
38+
39+
if e.IsNull() {
40+
resp.Diagnostics.AddAttributeError(
41+
req.Path,
42+
"Null Map Value",
43+
"This attribute contains a null value.",
44+
)
45+
}
46+
}
47+
}
48+
49+
func (v noNullValuesValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) {
50+
if req.Value.IsNull() || req.Value.IsUnknown() {
51+
return
52+
}
53+
54+
elements := req.Value.Elements()
55+
56+
for _, e := range elements {
57+
// Only evaluate known values for null
58+
if e.IsUnknown() {
59+
continue
60+
}
61+
62+
if e.IsNull() {
63+
resp.Error = function.ConcatFuncErrors(
64+
resp.Error,
65+
function.NewArgumentFuncError(
66+
req.ArgumentPosition,
67+
"Null Map Value: This attribute contains a null value.",
68+
),
69+
)
70+
}
71+
}
72+
}
73+
74+
// NoNullValues returns a validator which ensures that any configured map
75+
// only contains non-null values.
76+
func NoNullValues() noNullValuesValidator {
77+
return noNullValuesValidator{}
78+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/function"
10+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
)
13+
14+
func ExampleNoNullValues() {
15+
// Used within a Schema method of a DataSource, Provider, or Resource
16+
_ = schema.Schema{
17+
Attributes: map[string]schema.Attribute{
18+
"example_attr": schema.MapAttribute{
19+
ElementType: types.StringType,
20+
Required: true,
21+
Validators: []validator.Map{
22+
// Validate this map must contain no null values.
23+
mapvalidator.NoNullValues(),
24+
},
25+
},
26+
},
27+
}
28+
}
29+
30+
func ExampleNoNullValues_function() {
31+
_ = function.Definition{
32+
Parameters: []function.Parameter{
33+
function.MapParameter{
34+
Name: "example_param",
35+
Validators: []function.MapParameterValidator{
36+
// Validate this map must contain no null values.
37+
mapvalidator.NoNullValues(),
38+
},
39+
},
40+
},
41+
}
42+
}

mapvalidator/no_null_values_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapvalidator_test
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
12+
"github.com/hashicorp/terraform-plugin-framework/attr"
13+
"github.com/hashicorp/terraform-plugin-framework/function"
14+
"github.com/hashicorp/terraform-plugin-framework/path"
15+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
16+
"github.com/hashicorp/terraform-plugin-framework/types"
17+
)
18+
19+
func TestNoNullValuesValidator(t *testing.T) {
20+
t.Parallel()
21+
22+
type testCase struct {
23+
val types.Map
24+
expectError bool
25+
}
26+
tests := map[string]testCase{
27+
"Map unknown": {
28+
val: types.MapUnknown(
29+
types.StringType,
30+
),
31+
expectError: false,
32+
},
33+
"Map null": {
34+
val: types.MapNull(
35+
types.StringType,
36+
),
37+
expectError: false,
38+
},
39+
"No null values": {
40+
val: types.MapValueMust(
41+
types.StringType,
42+
map[string]attr.Value{
43+
"key1": types.StringValue("first"),
44+
"key2": types.StringValue("second"),
45+
},
46+
),
47+
expectError: false,
48+
},
49+
"Unknown value": {
50+
val: types.MapValueMust(
51+
types.StringType,
52+
map[string]attr.Value{
53+
"key1": types.StringValue("first"),
54+
"key2": types.StringUnknown(),
55+
},
56+
),
57+
expectError: false,
58+
},
59+
"Null value": {
60+
val: types.MapValueMust(
61+
types.StringType,
62+
map[string]attr.Value{
63+
"key1": types.StringValue("first"),
64+
"key2": types.StringNull(),
65+
},
66+
),
67+
expectError: true,
68+
},
69+
}
70+
71+
for name, test := range tests {
72+
t.Run(fmt.Sprintf("ValidateMap - %s", name), func(t *testing.T) {
73+
t.Parallel()
74+
request := validator.MapRequest{
75+
Path: path.Root("test"),
76+
PathExpression: path.MatchRoot("test"),
77+
ConfigValue: test.val,
78+
}
79+
response := validator.MapResponse{}
80+
mapvalidator.NoNullValues().ValidateMap(context.TODO(), request, &response)
81+
82+
if !response.Diagnostics.HasError() && test.expectError {
83+
t.Fatal("expected error, got no error")
84+
}
85+
86+
if response.Diagnostics.HasError() && !test.expectError {
87+
t.Fatalf("got unexpected error: %s", response.Diagnostics)
88+
}
89+
})
90+
91+
t.Run(fmt.Sprintf("ValidateParameterMap - %s", name), func(t *testing.T) {
92+
t.Parallel()
93+
request := function.MapParameterValidatorRequest{
94+
ArgumentPosition: 0,
95+
Value: test.val,
96+
}
97+
response := function.MapParameterValidatorResponse{}
98+
mapvalidator.NoNullValues().ValidateParameterMap(context.TODO(), request, &response)
99+
100+
if response.Error == nil && test.expectError {
101+
t.Fatal("expected error, got no error")
102+
}
103+
104+
if response.Error != nil && !test.expectError {
105+
t.Fatalf("got unexpected error: %s", response.Error)
106+
}
107+
})
108+
}
109+
}

0 commit comments

Comments
 (0)