Skip to content

Commit

Permalink
ephemeralvalidator: Introduce new package for common ephemeral resour…
Browse files Browse the repository at this point in the history
…ce configuration validators (#242)

* use WIP framework branch

* implementation of the shared config validators for ephemeral resources

* update go mod

* go mod tidy

* changelog
  • Loading branch information
austinvalle authored Oct 31, 2024
1 parent 761f545 commit b793fd3
Show file tree
Hide file tree
Showing 33 changed files with 1,941 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20241030-164618.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'ephemeralvalidator: Introduce new package with declarative validators for ephemeral
resource configurations'
time: 2024-10-30T16:46:18.935223-04:00
custom:
Issue: "242"
57 changes: 57 additions & 0 deletions ephemeralvalidator/all.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ephemeralvalidator

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/ephemeral"
)

// All returns a validator which ensures that any configured attribute value
// validates against all the given validators.
//
// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings
// as the Validators field automatically applies a logical AND.
func All(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator {
return allValidator{
validators: validators,
}
}

var _ ephemeral.ConfigValidator = allValidator{}

// allValidator implements the validator.
type allValidator struct {
validators []ephemeral.ConfigValidator
}

// Description describes the validation in plain text formatting.
func (v allValidator) Description(ctx context.Context) string {
var descriptions []string

for _, subValidator := range v.validators {
descriptions = append(descriptions, subValidator.Description(ctx))
}

return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + "))
}

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

// ValidateEphemeralResource performs the validation.
func (v allValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
for _, subValidator := range v.validators {
validateResp := &ephemeral.ValidateConfigResponse{}

subValidator.ValidateEphemeralResource(ctx, req, validateResp)

resp.Diagnostics.Append(validateResp.Diagnostics...)
}
}
21 changes: 21 additions & 0 deletions ephemeralvalidator/all_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ephemeralvalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework/ephemeral"

"github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator"
)

func ExampleAll() {
// Used inside a ephemeral.EphemeralResource type ConfigValidators method
_ = []ephemeral.ConfigValidator{
// The configuration must satisfy either All validator.
ephemeralvalidator.Any(
ephemeralvalidator.All( /* ... */ ),
ephemeralvalidator.All( /* ... */ ),
),
}
}
178 changes: 178 additions & 0 deletions ephemeralvalidator/all_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ephemeralvalidator_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-go/tftypes"

"github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator"
)

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

testCases := map[string]struct {
validators []ephemeral.ConfigValidator
req ephemeral.ValidateConfigRequest
expected *ephemeral.ValidateConfigResponse
}{
"no-diagnostics": {
validators: []ephemeral.ConfigValidator{
ephemeralvalidator.ExactlyOneOf(
path.MatchRoot("test1"),
path.MatchRoot("test2"),
),
ephemeralvalidator.All(
ephemeralvalidator.AtLeastOneOf(
path.MatchRoot("test3"),
path.MatchRoot("test4"),
),
ephemeralvalidator.Conflicting(
path.MatchRoot("test3"),
path.MatchRoot("test5"),
),
),
},
req: ephemeral.ValidateConfigRequest{
Config: tfsdk.Config{
Schema: schema.Schema{
Attributes: map[string]schema.Attribute{
"test1": schema.StringAttribute{
Optional: true,
},
"test2": schema.StringAttribute{
Optional: true,
},
"test3": schema.StringAttribute{
Optional: true,
},
"test4": schema.StringAttribute{
Optional: true,
},
"test5": schema.StringAttribute{
Optional: true,
},
},
},
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test1": tftypes.String,
"test2": tftypes.String,
"test3": tftypes.String,
"test4": tftypes.String,
"test5": tftypes.String,
},
},
map[string]tftypes.Value{
"test1": tftypes.NewValue(tftypes.String, nil),
"test2": tftypes.NewValue(tftypes.String, nil),
"test3": tftypes.NewValue(tftypes.String, "test-value"),
"test4": tftypes.NewValue(tftypes.String, nil),
"test5": tftypes.NewValue(tftypes.String, nil),
},
),
},
},
expected: &ephemeral.ValidateConfigResponse{},
},
"diagnostics": {
validators: []ephemeral.ConfigValidator{
ephemeralvalidator.ExactlyOneOf(
path.MatchRoot("test1"),
path.MatchRoot("test2"),
),
ephemeralvalidator.All(
ephemeralvalidator.AtLeastOneOf(
path.MatchRoot("test3"),
path.MatchRoot("test4"),
),
ephemeralvalidator.Conflicting(
path.MatchRoot("test3"),
path.MatchRoot("test5"),
),
),
},
req: ephemeral.ValidateConfigRequest{
Config: tfsdk.Config{
Schema: schema.Schema{
Attributes: map[string]schema.Attribute{
"test1": schema.StringAttribute{
Optional: true,
},
"test2": schema.StringAttribute{
Optional: true,
},
"test3": schema.StringAttribute{
Optional: true,
},
"test4": schema.StringAttribute{
Optional: true,
},
"test5": schema.StringAttribute{
Optional: true,
},
},
},
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test1": tftypes.String,
"test2": tftypes.String,
"test3": tftypes.String,
"test4": tftypes.String,
"test5": tftypes.String,
},
},
map[string]tftypes.Value{
"test1": tftypes.NewValue(tftypes.String, nil),
"test2": tftypes.NewValue(tftypes.String, nil),
"test3": tftypes.NewValue(tftypes.String, "test-value"),
"test4": tftypes.NewValue(tftypes.String, nil),
"test5": tftypes.NewValue(tftypes.String, "test-value"),
},
),
},
},
expected: &ephemeral.ValidateConfigResponse{
Diagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Missing Attribute Configuration",
"Exactly one of these attributes must be configured: [test1,test2]",
),
diag.WithPath(path.Root("test3"),
diag.NewErrorDiagnostic(
"Invalid Attribute Combination",
"These attributes cannot be configured together: [test3,test5]",
)),
},
},
},
}

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

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

got := &ephemeral.ValidateConfigResponse{}

ephemeralvalidator.Any(testCase.validators...).ValidateEphemeralResource(context.Background(), testCase.req, got)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
65 changes: 65 additions & 0 deletions ephemeralvalidator/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ephemeralvalidator

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/ephemeral"
)

// Any returns a validator which ensures that any configured attribute value
// passes at least one of the given validators.
//
// To prevent practitioner confusion should non-passing validators have
// conflicting logic, only warnings from the passing validator are returned.
// Use AnyWithAllWarnings() to return warnings from non-passing validators
// as well.
func Any(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator {
return anyValidator{
validators: validators,
}
}

var _ ephemeral.ConfigValidator = anyValidator{}

// anyValidator implements the validator.
type anyValidator struct {
validators []ephemeral.ConfigValidator
}

// Description describes the validation in plain text formatting.
func (v anyValidator) Description(ctx context.Context) string {
var descriptions []string

for _, subValidator := range v.validators {
descriptions = append(descriptions, subValidator.Description(ctx))
}

return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + "))
}

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

// ValidateEphemeralResource performs the validation.
func (v anyValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
for _, subValidator := range v.validators {
validateResp := &ephemeral.ValidateConfigResponse{}

subValidator.ValidateEphemeralResource(ctx, req, validateResp)

if !validateResp.Diagnostics.HasError() {
resp.Diagnostics = validateResp.Diagnostics

return
}

resp.Diagnostics.Append(validateResp.Diagnostics...)
}
}
17 changes: 17 additions & 0 deletions ephemeralvalidator/any_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ephemeralvalidator_test

import (
"github.com/hashicorp/terraform-plugin-framework/ephemeral"

"github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator"
)

func ExampleAny() {
// Used inside a ephemeral.EphemeralResource type ConfigValidators method
_ = []ephemeral.ConfigValidator{
ephemeralvalidator.Any( /* ... */ ),
}
}
Loading

0 comments on commit b793fd3

Please sign in to comment.