Skip to content

Commit

Permalink
Add IsRequired validators to list, set, and object validators…
Browse files Browse the repository at this point in the history
… for Blocks (#107)

* add `listvalidator` and diag helper

* renamed to is_required

* add set validator

* added object validator

* renamed to block

* added examples and removed line in comment
  • Loading branch information
austinvalle authored Feb 8, 2023
1 parent f5056ff commit f27f636
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 0 deletions.
9 changes: 9 additions & 0 deletions helpers/validatordiag/diag.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import (
"github.com/hashicorp/terraform-plugin-framework/path"
)

// InvalidBlockDiagnostic returns an error Diagnostic to be used when a block is invalid
func InvalidBlockDiagnostic(path path.Path, description string) diag.Diagnostic {
return diag.NewAttributeErrorDiagnostic(
path,
"Invalid Block",
fmt.Sprintf("Block %s %s", path, description),
)
}

// InvalidAttributeValueDiagnostic returns an error Diagnostic to be used when an attribute has an invalid value.
func InvalidAttributeValueDiagnostic(path path.Path, description string, value string) diag.Diagnostic {
return diag.NewAttributeErrorDiagnostic(
Expand Down
41 changes: 41 additions & 0 deletions listvalidator/is_required.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package listvalidator

import (
"context"

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

var _ validator.List = isRequiredValidator{}

// isRequiredValidator validates that a list has a configuration value.
type isRequiredValidator struct{}

// Description describes the validation in plain text formatting.
func (v isRequiredValidator) Description(_ context.Context) string {
return "must have a configuration value as the provider has marked it as required"
}

// MarkdownDescription describes the validation in Markdown formatting.
func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

// Validate performs the validation.
func (v isRequiredValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
if req.ConfigValue.IsNull() {
resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic(
req.Path,
v.Description(ctx),
))
}
}

// IsRequired returns a validator which ensures that any configured list has a value (not null).
//
// This validator is equivalent to the `Required` field on attributes and is only
// practical for use with `schema.ListNestedBlock`
func IsRequired() validator.List {
return isRequiredValidator{}
}
28 changes: 28 additions & 0 deletions listvalidator/is_required_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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"
)

func ExampleIsRequired() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Blocks: map[string]schema.Block{
"example_block": schema.ListNestedBlock{
Validators: []validator.List{
// Validate this block has a value (not null).
listvalidator.IsRequired(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"example_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
},
},
}
}
73 changes: 73 additions & 0 deletions listvalidator/is_required_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package listvalidator_test

import (
"context"
"testing"

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

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

type testCase struct {
val types.List
expectError bool
}
tests := map[string]testCase{
"List null": {
val: types.ListNull(
types.StringType,
),
expectError: true,
},
"List unknown": {
val: types.ListUnknown(
types.StringType,
),
expectError: false,
},
"List empty": {
val: types.ListValueMust(
types.StringType,
[]attr.Value{},
),
expectError: false,
},
"List with elements": {
val: types.ListValueMust(
types.StringType,
[]attr.Value{
types.StringValue("first"),
},
),
expectError: false,
},
}

for name, test := range tests {
name, test := name, test
t.Run(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.IsRequired().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)
}
})
}
}
41 changes: 41 additions & 0 deletions objectvalidator/is_required.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package objectvalidator

import (
"context"

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

var _ validator.Object = isRequiredValidator{}

// isRequiredValidator validates that an object has a configuration value.
type isRequiredValidator struct{}

// Description describes the validation in plain text formatting.
func (v isRequiredValidator) Description(_ context.Context) string {
return "must have a configuration value as the provider has marked it as required"
}

// MarkdownDescription describes the validation in Markdown formatting.
func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

// Validate performs the validation.
func (v isRequiredValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
if req.ConfigValue.IsNull() {
resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic(
req.Path,
v.Description(ctx),
))
}
}

// IsRequired returns a validator which ensures that any configured object has a value (not null).
//
// This validator is equivalent to the `Required` field on attributes and is only
// practical for use with `schema.SingleNestedBlock`
func IsRequired() validator.Object {
return isRequiredValidator{}
}
26 changes: 26 additions & 0 deletions objectvalidator/is_required_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package objectvalidator_test

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

func ExampleIsRequired() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Blocks: map[string]schema.Block{
"example_block": schema.SingleNestedBlock{
Validators: []validator.Object{
// Validate this block has a value (not null).
objectvalidator.IsRequired(),
},
Attributes: map[string]schema.Attribute{
"example_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
},
}
}
83 changes: 83 additions & 0 deletions objectvalidator/is_required_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package objectvalidator_test

import (
"context"
"testing"

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

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

type testCase struct {
val types.Object
expectError bool
}
tests := map[string]testCase{
"Object null": {
val: types.ObjectNull(
map[string]attr.Type{
"field1": types.StringType,
},
),
expectError: true,
},
"Object unknown": {
val: types.ObjectUnknown(
map[string]attr.Type{
"field1": types.StringType,
},
),
expectError: false,
},
"Object empty": {
val: types.ObjectValueMust(
map[string]attr.Type{
"field1": types.StringType,
},
map[string]attr.Value{
"field1": types.StringNull(),
},
),
expectError: false,
},
"Object with elements": {
val: types.ObjectValueMust(
map[string]attr.Type{
"field1": types.StringType,
},
map[string]attr.Value{
"field1": types.StringValue("value1"),
},
),
expectError: false,
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()
request := validator.ObjectRequest{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: test.val,
}
response := validator.ObjectResponse{}
objectvalidator.IsRequired().ValidateObject(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)
}
})
}
}
41 changes: 41 additions & 0 deletions setvalidator/is_required.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package setvalidator

import (
"context"

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

var _ validator.Set = isRequiredValidator{}

// isRequiredValidator validates that a set has a configuration value.
type isRequiredValidator struct{}

// Description describes the validation in plain text formatting.
func (v isRequiredValidator) Description(_ context.Context) string {
return "must have a configuration value as the provider has marked it as required"
}

// MarkdownDescription describes the validation in Markdown formatting.
func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

// Validate performs the validation.
func (v isRequiredValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) {
if req.ConfigValue.IsNull() {
resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic(
req.Path,
v.Description(ctx),
))
}
}

// IsRequired returns a validator which ensures that any configured set has a value (not null).
//
// This validator is equivalent to the `Required` field on attributes and is only
// practical for use with `schema.SetNestedBlock`
func IsRequired() validator.Set {
return isRequiredValidator{}
}
28 changes: 28 additions & 0 deletions setvalidator/is_required_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package setvalidator_test

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

func ExampleIsRequired() {
// Used within a Schema method of a DataSource, Provider, or Resource
_ = schema.Schema{
Blocks: map[string]schema.Block{
"example_block": schema.SetNestedBlock{
Validators: []validator.Set{
// Validate this block has a value (not null).
setvalidator.IsRequired(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"example_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
},
},
}
}
Loading

0 comments on commit f27f636

Please sign in to comment.