Skip to content
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

ephemeralvalidator: Introduce new package for common ephemeral resource configuration validators #242

Merged
merged 8 commits into from
Oct 31, 2024
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
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