Skip to content

Introduce Data Source, Provider, and Resource Level Validators #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changelog/60.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:feature
Introduced `datasourcevalidator` package with `AtLeastOneOf()`, `Conflicting()`, `ExactlyOneOf()`, and `RequiredTogether()` validation functions
```

```release-note:feature
Introduced `providervalidator` package with `AtLeastOneOf()`, `Conflicting()`, `ExactlyOneOf()`, and `RequiredTogether()` validation functions
```

```release-note:feature
Introduced `resourcevalidator` package with `AtLeastOneOf()`, `Conflicting()`, `ExactlyOneOf()`, and `RequiredTogether()` validation functions
```
15 changes: 15 additions & 0 deletions datasourcevalidator/at_least_one_of.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package datasourcevalidator

import (
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// AtLeastOneOf checks that a set of path.Expression has at least one non-null
// or unknown value.
func AtLeastOneOf(expressions ...path.Expression) datasource.ConfigValidator {
return &configvalidator.AtLeastOneOfValidator{
PathExpressions: expressions,
}
}
19 changes: 19 additions & 0 deletions datasourcevalidator/at_least_one_of_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package datasourcevalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
)

func ExampleAtLeastOneOf() {
// Used inside a datasource.DataSource type ConfigValidators method
_ = []datasource.ConfigValidator{
// Validate at least one of the schema defined attributes named attr1
// and attr2 has a known, non-null value.
datasourcevalidator.AtLeastOneOf(
path.MatchRoot("attr1"),
path.MatchRoot("attr2"),
),
}
}
125 changes: 125 additions & 0 deletions datasourcevalidator/at_least_one_of_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package datasourcevalidator_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

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

testCases := map[string]struct {
pathExpressions path.Expressions
req datasource.ValidateConfigRequest
expected *datasource.ValidateConfigResponse
}{
"no-diagnostics": {
pathExpressions: path.Expressions{
path.MatchRoot("test"),
},
req: datasource.ValidateConfigRequest{
Config: tfsdk.Config{
Schema: tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test": {
Optional: true,
Type: types.StringType,
},
"other": {
Optional: true,
Type: types.StringType,
},
},
},
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test": tftypes.String,
"other": tftypes.String,
},
},
map[string]tftypes.Value{
"test": tftypes.NewValue(tftypes.String, "test-value"),
"other": tftypes.NewValue(tftypes.String, "test-value"),
},
),
},
},
expected: &datasource.ValidateConfigResponse{},
},
"diagnostics": {
pathExpressions: path.Expressions{
path.MatchRoot("test1"),
path.MatchRoot("test2"),
},
req: datasource.ValidateConfigRequest{
Config: tfsdk.Config{
Schema: tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test1": {
Optional: true,
Type: types.StringType,
},
"test2": {
Optional: true,
Type: types.StringType,
},
"other": {
Optional: true,
Type: types.StringType,
},
},
},
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test1": tftypes.String,
"test2": tftypes.String,
"other": tftypes.String,
},
},
map[string]tftypes.Value{
"test1": tftypes.NewValue(tftypes.String, nil),
"test2": tftypes.NewValue(tftypes.String, nil),
"other": tftypes.NewValue(tftypes.String, "test-value"),
},
),
},
},
expected: &datasource.ValidateConfigResponse{
Diagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Missing Attribute Configuration",
"At least one of these attributes must be configured: [test1,test2]",
),
},
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

validator := datasourcevalidator.AtLeastOneOf(testCase.pathExpressions...)
got := &datasource.ValidateConfigResponse{}

validator.ValidateDataSource(context.Background(), testCase.req, got)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
15 changes: 15 additions & 0 deletions datasourcevalidator/conflicting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package datasourcevalidator

import (
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// Conflicting checks that a set of path.Expression, are not configured
// simultaneously.
func Conflicting(expressions ...path.Expression) datasource.ConfigValidator {
return &configvalidator.ConflictingValidator{
PathExpressions: expressions,
}
}
19 changes: 19 additions & 0 deletions datasourcevalidator/conflicting_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package datasourcevalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
)

func ExampleConflicting() {
// Used inside a datasource.DataSource type ConfigValidators method
_ = []datasource.ConfigValidator{
// Validate that schema defined attributes named attr1 and attr2 are not
// both configured with known, non-null values.
datasourcevalidator.Conflicting(
path.MatchRoot("attr1"),
path.MatchRoot("attr2"),
),
}
}
126 changes: 126 additions & 0 deletions datasourcevalidator/conflicting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package datasourcevalidator_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

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

testCases := map[string]struct {
pathExpressions path.Expressions
req datasource.ValidateConfigRequest
expected *datasource.ValidateConfigResponse
}{
"no-diagnostics": {
pathExpressions: path.Expressions{
path.MatchRoot("test"),
},
req: datasource.ValidateConfigRequest{
Config: tfsdk.Config{
Schema: tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test": {
Optional: true,
Type: types.StringType,
},
"other": {
Optional: true,
Type: types.StringType,
},
},
},
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test": tftypes.String,
"other": tftypes.String,
},
},
map[string]tftypes.Value{
"test": tftypes.NewValue(tftypes.String, "test-value"),
"other": tftypes.NewValue(tftypes.String, "test-value"),
},
),
},
},
expected: &datasource.ValidateConfigResponse{},
},
"diagnostics": {
pathExpressions: path.Expressions{
path.MatchRoot("test1"),
path.MatchRoot("test2"),
},
req: datasource.ValidateConfigRequest{
Config: tfsdk.Config{
Schema: tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test1": {
Optional: true,
Type: types.StringType,
},
"test2": {
Optional: true,
Type: types.StringType,
},
"other": {
Optional: true,
Type: types.StringType,
},
},
},
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test1": tftypes.String,
"test2": tftypes.String,
"other": tftypes.String,
},
},
map[string]tftypes.Value{
"test1": tftypes.NewValue(tftypes.String, "test-value"),
"test2": tftypes.NewValue(tftypes.String, "test-value"),
"other": tftypes.NewValue(tftypes.String, "test-value"),
},
),
},
},
expected: &datasource.ValidateConfigResponse{
Diagnostics: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("test1"),
"Invalid Attribute Combination",
"These attributes cannot be configured together: [test1,test2]",
),
},
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

validator := datasourcevalidator.Conflicting(testCase.pathExpressions...)
got := &datasource.ValidateConfigResponse{}

validator.ValidateDataSource(context.Background(), testCase.req, got)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
10 changes: 10 additions & 0 deletions datasourcevalidator/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Package datasourcevalidator provides validators to express relationships
// between multiple attributes of a data source. For example, checking that
// multiple attributes are not configured at the same time.
//
// These validators are implemented outside the schema, which may be easier to
// implement in provider code generation situations or suit provider code
// preferences differently than those in the schemavalidator package. Those
// validators start on a starting attribute, where relationships can be
// expressed as absolute paths to others or relative to the starting attribute.
package datasourcevalidator
15 changes: 15 additions & 0 deletions datasourcevalidator/exactly_one_of.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package datasourcevalidator

import (
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// ExactlyOneOf checks that a set of path.Expression does not have more than
// one known value.
func ExactlyOneOf(expressions ...path.Expression) datasource.ConfigValidator {
return &configvalidator.ExactlyOneOfValidator{
PathExpressions: expressions,
}
}
19 changes: 19 additions & 0 deletions datasourcevalidator/exactly_one_of_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package datasourcevalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
)

func ExampleExactlyOneOf() {
// Used inside a datasource.DataSource type ConfigValidators method
_ = []datasource.ConfigValidator{
// Validate only one of the schema defined attributes named attr1
// and attr2 has a known, non-null value.
datasourcevalidator.ExactlyOneOf(
path.MatchRoot("attr1"),
path.MatchRoot("attr2"),
),
}
}
Loading