From 189f02b4c480899398d1333a656932ed226a5d35 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 23 Jun 2022 16:10:28 +0100 Subject: [PATCH 01/20] Marking unused arguments as such --- listvalidator/size_at_least.go | 2 +- listvalidator/size_at_most.go | 2 +- listvalidator/size_between.go | 2 +- mapvalidator/size_at_least.go | 2 +- mapvalidator/size_at_most.go | 2 +- mapvalidator/size_between.go | 2 +- setvalidator/size_at_least.go | 2 +- setvalidator/size_at_most.go | 2 +- setvalidator/size_between.go | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/listvalidator/size_at_least.go b/listvalidator/size_at_least.go index f7724e2e..66c00cfe 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -16,7 +16,7 @@ type sizeAtLeastValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtLeastValidator) Description(ctx context.Context) string { +func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements", v.min) } diff --git a/listvalidator/size_at_most.go b/listvalidator/size_at_most.go index eb767290..a4dea246 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -16,7 +16,7 @@ type sizeAtMostValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtMostValidator) Description(ctx context.Context) string { +func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at most %d elements", v.max) } diff --git a/listvalidator/size_between.go b/listvalidator/size_between.go index 7b7e0672..9c3c5a12 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -18,7 +18,7 @@ type sizeBetweenValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeBetweenValidator) Description(ctx context.Context) string { +func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements and at most %d elements", v.min, v.max) } diff --git a/mapvalidator/size_at_least.go b/mapvalidator/size_at_least.go index 04bb6417..c8cd3363 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -16,7 +16,7 @@ type sizeAtLeastValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtLeastValidator) Description(ctx context.Context) string { +func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements", v.min) } diff --git a/mapvalidator/size_at_most.go b/mapvalidator/size_at_most.go index db6f6d75..cec24759 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -16,7 +16,7 @@ type sizeAtMostValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtMostValidator) Description(ctx context.Context) string { +func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at most %d elements", v.max) } diff --git a/mapvalidator/size_between.go b/mapvalidator/size_between.go index e7d284e1..033bfc34 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -18,7 +18,7 @@ type sizeBetweenValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeBetweenValidator) Description(ctx context.Context) string { +func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements and at most %d elements", v.min, v.max) } diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go index 78caea18..491cf6db 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -16,7 +16,7 @@ type sizeAtLeastValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtLeastValidator) Description(ctx context.Context) string { +func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements", v.min) } diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go index fe296bdb..e84e6daf 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -16,7 +16,7 @@ type sizeAtMostValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtMostValidator) Description(ctx context.Context) string { +func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at most %d elements", v.max) } diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go index 79ea8065..e3b2240b 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -18,7 +18,7 @@ type sizeBetweenValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeBetweenValidator) Description(ctx context.Context) string { +func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements and at most %d elements", v.min, v.max) } From 02ff6bf91a4ea8f0e4c7d187e6d444919fd09fc5 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 23 Jun 2022 16:31:43 +0100 Subject: [PATCH 02/20] temporary: attributepath helpers --- helpers/attributepath/contains.go | 16 ++++ helpers/attributepath/doc.go | 2 + helpers/attributepath/to_string.go | 46 ++++++++++ helpers/attributepath/to_string_test.go | 108 ++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 helpers/attributepath/contains.go create mode 100644 helpers/attributepath/doc.go create mode 100644 helpers/attributepath/to_string.go create mode 100644 helpers/attributepath/to_string_test.go diff --git a/helpers/attributepath/contains.go b/helpers/attributepath/contains.go new file mode 100644 index 00000000..6d27d61f --- /dev/null +++ b/helpers/attributepath/contains.go @@ -0,0 +1,16 @@ +package attributepath + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Contains returns true if needle (one *tftypes.AttributePath) +// can be found in haystack (collection of *tftypes.AttributePath). +func Contains(needle *tftypes.AttributePath, haystack ...*tftypes.AttributePath) bool { + for _, p := range haystack { + if needle.Equal(p) { + return true + } + } + return false +} diff --git a/helpers/attributepath/doc.go b/helpers/attributepath/doc.go new file mode 100644 index 00000000..c6d64c3f --- /dev/null +++ b/helpers/attributepath/doc.go @@ -0,0 +1,2 @@ +// Package attributepath provides helpers to interact with tftypes.AttributePath. +package attributepath diff --git a/helpers/attributepath/to_string.go b/helpers/attributepath/to_string.go new file mode 100644 index 00000000..5c562390 --- /dev/null +++ b/helpers/attributepath/to_string.go @@ -0,0 +1,46 @@ +package attributepath + +import ( + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ToString takes all the tftypes.AttributePathStep in a tftypes.AttributePath and concatenates them, +// using `.` as separator. +// +// This should be used only when trying to "print out" a tftypes.AttributePath in a log or an error message. +func ToString(path *tftypes.AttributePath) string { + var res strings.Builder + for pos, step := range path.Steps() { + switch v := step.(type) { + case tftypes.AttributeName: + if pos != 0 { + res.WriteString(".") + } + res.WriteString(string(v)) + case tftypes.ElementKeyString: + res.WriteString("[\"" + string(v) + "\"]") + case tftypes.ElementKeyInt: + res.WriteString("[" + strconv.FormatInt(int64(v), 10) + "]") + case tftypes.ElementKeyValue: + res.WriteString("[" + tftypes.Value(v).String() + "]") + } + } + + return res.String() +} + +// JoinToString works similarly to strings.Join: it takes a collection of *tftypes.AttributePath, +// applies to each ToString, and the resulting strings with a `,` separator. +// +// This should be used only when trying to "print out" a tftypes.AttributePath in a log or an error message. +func JoinToString(paths ...*tftypes.AttributePath) string { + res := make([]string, len(paths)) + for i, path := range paths { + res[i] = ToString(path) + } + + return strings.Join(res, ",") +} diff --git a/helpers/attributepath/to_string_test.go b/helpers/attributepath/to_string_test.go new file mode 100644 index 00000000..b04be2a1 --- /dev/null +++ b/helpers/attributepath/to_string_test.go @@ -0,0 +1,108 @@ +package attributepath_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestToString(t *testing.T) { + t.Parallel() + + type testCase struct { + in *tftypes.AttributePath + exp string + } + + testCases := map[string]testCase{ + "only-attribute-names": { + in: tftypes.NewAttributePath().WithAttributeName("foo").WithAttributeName("bar").WithAttributeName("baz"), + exp: "foo.bar.baz", + }, + "with-element-key-string": { + in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyString("bar").WithAttributeName("baz"), + exp: `foo["bar"].baz`, + }, + "with-element-key-int": { + in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyInt(10).WithAttributeName("baz"), + exp: `foo[10].baz`, + }, + "with-element-key-value": { + in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyInt(10).WithElementKeyValue(tftypes.NewValue(tftypes.Object{}, nil)), + exp: `foo[10][tftypes.Object[]]`, + }, + "with-element-key-string-and-int": { + in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyString("bar").WithElementKeyInt(10), + exp: `foo["bar"][10]`, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + actual := attributepath.ToString(test.in) + if test.exp != actual { + t.Fatalf("expected %q, got %q", test.exp, actual) + } + }) + } +} + +func TestJoinToString(t *testing.T) { + t.Parallel() + + type testCase struct { + in []*tftypes.AttributePath + exp string + } + + testCases := map[string]testCase{ + "only-attribute-names": { + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo").WithAttributeName("bar").WithAttributeName("baz"), + tftypes.NewAttributePath().WithAttributeName("bob").WithAttributeName("alice"), + }, + exp: `foo.bar.baz,bob.alice`, + }, + "with-element-key-string": { + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("bob").WithElementKeyString("alice"), + tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyString("bar").WithAttributeName("baz"), + }, + exp: `bob["alice"],foo["bar"].baz`, + }, + "with-element-key-int": { + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("bar").WithElementKeyInt(10), + tftypes.NewAttributePath().WithAttributeName("baz").WithElementKeyInt(100), + }, + exp: `foo,bar[10],baz[100]`, + }, + "with-element-key-value": { + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyValue(tftypes.NewValue(tftypes.Object{}, nil)), + tftypes.NewAttributePath().WithAttributeName("bob").WithElementKeyString("alice"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + exp: `foo[tftypes.Object[]],bob["alice"],baz`, + }, + "with-element-key-string-and-int": { + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("bob").WithAttributeName("alice"), + tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyInt(10), + tftypes.NewAttributePath().WithAttributeName("bar").WithElementKeyString("baz"), + }, + exp: `bob.alice,foo[10],bar["baz"]`, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + actual := attributepath.JoinToString(test.in...) + if test.exp != actual { + t.Fatalf("expected %q, got %q", test.exp, actual) + } + }) + } +} From 4a4a7a8a0808baf7aa61ab32d3e3c6782f2c825e Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 23 Jun 2022 16:32:00 +0100 Subject: [PATCH 03/20] Introducing `schemavalidator` package --- schemavalidator/at_least_one_of.go | 64 ++++++ schemavalidator/at_least_one_of_test.go | 251 +++++++++++++++++++++++ schemavalidator/conflicts_with.go | 67 +++++++ schemavalidator/conflicts_with_test.go | 248 +++++++++++++++++++++++ schemavalidator/doc.go | 5 + schemavalidator/exactly_one_of.go | 75 +++++++ schemavalidator/exactly_one_of_test.go | 254 ++++++++++++++++++++++++ schemavalidator/required_with.go | 67 +++++++ schemavalidator/required_with_test.go | 252 +++++++++++++++++++++++ 9 files changed, 1283 insertions(+) create mode 100644 schemavalidator/at_least_one_of.go create mode 100644 schemavalidator/at_least_one_of_test.go create mode 100644 schemavalidator/conflicts_with.go create mode 100644 schemavalidator/conflicts_with_test.go create mode 100644 schemavalidator/doc.go create mode 100644 schemavalidator/exactly_one_of.go create mode 100644 schemavalidator/exactly_one_of_test.go create mode 100644 schemavalidator/required_with.go create mode 100644 schemavalidator/required_with_test.go diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go new file mode 100644 index 00000000..16bc7540 --- /dev/null +++ b/schemavalidator/at_least_one_of.go @@ -0,0 +1,64 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// atLeastOneOfAttributeValidator is the underlying struct implementing AtLeastOneOf. +type atLeastOneOfAttributeValidator struct { + attrPaths []*tftypes.AttributePath +} + +// AtLeastOneOf checks that of a set of *tftypes.AttributePath, +// including the attribute it's applied to, at least one attribute out of all specified is configured. +// +// The provided tftypes.AttributePath must be "absolute", +// and starting with top level attribute names. +func AtLeastOneOf(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { + return &atLeastOneOfAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*atLeastOneOfAttributeValidator)(nil) + +func (av atLeastOneOfAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av atLeastOneOfAttributeValidator) MarkdownDescription(ctx context.Context) string { + return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %q", av.attrPaths) +} + +func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // Assemble a slice of paths, ensuring we don't repeat the attribute this validator is applied to + var paths []*tftypes.AttributePath + if attributepath.Contains(req.AttributePath, av.attrPaths...) { + paths = av.attrPaths + } else { + paths = append(av.attrPaths, req.AttributePath) + } + + for _, path := range paths { + var v attr.Value + diags := req.Config.GetAttribute(ctx, path, &v) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + if !v.IsNull() { + return + } + } + + res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + req.AttributePath, + fmt.Sprintf("At least one attribute out of %q must be specified", attributepath.JoinToString(paths...)), + )) +} diff --git a/schemavalidator/at_least_one_of_test.go b/schemavalidator/at_least_one_of_test.go new file mode 100644 index 00000000..f705fe4c --- /dev/null +++ b/schemavalidator/at_least_one_of_test.go @@ -0,0 +1,251 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtLeastOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in []*tftypes.AttributePath + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "error_none-set": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, nil), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 1, + }, + "multiple-set": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Float64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 4.2), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + }, + "allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("bar"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + }, + "unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.AtLeastOneOf(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + + if test.expErrors == 0 && res.Diagnostics.HasError() { + t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + }) + } +} diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go new file mode 100644 index 00000000..3225d203 --- /dev/null +++ b/schemavalidator/conflicts_with.go @@ -0,0 +1,67 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// conflictsWithAttributeValidator is the underlying struct implementing ConflictsWith. +type conflictsWithAttributeValidator struct { + attrPaths []*tftypes.AttributePath +} + +// ConflictsWith checks that a set of *tftypes.AttributePath, +// including the attribute it's applied to, are not set simultaneously. +// This implements the validation logic declaratively within the tfsdk.Schema. +// +// The provided tftypes.AttributePath must be "absolute", +// and starting with top level attribute names. +func ConflictsWith(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { + return &conflictsWithAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*conflictsWithAttributeValidator)(nil) + +func (av conflictsWithAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that if an attribute is set, these are not set: %q", av.attrPaths) +} + +func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + var v attr.Value + res.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &v)...) + if res.Diagnostics.HasError() { + return + } + + for _, path := range av.attrPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it. + if req.AttributePath.Equal(path) { + continue + } + + var o attr.Value + diags := req.Config.GetAttribute(ctx, path, &o) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + if !v.IsNull() && !o.IsNull() { + res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + req.AttributePath, + fmt.Sprintf("Attribute %q cannot be specified when %q is specified", attributepath.ToString(path), attributepath.ToString(req.AttributePath)), + )) + } + } +} diff --git a/schemavalidator/conflicts_with_test.go b/schemavalidator/conflicts_with_test.go new file mode 100644 index 00000000..0cc22487 --- /dev/null +++ b/schemavalidator/conflicts_with_test.go @@ -0,0 +1,248 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestConflictsWithValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in []*tftypes.AttributePath + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 2, + }, + "conflicting-is-nil": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "error_conflicting-is-unknown": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + expErrors: 1, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "error_allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("bar"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 2, + }, + "error_unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 2, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.ConflictsWith(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + + if test.expErrors == 0 && res.Diagnostics.HasError() { + t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + }) + } +} diff --git a/schemavalidator/doc.go b/schemavalidator/doc.go new file mode 100644 index 00000000..bacb65c8 --- /dev/null +++ b/schemavalidator/doc.go @@ -0,0 +1,5 @@ +// Package schemavalidator provides validators that allow to express +// relationships between multiple attributes within the schema of a resource, +// data source or provider. +// For example, checking that an attribute is present when another is present, or vice-versa. +package schemavalidator diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go new file mode 100644 index 00000000..2775466a --- /dev/null +++ b/schemavalidator/exactly_one_of.go @@ -0,0 +1,75 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// exactlyOneOfAttributeValidator is the underlying struct implementing ExactlyOneOf. +type exactlyOneOfAttributeValidator struct { + attrPaths []*tftypes.AttributePath +} + +// ExactlyOneOf checks that of a set of *tftypes.AttributePath, +// including the attribute it's applied to, one and only one attribute out of all specified is configured. +// It will also cause a validation error if none are specified. +// +// The provided tftypes.AttributePath must be "absolute", +// and starting with top level attribute names. +func ExactlyOneOf(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { + return &exactlyOneOfAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*exactlyOneOfAttributeValidator)(nil) + +func (av exactlyOneOfAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av exactlyOneOfAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that one and only one attribute from this collection is set: %q", av.attrPaths) +} + +func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // Assemble a slice of paths, ensuring we don't repeat the attribute this validator is applied to + var paths []*tftypes.AttributePath + if attributepath.Contains(req.AttributePath, av.attrPaths...) { + paths = av.attrPaths + } else { + paths = append(av.attrPaths, req.AttributePath) + } + + count := 0 + for _, path := range paths { + var v attr.Value + diags := req.Config.GetAttribute(ctx, path, &v) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + if !v.IsNull() { + count++ + } + } + + if count == 0 { + res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + req.AttributePath, + fmt.Sprintf("No attribute specified when one (and only one) of %q is required", attributepath.JoinToString(paths...)), + )) + } + + if count > 1 { + res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + req.AttributePath, + fmt.Sprintf("%d attributes specified when one (and only one) of %q is required", count, attributepath.JoinToString(paths...)), + )) + } +} diff --git a/schemavalidator/exactly_one_of_test.go b/schemavalidator/exactly_one_of_test.go new file mode 100644 index 00000000..1763b0e6 --- /dev/null +++ b/schemavalidator/exactly_one_of_test.go @@ -0,0 +1,254 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestExactlyOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in []*tftypes.AttributePath + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + expErrors: 1, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "error_too-many": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Float64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 4.2), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 1, + }, + "error_too-few": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, nil), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 1, + }, + "allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("bar"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + }, + "error_unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 1, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.ExactlyOneOf(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + + if test.expErrors == 0 && res.Diagnostics.HasError() { + t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + }) + } +} diff --git a/schemavalidator/required_with.go b/schemavalidator/required_with.go new file mode 100644 index 00000000..6645cd9c --- /dev/null +++ b/schemavalidator/required_with.go @@ -0,0 +1,67 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// requiredWithAttributeValidator is the underlying struct implementing RequiredWith. +type requiredWithAttributeValidator struct { + attrPaths []*tftypes.AttributePath +} + +// RequiredWith checks that a set of *tftypes.AttributePath, +// including the attribute it's applied to, are set simultaneously. +// This implements the validation logic declaratively within the tfsdk.Schema. +// +// The provided tftypes.AttributePath must be "absolute", +// and starting with top level attribute names. +func RequiredWith(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { + return &requiredWithAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*requiredWithAttributeValidator)(nil) + +func (av requiredWithAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av requiredWithAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that if an attribute is set, also these are set: %q", av.attrPaths) +} + +func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + var v attr.Value + res.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &v)...) + if res.Diagnostics.HasError() { + return + } + + for _, path := range av.attrPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it. + if req.AttributePath.Equal(path) { + continue + } + + var o attr.Value + diags := req.Config.GetAttribute(ctx, path, &o) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + if !v.IsNull() && o.IsNull() { + res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + req.AttributePath, + fmt.Sprintf("Attribute %q must be specified when %q is specified", attributepath.ToString(path), attributepath.ToString(req.AttributePath)), + )) + } + } +} diff --git a/schemavalidator/required_with_test.go b/schemavalidator/required_with_test.go new file mode 100644 index 00000000..c00726ce --- /dev/null +++ b/schemavalidator/required_with_test.go @@ -0,0 +1,252 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiredWithValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in []*tftypes.AttributePath + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + "error_missing-one": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 1, + }, + "error_missing-two": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + expErrors: 2, + }, + "allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("bar"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + }, + "unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + tftypes.NewAttributePath().WithAttributeName("baz"), + }, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.RequiredWith(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + + if test.expErrors == 0 && res.Diagnostics.HasError() { + t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + } + }) + } +} From 4a7c9abf9aa84384bc2dbeb741fcdbcb847499fc Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 23 Jun 2022 16:33:25 +0100 Subject: [PATCH 04/20] CHANGELOG entry --- .changelog/32.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/32.txt diff --git a/.changelog/32.txt b/.changelog/32.txt new file mode 100644 index 00000000..0975ad38 --- /dev/null +++ b/.changelog/32.txt @@ -0,0 +1,7 @@ +```release-note:breaking-change +Renaming package `validatordiag` to be at `helpers/validatordiag` +``` + +```release-note:feature +Introduced `schemavalidator` package with 4 new validation functions: `RequiredWith()`, `ConflictsWith()`, `AtLeastOneOf()`, `ExactlyOneOf()` +``` From 190e1e79f7c5d997aa97c529cbbb137c1c385c7d Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 6 Jul 2022 14:31:22 +0100 Subject: [PATCH 05/20] Removing any reference to `tftypes.AttributePath` in favour of `path.*` --- float64validator/at_least_test.go | 5 +- float64validator/at_most_test.go | 5 +- float64validator/between_test.go | 5 +- float64validator/none_of_test.go | 7 +- float64validator/one_of_test.go | 7 +- float64validator/type_validation_test.go | 20 ++-- helpers/attributepath/contains.go | 16 --- helpers/attributepath/doc.go | 2 - helpers/attributepath/to_string.go | 46 --------- helpers/attributepath/to_string_test.go | 108 -------------------- int64validator/at_least_test.go | 5 +- int64validator/at_most_test.go | 5 +- int64validator/between_test.go | 5 +- int64validator/none_of_test.go | 7 +- int64validator/one_of_test.go | 7 +- int64validator/type_validation_test.go | 20 ++-- internal/primitivevalidator/none_of_test.go | 7 +- internal/primitivevalidator/one_of_test.go | 7 +- listvalidator/size_at_least_test.go | 5 +- listvalidator/size_at_most_test.go | 5 +- listvalidator/size_between_test.go | 5 +- listvalidator/type_validation_test.go | 18 ++-- listvalidator/values_are.go | 8 +- listvalidator/values_are_test.go | 5 +- mapvalidator/keys_are.go | 8 +- mapvalidator/keys_are_test.go | 5 +- mapvalidator/size_at_least_test.go | 5 +- mapvalidator/size_at_most_test.go | 5 +- mapvalidator/size_between_test.go | 5 +- mapvalidator/type_validation_test.go | 18 ++-- mapvalidator/values_are.go | 8 +- mapvalidator/values_are_test.go | 5 +- metavalidator/all_test.go | 5 +- metavalidator/any_test.go | 5 +- metavalidator/any_with_all_warnings_test.go | 5 +- numbervalidator/none_of_test.go | 7 +- numbervalidator/one_of_test.go | 7 +- setvalidator/size_at_least_test.go | 5 +- setvalidator/size_at_most_test.go | 5 +- setvalidator/size_between_test.go | 5 +- setvalidator/type_validation_test.go | 18 ++-- setvalidator/values_are.go | 8 +- setvalidator/values_are_test.go | 5 +- stringvalidator/length_at_least_test.go | 5 +- stringvalidator/length_at_most_test.go | 5 +- stringvalidator/length_between_test.go | 5 +- stringvalidator/none_of_test.go | 13 ++- stringvalidator/one_of_test.go | 13 ++- stringvalidator/regex_matches_test.go | 5 +- stringvalidator/type_validation_test.go | 20 ++-- 50 files changed, 203 insertions(+), 327 deletions(-) delete mode 100644 helpers/attributepath/contains.go delete mode 100644 helpers/attributepath/doc.go delete mode 100644 helpers/attributepath/to_string.go delete mode 100644 helpers/attributepath/to_string_test.go diff --git a/float64validator/at_least_test.go b/float64validator/at_least_test.go index 1773b659..ab8c8e50 100644 --- a/float64validator/at_least_test.go +++ b/float64validator/at_least_test.go @@ -55,8 +55,9 @@ func TestAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} float64validator.AtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/float64validator/at_most_test.go b/float64validator/at_most_test.go index e5182b50..2443c3b8 100644 --- a/float64validator/at_most_test.go +++ b/float64validator/at_most_test.go @@ -55,8 +55,9 @@ func TestAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} float64validator.AtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/float64validator/between_test.go b/float64validator/between_test.go index 4add5502..74c2e608 100644 --- a/float64validator/between_test.go +++ b/float64validator/between_test.go @@ -73,8 +73,9 @@ func TestBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} float64validator.Between(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/float64validator/none_of_test.go b/float64validator/none_of_test.go index 102f779b..ceccda2c 100644 --- a/float64validator/none_of_test.go +++ b/float64validator/none_of_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -73,12 +72,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/float64validator/one_of_test.go b/float64validator/one_of_test.go index afe79b3b..dc1b55a9 100644 --- a/float64validator/one_of_test.go +++ b/float64validator/one_of_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -73,12 +72,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/float64validator/type_validation_test.go b/float64validator/type_validation_test.go index 7f8f2eaa..614a3585 100644 --- a/float64validator/type_validation_test.go +++ b/float64validator/type_validation_test.go @@ -20,32 +20,36 @@ func TestValidateFloat(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 0.0, expectedOk: false, }, "float64-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Float64{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Float64{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 0.0, expectedOk: false, }, "float64-value": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Float64{Value: 1.2}, - AttributePath: path.Root("test"), + AttributeConfig: types.Float64{Value: 1.2}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 1.2, expectedOk: true, }, "float64-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Float64{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Float64{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 0.0, expectedOk: false, diff --git a/helpers/attributepath/contains.go b/helpers/attributepath/contains.go deleted file mode 100644 index 6d27d61f..00000000 --- a/helpers/attributepath/contains.go +++ /dev/null @@ -1,16 +0,0 @@ -package attributepath - -import ( - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -// Contains returns true if needle (one *tftypes.AttributePath) -// can be found in haystack (collection of *tftypes.AttributePath). -func Contains(needle *tftypes.AttributePath, haystack ...*tftypes.AttributePath) bool { - for _, p := range haystack { - if needle.Equal(p) { - return true - } - } - return false -} diff --git a/helpers/attributepath/doc.go b/helpers/attributepath/doc.go deleted file mode 100644 index c6d64c3f..00000000 --- a/helpers/attributepath/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package attributepath provides helpers to interact with tftypes.AttributePath. -package attributepath diff --git a/helpers/attributepath/to_string.go b/helpers/attributepath/to_string.go deleted file mode 100644 index 5c562390..00000000 --- a/helpers/attributepath/to_string.go +++ /dev/null @@ -1,46 +0,0 @@ -package attributepath - -import ( - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -// ToString takes all the tftypes.AttributePathStep in a tftypes.AttributePath and concatenates them, -// using `.` as separator. -// -// This should be used only when trying to "print out" a tftypes.AttributePath in a log or an error message. -func ToString(path *tftypes.AttributePath) string { - var res strings.Builder - for pos, step := range path.Steps() { - switch v := step.(type) { - case tftypes.AttributeName: - if pos != 0 { - res.WriteString(".") - } - res.WriteString(string(v)) - case tftypes.ElementKeyString: - res.WriteString("[\"" + string(v) + "\"]") - case tftypes.ElementKeyInt: - res.WriteString("[" + strconv.FormatInt(int64(v), 10) + "]") - case tftypes.ElementKeyValue: - res.WriteString("[" + tftypes.Value(v).String() + "]") - } - } - - return res.String() -} - -// JoinToString works similarly to strings.Join: it takes a collection of *tftypes.AttributePath, -// applies to each ToString, and the resulting strings with a `,` separator. -// -// This should be used only when trying to "print out" a tftypes.AttributePath in a log or an error message. -func JoinToString(paths ...*tftypes.AttributePath) string { - res := make([]string, len(paths)) - for i, path := range paths { - res[i] = ToString(path) - } - - return strings.Join(res, ",") -} diff --git a/helpers/attributepath/to_string_test.go b/helpers/attributepath/to_string_test.go deleted file mode 100644 index b04be2a1..00000000 --- a/helpers/attributepath/to_string_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package attributepath_test - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -func TestToString(t *testing.T) { - t.Parallel() - - type testCase struct { - in *tftypes.AttributePath - exp string - } - - testCases := map[string]testCase{ - "only-attribute-names": { - in: tftypes.NewAttributePath().WithAttributeName("foo").WithAttributeName("bar").WithAttributeName("baz"), - exp: "foo.bar.baz", - }, - "with-element-key-string": { - in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyString("bar").WithAttributeName("baz"), - exp: `foo["bar"].baz`, - }, - "with-element-key-int": { - in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyInt(10).WithAttributeName("baz"), - exp: `foo[10].baz`, - }, - "with-element-key-value": { - in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyInt(10).WithElementKeyValue(tftypes.NewValue(tftypes.Object{}, nil)), - exp: `foo[10][tftypes.Object[]]`, - }, - "with-element-key-string-and-int": { - in: tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyString("bar").WithElementKeyInt(10), - exp: `foo["bar"][10]`, - }, - } - - for name, test := range testCases { - t.Run(name, func(t *testing.T) { - actual := attributepath.ToString(test.in) - if test.exp != actual { - t.Fatalf("expected %q, got %q", test.exp, actual) - } - }) - } -} - -func TestJoinToString(t *testing.T) { - t.Parallel() - - type testCase struct { - in []*tftypes.AttributePath - exp string - } - - testCases := map[string]testCase{ - "only-attribute-names": { - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo").WithAttributeName("bar").WithAttributeName("baz"), - tftypes.NewAttributePath().WithAttributeName("bob").WithAttributeName("alice"), - }, - exp: `foo.bar.baz,bob.alice`, - }, - "with-element-key-string": { - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("bob").WithElementKeyString("alice"), - tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyString("bar").WithAttributeName("baz"), - }, - exp: `bob["alice"],foo["bar"].baz`, - }, - "with-element-key-int": { - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("bar").WithElementKeyInt(10), - tftypes.NewAttributePath().WithAttributeName("baz").WithElementKeyInt(100), - }, - exp: `foo,bar[10],baz[100]`, - }, - "with-element-key-value": { - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyValue(tftypes.NewValue(tftypes.Object{}, nil)), - tftypes.NewAttributePath().WithAttributeName("bob").WithElementKeyString("alice"), - tftypes.NewAttributePath().WithAttributeName("baz"), - }, - exp: `foo[tftypes.Object[]],bob["alice"],baz`, - }, - "with-element-key-string-and-int": { - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("bob").WithAttributeName("alice"), - tftypes.NewAttributePath().WithAttributeName("foo").WithElementKeyInt(10), - tftypes.NewAttributePath().WithAttributeName("bar").WithElementKeyString("baz"), - }, - exp: `bob.alice,foo[10],bar["baz"]`, - }, - } - - for name, test := range testCases { - t.Run(name, func(t *testing.T) { - actual := attributepath.JoinToString(test.in...) - if test.exp != actual { - t.Fatalf("expected %q, got %q", test.exp, actual) - } - }) - } -} diff --git a/int64validator/at_least_test.go b/int64validator/at_least_test.go index 63a808d9..b5294bdb 100644 --- a/int64validator/at_least_test.go +++ b/int64validator/at_least_test.go @@ -51,8 +51,9 @@ func TestAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} int64validator.AtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/int64validator/at_most_test.go b/int64validator/at_most_test.go index c0b8ca33..72a2801c 100644 --- a/int64validator/at_most_test.go +++ b/int64validator/at_most_test.go @@ -51,8 +51,9 @@ func TestAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} int64validator.AtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/int64validator/between_test.go b/int64validator/between_test.go index 659820a5..1d454b85 100644 --- a/int64validator/between_test.go +++ b/int64validator/between_test.go @@ -68,8 +68,9 @@ func TestBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} int64validator.Between(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/int64validator/none_of_test.go b/int64validator/none_of_test.go index 3d7dd0ee..fea7ddcb 100644 --- a/int64validator/none_of_test.go +++ b/int64validator/none_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -73,12 +72,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/int64validator/one_of_test.go b/int64validator/one_of_test.go index 95517cb1..de2607cf 100644 --- a/int64validator/one_of_test.go +++ b/int64validator/one_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -73,12 +72,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/int64validator/type_validation_test.go b/int64validator/type_validation_test.go index 7c6dc11e..596b51e7 100644 --- a/int64validator/type_validation_test.go +++ b/int64validator/type_validation_test.go @@ -20,32 +20,36 @@ func TestValidateInt(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 0.0, expectedOk: false, }, "int64-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 0.0, expectedOk: false, }, "int64-value": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Value: 123}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Value: 123}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 123, expectedOk: true, }, "int64-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 0.0, expectedOk: false, diff --git a/internal/primitivevalidator/none_of_test.go b/internal/primitivevalidator/none_of_test.go index c0329660..91cebbbd 100644 --- a/internal/primitivevalidator/none_of_test.go +++ b/internal/primitivevalidator/none_of_test.go @@ -6,7 +6,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/internal/primitivevalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -181,12 +180,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/internal/primitivevalidator/one_of_test.go b/internal/primitivevalidator/one_of_test.go index d132aff0..f55a1571 100644 --- a/internal/primitivevalidator/one_of_test.go +++ b/internal/primitivevalidator/one_of_test.go @@ -6,7 +6,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/internal/primitivevalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -181,12 +180,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/listvalidator/size_at_least_test.go b/listvalidator/size_at_least_test.go index a03b283f..024f4007 100644 --- a/listvalidator/size_at_least_test.go +++ b/listvalidator/size_at_least_test.go @@ -72,8 +72,9 @@ func TestSizeAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/listvalidator/size_at_most_test.go b/listvalidator/size_at_most_test.go index 82177922..af4d121a 100644 --- a/listvalidator/size_at_most_test.go +++ b/listvalidator/size_at_most_test.go @@ -75,8 +75,9 @@ func TestSizeAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/listvalidator/size_between_test.go b/listvalidator/size_between_test.go index 76a566ba..7276e66d 100644 --- a/listvalidator/size_between_test.go +++ b/listvalidator/size_between_test.go @@ -115,8 +115,9 @@ func TestSizeBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/listvalidator/type_validation_test.go b/listvalidator/type_validation_test.go index 29b05705..c2538a8c 100644 --- a/listvalidator/type_validation_test.go +++ b/listvalidator/type_validation_test.go @@ -21,24 +21,27 @@ func TestValidateList(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: nil, expectedOk: false, }, "list-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.List{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.List{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: nil, expectedOk: false, }, "list-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.List{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.List{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: nil, expectedOk: false, @@ -52,7 +55,8 @@ func TestValidateList(t *testing.T) { types.String{Value: "second"}, }, }, - AttributePath: path.Root("test"), + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: []attr.Value{ types.String{Value: "first"}, diff --git a/listvalidator/values_are.go b/listvalidator/values_are.go index e72c14d5..ac6cb84f 100644 --- a/listvalidator/values_are.go +++ b/listvalidator/values_are.go @@ -38,10 +38,12 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr } for k, elem := range elems { + attrPath := req.AttributePath.AtListIndex(k) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(k), - AttributeConfig: elem, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: elem, + Config: req.Config, } for _, validator := range v.valueValidators { diff --git a/listvalidator/values_are_test.go b/listvalidator/values_are_test.go index 1d12c6a1..6b9b1a50 100644 --- a/listvalidator/values_are_test.go +++ b/listvalidator/values_are_test.go @@ -101,8 +101,9 @@ func TestValuesAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/keys_are.go b/mapvalidator/keys_are.go index cf4ccf29..5bdf7bfa 100644 --- a/mapvalidator/keys_are.go +++ b/mapvalidator/keys_are.go @@ -42,10 +42,12 @@ func (v keysAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttrib } for k := range elems { + attrPath := req.AttributePath.AtMapKey(k) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtMapKey(k), - AttributeConfig: types.String{Value: k}, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: types.String{Value: k}, + Config: req.Config, } for _, validator := range v.keyValidators { diff --git a/mapvalidator/keys_are_test.go b/mapvalidator/keys_are_test.go index 61e5eac6..4004b5d5 100644 --- a/mapvalidator/keys_are_test.go +++ b/mapvalidator/keys_are_test.go @@ -101,8 +101,9 @@ func TestKeysAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} KeysAre(test.keysAreValidators...).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/size_at_least_test.go b/mapvalidator/size_at_least_test.go index 1d532042..26cd0b52 100644 --- a/mapvalidator/size_at_least_test.go +++ b/mapvalidator/size_at_least_test.go @@ -72,8 +72,9 @@ func TestSizeAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/size_at_most_test.go b/mapvalidator/size_at_most_test.go index 881326ad..fe2c327e 100644 --- a/mapvalidator/size_at_most_test.go +++ b/mapvalidator/size_at_most_test.go @@ -75,8 +75,9 @@ func TestSizeAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/size_between_test.go b/mapvalidator/size_between_test.go index 27705210..d701ac76 100644 --- a/mapvalidator/size_between_test.go +++ b/mapvalidator/size_between_test.go @@ -115,8 +115,9 @@ func TestSizeBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/type_validation_test.go b/mapvalidator/type_validation_test.go index 0c3fc084..0c4cf8ae 100644 --- a/mapvalidator/type_validation_test.go +++ b/mapvalidator/type_validation_test.go @@ -21,24 +21,27 @@ func TestValidateMap(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: nil, expectedOk: false, }, "map-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Map{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Map{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: nil, expectedOk: false, }, "map-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Map{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Map{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: nil, expectedOk: false, @@ -52,7 +55,8 @@ func TestValidateMap(t *testing.T) { "two": types.String{Value: "second"}, }, }, - AttributePath: path.Root("test"), + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: map[string]attr.Value{ "one": types.String{Value: "first"}, diff --git a/mapvalidator/values_are.go b/mapvalidator/values_are.go index 8a8ba0b9..cef36290 100644 --- a/mapvalidator/values_are.go +++ b/mapvalidator/values_are.go @@ -38,10 +38,12 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr } for k, elem := range elems { + attrPath := req.AttributePath.AtMapKey(k) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtMapKey(k), - AttributeConfig: elem, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: elem, + Config: req.Config, } for _, validator := range v.valueValidators { diff --git a/mapvalidator/values_are_test.go b/mapvalidator/values_are_test.go index 91680b39..d4eda8a8 100644 --- a/mapvalidator/values_are_test.go +++ b/mapvalidator/values_are_test.go @@ -95,8 +95,9 @@ func TestValuesAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) diff --git a/metavalidator/all_test.go b/metavalidator/all_test.go index 8a0836a9..fa0bef57 100644 --- a/metavalidator/all_test.go +++ b/metavalidator/all_test.go @@ -70,8 +70,9 @@ func TestAllValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} metavalidator.All(test.valueValidators...).Validate(context.TODO(), request, &response) diff --git a/metavalidator/any_test.go b/metavalidator/any_test.go index f4f9a6b4..29dc48e3 100644 --- a/metavalidator/any_test.go +++ b/metavalidator/any_test.go @@ -142,8 +142,9 @@ func TestAnyValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} metavalidator.Any(test.valueValidators...).Validate(context.TODO(), request, &response) diff --git a/metavalidator/any_with_all_warnings_test.go b/metavalidator/any_with_all_warnings_test.go index 20798728..1707a89b 100644 --- a/metavalidator/any_with_all_warnings_test.go +++ b/metavalidator/any_with_all_warnings_test.go @@ -164,8 +164,9 @@ func TestAnyWithAllWarningsValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} metavalidator.AnyWithAllWarnings(test.valueValidators...).Validate(context.TODO(), request, &response) diff --git a/numbervalidator/none_of_test.go b/numbervalidator/none_of_test.go index b9eb386f..53623a90 100644 --- a/numbervalidator/none_of_test.go +++ b/numbervalidator/none_of_test.go @@ -5,7 +5,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -74,12 +73,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/numbervalidator/one_of_test.go b/numbervalidator/one_of_test.go index f90da2f4..e9f76b17 100644 --- a/numbervalidator/one_of_test.go +++ b/numbervalidator/one_of_test.go @@ -5,7 +5,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -74,12 +73,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/setvalidator/size_at_least_test.go b/setvalidator/size_at_least_test.go index 7f45c774..aa5b9bc6 100644 --- a/setvalidator/size_at_least_test.go +++ b/setvalidator/size_at_least_test.go @@ -72,8 +72,9 @@ func TestSizeAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/setvalidator/size_at_most_test.go b/setvalidator/size_at_most_test.go index a44ee28f..26561f64 100644 --- a/setvalidator/size_at_most_test.go +++ b/setvalidator/size_at_most_test.go @@ -75,8 +75,9 @@ func TestSizeAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/setvalidator/size_between_test.go b/setvalidator/size_between_test.go index 5f3dd38d..273130e2 100644 --- a/setvalidator/size_between_test.go +++ b/setvalidator/size_between_test.go @@ -115,8 +115,9 @@ func TestSizeBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/setvalidator/type_validation_test.go b/setvalidator/type_validation_test.go index f198c738..db8f32be 100644 --- a/setvalidator/type_validation_test.go +++ b/setvalidator/type_validation_test.go @@ -21,24 +21,27 @@ func TestValidateSet(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: nil, expectedOk: false, }, "set-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Set{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Set{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: nil, expectedOk: false, }, "set-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Set{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Set{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: nil, expectedOk: false, @@ -52,7 +55,8 @@ func TestValidateSet(t *testing.T) { types.String{Value: "second"}, }, }, - AttributePath: path.Root("test"), + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: []attr.Value{ types.String{Value: "first"}, diff --git a/setvalidator/values_are.go b/setvalidator/values_are.go index ac769cb3..a9c971b2 100644 --- a/setvalidator/values_are.go +++ b/setvalidator/values_are.go @@ -38,10 +38,12 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr } for _, elem := range elems { + attrPath := req.AttributePath.AtSetValue(elem) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(elem), - AttributeConfig: elem, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: elem, + Config: req.Config, } for _, validator := range v.valueValidators { diff --git a/setvalidator/values_are_test.go b/setvalidator/values_are_test.go index a3d00c81..e5caa8e7 100644 --- a/setvalidator/values_are_test.go +++ b/setvalidator/values_are_test.go @@ -101,8 +101,9 @@ func TestValuesAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/length_at_least_test.go b/stringvalidator/length_at_least_test.go index 3e0fb649..0a3bbbc6 100644 --- a/stringvalidator/length_at_least_test.go +++ b/stringvalidator/length_at_least_test.go @@ -47,8 +47,9 @@ func TestLengthAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.LengthAtLeast(test.minLength).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/length_at_most_test.go b/stringvalidator/length_at_most_test.go index 18666a85..39d315d0 100644 --- a/stringvalidator/length_at_most_test.go +++ b/stringvalidator/length_at_most_test.go @@ -47,8 +47,9 @@ func TestLengthAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.LengthAtMost(test.maxLength).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/length_between_test.go b/stringvalidator/length_between_test.go index 6301974d..ce8cf560 100644 --- a/stringvalidator/length_between_test.go +++ b/stringvalidator/length_between_test.go @@ -58,8 +58,9 @@ func TestLengthBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.LengthBetween(test.minLength, test.maxLength).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/none_of_test.go b/stringvalidator/none_of_test.go index 78446222..e4361e58 100644 --- a/stringvalidator/none_of_test.go +++ b/stringvalidator/none_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -159,12 +158,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } @@ -318,12 +317,12 @@ func TestNoneOfCaseInsensitiveValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/stringvalidator/one_of_test.go b/stringvalidator/one_of_test.go index a536d3d8..568dd120 100644 --- a/stringvalidator/one_of_test.go +++ b/stringvalidator/one_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -159,12 +158,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } @@ -318,12 +317,12 @@ func TestOneOfCaseInsensitiveValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/stringvalidator/regex_matches_test.go b/stringvalidator/regex_matches_test.go index 92239b96..76f04306 100644 --- a/stringvalidator/regex_matches_test.go +++ b/stringvalidator/regex_matches_test.go @@ -49,8 +49,9 @@ func TestRegexMatchesValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.RegexMatches(test.regexp, "").Validate(context.TODO(), request, &response) diff --git a/stringvalidator/type_validation_test.go b/stringvalidator/type_validation_test.go index 003cb628..b02ed6f5 100644 --- a/stringvalidator/type_validation_test.go +++ b/stringvalidator/type_validation_test.go @@ -20,32 +20,36 @@ func TestValidateString(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "", expectedOk: false, }, "string-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "", expectedOk: false, }, "string-value": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "test-value"}, - AttributePath: path.Root("test"), + AttributeConfig: types.String{Value: "test-value"}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "test-value", expectedOk: true, }, "string-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "", expectedOk: false, From 06801b81517b26ecadf64f4c13ae9087b6c7b837 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 6 Jul 2022 14:31:40 +0100 Subject: [PATCH 06/20] Bump up to latest version of `terraform-plugin-framework` --- go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e2c33057..bf02fd42 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.17 require ( github.com/google/go-cmp v0.5.8 - github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906 + github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220629230053-34bd9b67bff9 + github.com/hashicorp/terraform-plugin-go v0.10.0 ) require ( From 398db10c2ff2057446393f2b0d1b1b3b535ca986 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 6 Jul 2022 14:32:02 +0100 Subject: [PATCH 07/20] Fix changelog entries --- .changelog/32.txt | 4 ---- .changelog/42.txt | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changelog/32.txt b/.changelog/32.txt index 0975ad38..84062a84 100644 --- a/.changelog/32.txt +++ b/.changelog/32.txt @@ -1,7 +1,3 @@ -```release-note:breaking-change -Renaming package `validatordiag` to be at `helpers/validatordiag` -``` - ```release-note:feature Introduced `schemavalidator` package with 4 new validation functions: `RequiredWith()`, `ConflictsWith()`, `AtLeastOneOf()`, `ExactlyOneOf()` ``` diff --git a/.changelog/42.txt b/.changelog/42.txt index ee2fa9f8..2898e242 100644 --- a/.changelog/42.txt +++ b/.changelog/42.txt @@ -9,3 +9,7 @@ int64validator: Added `OneOf()` and `NoneOf()` validation functions ```release-note:feature Introduced `numbervalidator` package with `OneOf()` and `NoneOf()` validation functions ``` + +```release-note:breaking-change +Renaming package `validatordiag` to be at `helpers/validatordiag` +``` From d0f45979be07b8714d16fa59e367d9ea68c8917e Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 6 Jul 2022 14:32:36 +0100 Subject: [PATCH 08/20] Update helpers with a `pathutils` sub-package to help with the new `path.Expressions` manipulation --- helpers/pathutils/merge.go | 30 ++++++++++++++++++++++ helpers/pathutils/resolve.go | 47 +++++++++++++++++++++++++++++++++++ helpers/validatordiag/diag.go | 32 +++++------------------- 3 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 helpers/pathutils/merge.go create mode 100644 helpers/pathutils/resolve.go diff --git a/helpers/pathutils/merge.go b/helpers/pathutils/merge.go new file mode 100644 index 00000000..f97333a3 --- /dev/null +++ b/helpers/pathutils/merge.go @@ -0,0 +1,30 @@ +package pathutils + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// MergeExpressionsWithAttribute returns the given path.Expressions, +// but each has been merged with the given attribute path.Expression, +// and then resolved. +// +// Additionally, if the attribute path.Expression was not part of the initial slice, +// it is added to the result. +func MergeExpressionsWithAttribute(pathExps path.Expressions, attrPathExp path.Expression) path.Expressions { + result := make(path.Expressions, 0, len(pathExps)+1) + + // First, add the attribute own path expression to the result + result = append(result, attrPathExp) + + for _, pe := range pathExps { + mpe := attrPathExp.Merge(pe).Resolve() + + // Include the merged path expression, + // only if it's not the same as the attribute + if !mpe.Equal(attrPathExp) { + result = append(result, mpe) + } + } + + return result +} diff --git a/helpers/pathutils/resolve.go b/helpers/pathutils/resolve.go new file mode 100644 index 00000000..15e91911 --- /dev/null +++ b/helpers/pathutils/resolve.go @@ -0,0 +1,47 @@ +package pathutils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// PathMatchExpressionsAgainstAttributeConfig returns the path.Paths matching the given path.Expressions. +// +// Each path.Expression has been merged with the given attribute path.Expression +// (likely from the tfsdk.ValidateAttributeRequest), resolved, +// and then matched against the given attribute tfsdk.Config (also from the tfsdk.ValidateAttributeRequest). +// +// This is useful for tfsdk.AttributeValidator that accept path.Expressions, and validate the attributes matching +// to the expressions, in relation to the attribute the validator is applied to. +// For example usage, please look at the `schemavalidator` package in this repository. +func PathMatchExpressionsAgainstAttributeConfig(ctx context.Context, pathExps path.Expressions, attrPathExp path.Expression, attrConfig tfsdk.Config) (path.Paths, diag.Diagnostics) { + var resDiags diag.Diagnostics + + pathExpressions := MergeExpressionsWithAttribute(pathExps, attrPathExp) + + resPaths := make(path.Paths, 0, len(pathExpressions)) + + for _, pe := range pathExpressions { + // Retrieve all the attribute paths that match the given expressions + matchingPaths, diags := attrConfig.PathMatches(ctx, pe) + resDiags.Append(diags...) + if diags.HasError() { + return nil, resDiags + } + + // Confirm at least one attribute was matched. + // If not, collect errors so that the callee can bubble the bugs up. + if len(matchingPaths) == 0 { + resDiags.Append(validatordiag.BugInProviderDiagnostic(fmt.Sprintf("Path expression %q matches no attribute", pe))) + } + + resPaths = append(resPaths, matchingPaths...) + } + + return resPaths, resDiags +} diff --git a/helpers/validatordiag/diag.go b/helpers/validatordiag/diag.go index 44fcc87f..ca323fc9 100644 --- a/helpers/validatordiag/diag.go +++ b/helpers/validatordiag/diag.go @@ -35,8 +35,8 @@ func InvalidAttributeValueMatchDiagnostic(path path.Path, description string, va ) } -// InvalidAttributeSchemaDiagnostic returns an error Diagnostic to be used when a schemavalidator of attributes is invalid. -func InvalidAttributeSchemaDiagnostic(path path.Path, description string) diag.Diagnostic { +// InvalidAttributeCombinationDiagnostic returns an error Diagnostic to be used when a schemavalidator of attributes is invalid. +func InvalidAttributeCombinationDiagnostic(path path.Path, description string) diag.Diagnostic { return diag.NewAttributeErrorDiagnostic( path, "Invalid Attribute Combination", @@ -53,30 +53,10 @@ func InvalidAttributeTypeDiagnostic(path path.Path, description string, value st ) } -// ErrorsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityError. -func ErrorsCount(diags diag.Diagnostics) int { - count := 0 - - for _, d := range diags { - if diag.SeverityError == d.Severity() { - count++ - } - } - - return count -} - -// WarningsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityWarning. -func WarningsCount(diags diag.Diagnostics) int { - count := 0 - - for _, d := range diags { - if diag.SeverityWarning == d.Severity() { - count++ - } - } - - return count +func BugInProviderDiagnostic(summary string) diag.Diagnostic { + return diag.NewErrorDiagnostic(summary, + "This is a bug in the provider, which should be reported in the provider's own issue tracker", + ) } // capitalize will uppercase the first letter in a UTF-8 string. From a5fdeb7f0fa97fb6af2712fb76dd483ac48d58d8 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 6 Jul 2022 14:32:49 +0100 Subject: [PATCH 09/20] Update all `schemavalidator`s to use `path.Expressions` --- schemavalidator/at_least_one_of.go | 36 ++++---- schemavalidator/at_least_one_of_test.go | 106 +++++++++++++++-------- schemavalidator/conflicts_with.go | 32 ++++--- schemavalidator/conflicts_with_test.go | 110 +++++++++++++++++------- schemavalidator/exactly_one_of.go | 38 ++++---- schemavalidator/exactly_one_of_test.go | 107 +++++++++++++++-------- schemavalidator/required_with.go | 32 ++++--- schemavalidator/required_with_test.go | 106 +++++++++++++++-------- 8 files changed, 367 insertions(+), 200 deletions(-) diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 16bc7540..1ffe1c27 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -4,24 +4,23 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // atLeastOneOfAttributeValidator is the underlying struct implementing AtLeastOneOf. type atLeastOneOfAttributeValidator struct { - attrPaths []*tftypes.AttributePath + pathExpressions path.Expressions } -// AtLeastOneOf checks that of a set of *tftypes.AttributePath, +// AtLeastOneOf checks that of a set of path.Expression, // including the attribute it's applied to, at least one attribute out of all specified is configured. // -// The provided tftypes.AttributePath must be "absolute", -// and starting with top level attribute names. -func AtLeastOneOf(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { +// Relative path.Expression will be resolved against the validated attribute. +func AtLeastOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { return &atLeastOneOfAttributeValidator{attributePaths} } @@ -31,22 +30,21 @@ func (av atLeastOneOfAttributeValidator) Description(ctx context.Context) string return av.MarkdownDescription(ctx) } -func (av atLeastOneOfAttributeValidator) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %q", av.attrPaths) +func (av atLeastOneOfAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %q", av.pathExpressions) } func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // Assemble a slice of paths, ensuring we don't repeat the attribute this validator is applied to - var paths []*tftypes.AttributePath - if attributepath.Contains(req.AttributePath, av.attrPaths...) { - paths = av.attrPaths - } else { - paths = append(av.attrPaths, req.AttributePath) + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return } - for _, path := range paths { + // Validate values at the matching paths + for _, p := range matchingPaths { var v attr.Value - diags := req.Config.GetAttribute(ctx, path, &v) + diags := req.Config.GetAttribute(ctx, p, &v) res.Diagnostics.Append(diags...) if diags.HasError() { return @@ -57,8 +55,8 @@ func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk } } - res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("At least one attribute out of %q must be specified", attributepath.JoinToString(paths...)), + fmt.Sprintf("At least one attribute out of %q must be specified", matchingPaths), )) } diff --git a/schemavalidator/at_least_one_of_test.go b/schemavalidator/at_least_one_of_test.go index f705fe4c..35a32da1 100644 --- a/schemavalidator/at_least_one_of_test.go +++ b/schemavalidator/at_least_one_of_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "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" @@ -16,15 +16,16 @@ func TestAtLeastOneOfValidator(t *testing.T) { type testCase struct { req tfsdk.ValidateAttributeRequest - in []*tftypes.AttributePath + in path.Expressions expErrors int } testCases := map[string]testCase{ "base": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -47,14 +48,15 @@ func TestAtLeastOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "self-is-null": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Null: true}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -77,14 +79,15 @@ func TestAtLeastOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "error_none-set": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -112,16 +115,17 @@ func TestAtLeastOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 1, }, "multiple-set": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -149,15 +153,16 @@ func TestAtLeastOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, }, "allow-duplicate-input": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -185,16 +190,17 @@ func TestAtLeastOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("bar"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), }, }, "unknowns": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -222,11 +228,43 @@ func TestAtLeastOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + }, + expErrors: 1, + }, } for name, test := range testCases { @@ -239,12 +277,12 @@ func TestAtLeastOneOfValidator(t *testing.T) { t.Fatal("expected error(s), got none") } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go index 3225d203..78093435 100644 --- a/schemavalidator/conflicts_with.go +++ b/schemavalidator/conflicts_with.go @@ -4,25 +4,24 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // conflictsWithAttributeValidator is the underlying struct implementing ConflictsWith. type conflictsWithAttributeValidator struct { - attrPaths []*tftypes.AttributePath + pathExpressions path.Expressions } -// ConflictsWith checks that a set of *tftypes.AttributePath, +// ConflictsWith checks that a set of path.Expression, // including the attribute it's applied to, are not set simultaneously. // This implements the validation logic declaratively within the tfsdk.Schema. // -// The provided tftypes.AttributePath must be "absolute", -// and starting with top level attribute names. -func ConflictsWith(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { +// Relative path.Expression will be resolved against the validated attribute. +func ConflictsWith(attributePaths ...path.Expression) tfsdk.AttributeValidator { return &conflictsWithAttributeValidator{attributePaths} } @@ -33,7 +32,7 @@ func (av conflictsWithAttributeValidator) Description(ctx context.Context) strin } func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("Ensure that if an attribute is set, these are not set: %q", av.attrPaths) + return fmt.Sprintf("Ensure that if an attribute is set, these are not set: %q", av.pathExpressions) } func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { @@ -43,24 +42,31 @@ func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsd return } - for _, path := range av.attrPaths { + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + // Validate values at the matching paths + for _, p := range matchingPaths { // If the user specifies the same attribute this validator is applied to, // also as part of the input, skip it. - if req.AttributePath.Equal(path) { + if p.Equal(req.AttributePath) { continue } var o attr.Value - diags := req.Config.GetAttribute(ctx, path, &o) + diags := req.Config.GetAttribute(ctx, p, &o) res.Diagnostics.Append(diags...) if diags.HasError() { return } if !v.IsNull() && !o.IsNull() { - res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("Attribute %q cannot be specified when %q is specified", attributepath.ToString(path), attributepath.ToString(req.AttributePath)), + fmt.Sprintf("Attribute %q cannot be specified when %q is specified", p, req.AttributePath), )) } } diff --git a/schemavalidator/conflicts_with_test.go b/schemavalidator/conflicts_with_test.go index 0cc22487..eabc15e6 100644 --- a/schemavalidator/conflicts_with_test.go +++ b/schemavalidator/conflicts_with_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "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" @@ -16,15 +16,16 @@ func TestConflictsWithValidator(t *testing.T) { type testCase struct { req tfsdk.ValidateAttributeRequest - in []*tftypes.AttributePath + in path.Expressions expErrors int } testCases := map[string]testCase{ "base": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -52,16 +53,17 @@ func TestConflictsWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 2, }, "conflicting-is-nil": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -84,14 +86,15 @@ func TestConflictsWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "error_conflicting-is-unknown": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -114,15 +117,16 @@ func TestConflictsWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, expErrors: 1, }, "self-is-null": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Null: true}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -145,14 +149,15 @@ func TestConflictsWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "error_allow-duplicate-input": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -180,17 +185,18 @@ func TestConflictsWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("bar"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), }, expErrors: 2, }, "error_unknowns": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -218,9 +224,47 @@ func TestConflictsWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 2, + }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + path.MatchRoot("barz"), }, expErrors: 2, }, @@ -236,12 +280,12 @@ func TestConflictsWithValidator(t *testing.T) { t.Fatal("expected error(s), got none") } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go index 2775466a..505412c8 100644 --- a/schemavalidator/exactly_one_of.go +++ b/schemavalidator/exactly_one_of.go @@ -4,25 +4,24 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // exactlyOneOfAttributeValidator is the underlying struct implementing ExactlyOneOf. type exactlyOneOfAttributeValidator struct { - attrPaths []*tftypes.AttributePath + pathExpressions path.Expressions } -// ExactlyOneOf checks that of a set of *tftypes.AttributePath, +// ExactlyOneOf checks that of a set of path.Expression, // including the attribute it's applied to, one and only one attribute out of all specified is configured. // It will also cause a validation error if none are specified. // -// The provided tftypes.AttributePath must be "absolute", -// and starting with top level attribute names. -func ExactlyOneOf(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { +// Relative path.Expression will be resolved against the validated attribute. +func ExactlyOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { return &exactlyOneOfAttributeValidator{attributePaths} } @@ -33,22 +32,21 @@ func (av exactlyOneOfAttributeValidator) Description(ctx context.Context) string } func (av exactlyOneOfAttributeValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("Ensure that one and only one attribute from this collection is set: %q", av.attrPaths) + return fmt.Sprintf("Ensure that one and only one attribute from this collection is set: %q", av.pathExpressions) } func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // Assemble a slice of paths, ensuring we don't repeat the attribute this validator is applied to - var paths []*tftypes.AttributePath - if attributepath.Contains(req.AttributePath, av.attrPaths...) { - paths = av.attrPaths - } else { - paths = append(av.attrPaths, req.AttributePath) + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return } + // Validate values at the matching paths count := 0 - for _, path := range paths { + for _, p := range matchingPaths { var v attr.Value - diags := req.Config.GetAttribute(ctx, path, &v) + diags := req.Config.GetAttribute(ctx, p, &v) res.Diagnostics.Append(diags...) if diags.HasError() { return @@ -60,16 +58,16 @@ func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk } if count == 0 { - res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("No attribute specified when one (and only one) of %q is required", attributepath.JoinToString(paths...)), + fmt.Sprintf("No attribute specified when one (and only one) of %q is required", matchingPaths), )) } if count > 1 { - res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("%d attributes specified when one (and only one) of %q is required", count, attributepath.JoinToString(paths...)), + fmt.Sprintf("%d attributes specified when one (and only one) of %q is required", count, matchingPaths), )) } } diff --git a/schemavalidator/exactly_one_of_test.go b/schemavalidator/exactly_one_of_test.go index 1763b0e6..79507ba4 100644 --- a/schemavalidator/exactly_one_of_test.go +++ b/schemavalidator/exactly_one_of_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "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" @@ -16,15 +16,16 @@ func TestExactlyOneOfValidator(t *testing.T) { type testCase struct { req tfsdk.ValidateAttributeRequest - in []*tftypes.AttributePath + in path.Expressions expErrors int } testCases := map[string]testCase{ "base": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -47,15 +48,16 @@ func TestExactlyOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, expErrors: 1, }, "self-is-null": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Null: true}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -78,14 +80,15 @@ func TestExactlyOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "error_too-many": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -113,16 +116,17 @@ func TestExactlyOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 1, }, "error_too-few": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -150,16 +154,17 @@ func TestExactlyOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 1, }, "allow-duplicate-input": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -187,16 +192,17 @@ func TestExactlyOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("bar"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), }, }, "error_unknowns": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -224,12 +230,45 @@ func TestExactlyOneOfValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 1, }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + path.MatchRoot("barz"), + }, + expErrors: 2, + }, } for name, test := range testCases { @@ -242,12 +281,12 @@ func TestExactlyOneOfValidator(t *testing.T) { t.Fatal("expected error(s), got none") } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/schemavalidator/required_with.go b/schemavalidator/required_with.go index 6645cd9c..dbdb510b 100644 --- a/schemavalidator/required_with.go +++ b/schemavalidator/required_with.go @@ -4,25 +4,24 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // requiredWithAttributeValidator is the underlying struct implementing RequiredWith. type requiredWithAttributeValidator struct { - attrPaths []*tftypes.AttributePath + pathExpressions path.Expressions } -// RequiredWith checks that a set of *tftypes.AttributePath, +// RequiredWith checks that a set of path.Expression, // including the attribute it's applied to, are set simultaneously. // This implements the validation logic declaratively within the tfsdk.Schema. // -// The provided tftypes.AttributePath must be "absolute", -// and starting with top level attribute names. -func RequiredWith(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { +// Relative path.Expression will be resolved against the validated attribute. +func RequiredWith(attributePaths ...path.Expression) tfsdk.AttributeValidator { return &requiredWithAttributeValidator{attributePaths} } @@ -33,7 +32,7 @@ func (av requiredWithAttributeValidator) Description(ctx context.Context) string } func (av requiredWithAttributeValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("Ensure that if an attribute is set, also these are set: %q", av.attrPaths) + return fmt.Sprintf("Ensure that if an attribute is set, also these are set: %q", av.pathExpressions) } func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { @@ -43,24 +42,31 @@ func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk return } - for _, path := range av.attrPaths { + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) + res.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + // Validate values at the matching paths + for _, p := range matchingPaths { // If the user specifies the same attribute this validator is applied to, // also as part of the input, skip it. - if req.AttributePath.Equal(path) { + if p.Equal(req.AttributePath) { continue } var o attr.Value - diags := req.Config.GetAttribute(ctx, path, &o) + diags := req.Config.GetAttribute(ctx, p, &o) res.Diagnostics.Append(diags...) if diags.HasError() { return } if !v.IsNull() && o.IsNull() { - res.Diagnostics.Append(validatordiag.InvalidAttributeSchemaDiagnostic( + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("Attribute %q must be specified when %q is specified", attributepath.ToString(path), attributepath.ToString(req.AttributePath)), + fmt.Sprintf("Attribute %q must be specified when %q is specified", p, req.AttributePath), )) } } diff --git a/schemavalidator/required_with_test.go b/schemavalidator/required_with_test.go index c00726ce..f6452dbc 100644 --- a/schemavalidator/required_with_test.go +++ b/schemavalidator/required_with_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "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" @@ -16,15 +16,16 @@ func TestRequiredWithValidator(t *testing.T) { type testCase struct { req tfsdk.ValidateAttributeRequest - in []*tftypes.AttributePath + in path.Expressions expErrors int } testCases := map[string]testCase{ "base": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -47,14 +48,15 @@ func TestRequiredWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "self-is-null": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Null: true}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -77,14 +79,15 @@ func TestRequiredWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), + in: path.Expressions{ + path.MatchRoot("foo"), }, }, "error_missing-one": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -112,16 +115,17 @@ func TestRequiredWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 1, }, "error_missing-two": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -149,16 +153,17 @@ func TestRequiredWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, expErrors: 2, }, "allow-duplicate-input": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -186,16 +191,17 @@ func TestRequiredWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("bar"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), }, }, "unknowns": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, - AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"), + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ Schema: tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ @@ -223,11 +229,43 @@ func TestRequiredWithValidator(t *testing.T) { }), }, }, - in: []*tftypes.AttributePath{ - tftypes.NewAttributePath().WithAttributeName("foo"), - tftypes.NewAttributePath().WithAttributeName("baz"), + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), }, }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + }, + expErrors: 1, + }, } for name, test := range testCases { @@ -240,12 +278,12 @@ func TestRequiredWithValidator(t *testing.T) { t.Fatal("expected error(s), got none") } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) } if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } From e3b6a99ad1f389203002aa126d7782c7bc95deb8 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Wed, 6 Jul 2022 14:35:51 +0100 Subject: [PATCH 10/20] Updating dependencies --- go.mod | 1 - go.sum | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bf02fd42..530b7c77 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.10.0 // indirect github.com/hashicorp/terraform-plugin-log v0.4.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index ba3d6efd..b0788268 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,8 @@ github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906 h1:Y9JWkRpLhbJfdN85Dx+joLWRiwzLlZQkhf1qSbyPJI8= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906/go.mod h1:ActelD2V6yt2m0MwIX4jESGDYJ573rAvZswGjSGm1rY= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220629230053-34bd9b67bff9 h1:VqgfzFc6Vv9kcw5A6rwZuicOjzXl2bqJv7H+suDtysY= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220629230053-34bd9b67bff9/go.mod h1:ActelD2V6yt2m0MwIX4jESGDYJ573rAvZswGjSGm1rY= github.com/hashicorp/terraform-plugin-go v0.9.1/go.mod h1:ItjVSlQs70otlzcCwlPcU8FRXLdO973oYFRZwAOxy8M= github.com/hashicorp/terraform-plugin-go v0.10.0 h1:FIQDt/AZDSOXnN+znBnLLZA9aFk4/GwL40rwMLnvuTk= github.com/hashicorp/terraform-plugin-go v0.10.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= From c5d5a411fdebecb51c821aeaa3194efaa7ef65a8 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 7 Jul 2022 11:54:25 +0100 Subject: [PATCH 11/20] Update .changelog/42.txt Co-authored-by: Brian Flad --- .changelog/42.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.changelog/42.txt b/.changelog/42.txt index 2898e242..ee2fa9f8 100644 --- a/.changelog/42.txt +++ b/.changelog/42.txt @@ -9,7 +9,3 @@ int64validator: Added `OneOf()` and `NoneOf()` validation functions ```release-note:feature Introduced `numbervalidator` package with `OneOf()` and `NoneOf()` validation functions ``` - -```release-note:breaking-change -Renaming package `validatordiag` to be at `helpers/validatordiag` -``` From f19e7d0141afb0721f59fd2f94bdfc2718cbe286 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 7 Jul 2022 11:57:51 +0100 Subject: [PATCH 12/20] Apply suggestions from code review Co-authored-by: Brian Flad --- schemavalidator/at_least_one_of.go | 2 +- schemavalidator/doc.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 1ffe1c27..d0b6a73f 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -31,7 +31,7 @@ func (av atLeastOneOfAttributeValidator) Description(ctx context.Context) string } func (av atLeastOneOfAttributeValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %q", av.pathExpressions) + return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %s", av.pathExpressions) } func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { diff --git a/schemavalidator/doc.go b/schemavalidator/doc.go index bacb65c8..ddd097b8 100644 --- a/schemavalidator/doc.go +++ b/schemavalidator/doc.go @@ -1,5 +1,4 @@ -// Package schemavalidator provides validators that allow to express -// relationships between multiple attributes within the schema of a resource, -// data source or provider. +// Package schemavalidator provides validators to express relationships between +// multiple attributes within the schema of a resource, data source, or provider. // For example, checking that an attribute is present when another is present, or vice-versa. package schemavalidator From b6d9209b698cff67903340b377a333a4ff06a9ea Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 7 Jul 2022 14:14:35 +0100 Subject: [PATCH 13/20] Update schemavalidator/at_least_one_of.go Co-authored-by: Brian Flad --- schemavalidator/at_least_one_of.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index d0b6a73f..52b4b3e2 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -19,7 +19,7 @@ type atLeastOneOfAttributeValidator struct { // AtLeastOneOf checks that of a set of path.Expression, // including the attribute it's applied to, at least one attribute out of all specified is configured. // -// Relative path.Expression will be resolved against the validated attribute. +// Any relative path.Expression will be resolved against the attribute with this validator. func AtLeastOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { return &atLeastOneOfAttributeValidator{attributePaths} } From 2d96f2a0b49bb209d0dd2097ba80f559d9b64d93 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Thu, 7 Jul 2022 14:21:38 +0100 Subject: [PATCH 14/20] PR review feedback --- helpers/pathutils/merge.go | 2 +- schemavalidator/at_least_one_of.go | 8 ++++---- schemavalidator/conflicts_with.go | 18 ++++++------------ schemavalidator/exactly_one_of.go | 8 ++++---- schemavalidator/required_with.go | 18 ++++++------------ 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/helpers/pathutils/merge.go b/helpers/pathutils/merge.go index f97333a3..da01d32f 100644 --- a/helpers/pathutils/merge.go +++ b/helpers/pathutils/merge.go @@ -17,7 +17,7 @@ func MergeExpressionsWithAttribute(pathExps path.Expressions, attrPathExp path.E result = append(result, attrPathExp) for _, pe := range pathExps { - mpe := attrPathExp.Merge(pe).Resolve() + mpe := attrPathExp.Merge(pe) // Include the merged path expression, // only if it's not the same as the attribute diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 52b4b3e2..583eeb92 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -42,15 +42,15 @@ func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk } // Validate values at the matching paths - for _, p := range matchingPaths { - var v attr.Value - diags := req.Config.GetAttribute(ctx, p, &v) + for _, mp := range matchingPaths { + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) if diags.HasError() { return } - if !v.IsNull() { + if !mpVal.IsNull() { return } } diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go index 78093435..4815fae4 100644 --- a/schemavalidator/conflicts_with.go +++ b/schemavalidator/conflicts_with.go @@ -36,12 +36,6 @@ func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - var v attr.Value - res.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &v)...) - if res.Diagnostics.HasError() { - return - } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { @@ -49,24 +43,24 @@ func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsd } // Validate values at the matching paths - for _, p := range matchingPaths { + for _, mp := range matchingPaths { // If the user specifies the same attribute this validator is applied to, // also as part of the input, skip it. - if p.Equal(req.AttributePath) { + if mp.Equal(req.AttributePath) { continue } - var o attr.Value - diags := req.Config.GetAttribute(ctx, p, &o) + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) if diags.HasError() { return } - if !v.IsNull() && !o.IsNull() { + if !req.AttributeConfig.IsNull() && !mpVal.IsNull() { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("Attribute %q cannot be specified when %q is specified", p, req.AttributePath), + fmt.Sprintf("Attribute %q cannot be specified when %q is specified", mp, req.AttributePath), )) } } diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go index 505412c8..0306209c 100644 --- a/schemavalidator/exactly_one_of.go +++ b/schemavalidator/exactly_one_of.go @@ -44,15 +44,15 @@ func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk // Validate values at the matching paths count := 0 - for _, p := range matchingPaths { - var v attr.Value - diags := req.Config.GetAttribute(ctx, p, &v) + for _, mp := range matchingPaths { + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) if diags.HasError() { return } - if !v.IsNull() { + if !mpVal.IsNull() { count++ } } diff --git a/schemavalidator/required_with.go b/schemavalidator/required_with.go index dbdb510b..8512d85c 100644 --- a/schemavalidator/required_with.go +++ b/schemavalidator/required_with.go @@ -36,12 +36,6 @@ func (av requiredWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - var v attr.Value - res.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &v)...) - if res.Diagnostics.HasError() { - return - } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { @@ -49,24 +43,24 @@ func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk } // Validate values at the matching paths - for _, p := range matchingPaths { + for _, mp := range matchingPaths { // If the user specifies the same attribute this validator is applied to, // also as part of the input, skip it. - if p.Equal(req.AttributePath) { + if mp.Equal(req.AttributePath) { continue } - var o attr.Value - diags := req.Config.GetAttribute(ctx, p, &o) + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) if diags.HasError() { return } - if !v.IsNull() && o.IsNull() { + if !req.AttributeConfig.IsNull() && mpVal.IsNull() { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("Attribute %q must be specified when %q is specified", p, req.AttributePath), + fmt.Sprintf("Attribute %q must be specified when %q is specified", mp, req.AttributePath), )) } } From 7e4f00402495df6c29127b6bf9da3d72eb5dba19 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Fri, 8 Jul 2022 14:59:39 +0100 Subject: [PATCH 15/20] Making the `schemavalidator` permissive when the involved attributes' values are `Unknown` --- schemavalidator/at_least_one_of.go | 6 ++++++ schemavalidator/conflicts_with.go | 16 ++++++++++++++-- schemavalidator/conflicts_with_test.go | 5 ++--- schemavalidator/exactly_one_of.go | 11 +++++++++++ schemavalidator/exactly_one_of_test.go | 3 +-- schemavalidator/required_with.go | 11 +++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 583eeb92..6e5e6d4b 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -50,6 +50,12 @@ func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk return } + // Delay validation until all involved attribute + // have a known value + if mpVal.IsUnknown() { + return + } + if !mpVal.IsNull() { return } diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go index 4815fae4..7f06b943 100644 --- a/schemavalidator/conflicts_with.go +++ b/schemavalidator/conflicts_with.go @@ -36,6 +36,12 @@ func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is null, it cannot conflict with others; + // if it is unknown, we can't validate yet + if req.AttributeConfig.IsNull() || req.AttributeConfig.IsUnknown() { + return + } + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { @@ -45,7 +51,7 @@ func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsd // Validate values at the matching paths for _, mp := range matchingPaths { // If the user specifies the same attribute this validator is applied to, - // also as part of the input, skip it. + // also as part of the input, skip it if mp.Equal(req.AttributePath) { continue } @@ -57,7 +63,13 @@ func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsd return } - if !req.AttributeConfig.IsNull() && !mpVal.IsNull() { + // Delay validation until all involved attribute + // have a known value + if mpVal.IsUnknown() { + return + } + + if !mpVal.IsNull() { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, fmt.Sprintf("Attribute %q cannot be specified when %q is specified", mp, req.AttributePath), diff --git a/schemavalidator/conflicts_with_test.go b/schemavalidator/conflicts_with_test.go index eabc15e6..bd151f8e 100644 --- a/schemavalidator/conflicts_with_test.go +++ b/schemavalidator/conflicts_with_test.go @@ -90,7 +90,7 @@ func TestConflictsWithValidator(t *testing.T) { path.MatchRoot("foo"), }, }, - "error_conflicting-is-unknown": { + "conflicting-is-unknown": { req: tfsdk.ValidateAttributeRequest{ AttributeConfig: types.String{Value: "bar value"}, AttributePath: path.Root("bar"), @@ -120,7 +120,6 @@ func TestConflictsWithValidator(t *testing.T) { in: path.Expressions{ path.MatchRoot("foo"), }, - expErrors: 1, }, "self-is-null": { req: tfsdk.ValidateAttributeRequest{ @@ -228,7 +227,7 @@ func TestConflictsWithValidator(t *testing.T) { path.MatchRoot("foo"), path.MatchRoot("baz"), }, - expErrors: 2, + //expErrors: 2, }, "matches-no-attribute-in-schema": { req: tfsdk.ValidateAttributeRequest{ diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go index 0306209c..a1f56904 100644 --- a/schemavalidator/exactly_one_of.go +++ b/schemavalidator/exactly_one_of.go @@ -36,6 +36,11 @@ func (av exactlyOneOfAttributeValidator) MarkdownDescription(_ context.Context) } func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is unknown, we can't validate yet + if req.AttributeConfig.IsUnknown() { + return + } + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { @@ -52,6 +57,12 @@ func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk return } + // Delay validation until all involved attribute + // have a known value + if mpVal.IsUnknown() { + return + } + if !mpVal.IsNull() { count++ } diff --git a/schemavalidator/exactly_one_of_test.go b/schemavalidator/exactly_one_of_test.go index 79507ba4..6973a47f 100644 --- a/schemavalidator/exactly_one_of_test.go +++ b/schemavalidator/exactly_one_of_test.go @@ -198,7 +198,7 @@ func TestExactlyOneOfValidator(t *testing.T) { path.MatchRoot("baz"), }, }, - "error_unknowns": { + "other-attributes-are-unknown": { req: tfsdk.ValidateAttributeRequest{ AttributeConfig: types.String{Value: "bar value"}, AttributePath: path.Root("bar"), @@ -234,7 +234,6 @@ func TestExactlyOneOfValidator(t *testing.T) { path.MatchRoot("foo"), path.MatchRoot("baz"), }, - expErrors: 1, }, "matches-no-attribute-in-schema": { req: tfsdk.ValidateAttributeRequest{ diff --git a/schemavalidator/required_with.go b/schemavalidator/required_with.go index 8512d85c..60dabf5e 100644 --- a/schemavalidator/required_with.go +++ b/schemavalidator/required_with.go @@ -36,6 +36,11 @@ func (av requiredWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is unknown, we can't validate yet + if req.AttributeConfig.IsUnknown() { + return + } + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { @@ -57,6 +62,12 @@ func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk return } + // Delay validation until all involved attribute + // have a known value + if mpVal.IsUnknown() { + return + } + if !req.AttributeConfig.IsNull() && mpVal.IsNull() { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, From 02395ad41d8e33eefd22ba2d3fae1dd32d771c00 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Fri, 8 Jul 2022 15:25:40 +0100 Subject: [PATCH 16/20] Further PR review tweaks --- schemavalidator/at_least_one_of.go | 6 ++++++ schemavalidator/at_least_one_of_test.go | 4 ++-- schemavalidator/conflicts_with.go | 6 +++--- schemavalidator/exactly_one_of.go | 5 ----- schemavalidator/required_with.go | 5 ----- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 6e5e6d4b..843dc018 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -35,6 +35,12 @@ func (av atLeastOneOfAttributeValidator) MarkdownDescription(_ context.Context) } func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is not null, + // then we already know this validator is going to succeed. + if !req.AttributeConfig.IsNull() { + return + } + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { diff --git a/schemavalidator/at_least_one_of_test.go b/schemavalidator/at_least_one_of_test.go index 35a32da1..1c28d181 100644 --- a/schemavalidator/at_least_one_of_test.go +++ b/schemavalidator/at_least_one_of_test.go @@ -85,7 +85,7 @@ func TestAtLeastOneOfValidator(t *testing.T) { }, "error_none-set": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, + AttributeConfig: types.String{Null: true}, AttributePath: path.Root("bar"), AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ @@ -235,7 +235,7 @@ func TestAtLeastOneOfValidator(t *testing.T) { }, "matches-no-attribute-in-schema": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, + AttributeConfig: types.String{Null: true}, AttributePath: path.Root("bar"), AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go index 7f06b943..28c7a20c 100644 --- a/schemavalidator/conflicts_with.go +++ b/schemavalidator/conflicts_with.go @@ -36,9 +36,9 @@ func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // If attribute configuration is null, it cannot conflict with others; - // if it is unknown, we can't validate yet - if req.AttributeConfig.IsNull() || req.AttributeConfig.IsUnknown() { + // If attribute configuration is null, + // it cannot conflict with others + if req.AttributeConfig.IsNull() { return } diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go index a1f56904..41162bc6 100644 --- a/schemavalidator/exactly_one_of.go +++ b/schemavalidator/exactly_one_of.go @@ -36,11 +36,6 @@ func (av exactlyOneOfAttributeValidator) MarkdownDescription(_ context.Context) } func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // If attribute configuration is unknown, we can't validate yet - if req.AttributeConfig.IsUnknown() { - return - } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { diff --git a/schemavalidator/required_with.go b/schemavalidator/required_with.go index 60dabf5e..e39cc740 100644 --- a/schemavalidator/required_with.go +++ b/schemavalidator/required_with.go @@ -36,11 +36,6 @@ func (av requiredWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // If attribute configuration is unknown, we can't validate yet - if req.AttributeConfig.IsUnknown() { - return - } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { From 3d5a6f1604c8fd936d91cd838af47116bbc8df63 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Fri, 8 Jul 2022 16:38:49 +0100 Subject: [PATCH 17/20] Renamed 'RequiredWith' to 'AlsoRequires', and also updated the godoc --- .../{required_with.go => also_requires.go} | 31 +++++++++++-------- ...red_with_test.go => also_requires_test.go} | 2 +- schemavalidator/at_least_one_of.go | 11 ++++--- schemavalidator/conflicts_with.go | 9 +++--- schemavalidator/exactly_one_of.go | 8 +++-- 5 files changed, 34 insertions(+), 27 deletions(-) rename schemavalidator/{required_with.go => also_requires.go} (67%) rename schemavalidator/{required_with_test.go => also_requires_test.go} (99%) diff --git a/schemavalidator/required_with.go b/schemavalidator/also_requires.go similarity index 67% rename from schemavalidator/required_with.go rename to schemavalidator/also_requires.go index e39cc740..e36497d7 100644 --- a/schemavalidator/required_with.go +++ b/schemavalidator/also_requires.go @@ -11,31 +11,37 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -// requiredWithAttributeValidator is the underlying struct implementing RequiredWith. -type requiredWithAttributeValidator struct { +// alsoRequiresAttributeValidator is the underlying struct implementing AlsoRequires. +type alsoRequiresAttributeValidator struct { pathExpressions path.Expressions } -// RequiredWith checks that a set of path.Expression, -// including the attribute it's applied to, are set simultaneously. +// AlsoRequires checks that a set of path.Expression has a non-null value, +// if the current attribute also has a non-null value. +// // This implements the validation logic declaratively within the tfsdk.Schema. // // Relative path.Expression will be resolved against the validated attribute. -func RequiredWith(attributePaths ...path.Expression) tfsdk.AttributeValidator { - return &requiredWithAttributeValidator{attributePaths} +func AlsoRequires(attributePaths ...path.Expression) tfsdk.AttributeValidator { + return &alsoRequiresAttributeValidator{attributePaths} } -var _ tfsdk.AttributeValidator = (*requiredWithAttributeValidator)(nil) +var _ tfsdk.AttributeValidator = (*alsoRequiresAttributeValidator)(nil) -func (av requiredWithAttributeValidator) Description(ctx context.Context) string { +func (av alsoRequiresAttributeValidator) Description(ctx context.Context) string { return av.MarkdownDescription(ctx) } -func (av requiredWithAttributeValidator) MarkdownDescription(_ context.Context) string { +func (av alsoRequiresAttributeValidator) MarkdownDescription(_ context.Context) string { return fmt.Sprintf("Ensure that if an attribute is set, also these are set: %q", av.pathExpressions) } -func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { +func (av alsoRequiresAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is null, there is nothing else to validate + if req.AttributeConfig.IsNull() { + return + } + matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) res.Diagnostics.Append(diags...) if diags.HasError() { @@ -57,13 +63,12 @@ func (av requiredWithAttributeValidator) Validate(ctx context.Context, req tfsdk return } - // Delay validation until all involved attribute - // have a known value + // Delay validation until all involved attribute have a known value if mpVal.IsUnknown() { return } - if !req.AttributeConfig.IsNull() && mpVal.IsNull() { + if mpVal.IsNull() { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, fmt.Sprintf("Attribute %q must be specified when %q is specified", mp, req.AttributePath), diff --git a/schemavalidator/required_with_test.go b/schemavalidator/also_requires_test.go similarity index 99% rename from schemavalidator/required_with_test.go rename to schemavalidator/also_requires_test.go index f6452dbc..46bb89c6 100644 --- a/schemavalidator/required_with_test.go +++ b/schemavalidator/also_requires_test.go @@ -272,7 +272,7 @@ func TestRequiredWithValidator(t *testing.T) { t.Run(name, func(t *testing.T) { res := tfsdk.ValidateAttributeResponse{} - schemavalidator.RequiredWith(test.in...).Validate(context.TODO(), test.req, &res) + schemavalidator.AlsoRequires(test.in...).Validate(context.TODO(), test.req, &res) if test.expErrors > 0 && !res.Diagnostics.HasError() { t.Fatal("expected error(s), got none") diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 843dc018..19eb1a6e 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -17,7 +17,10 @@ type atLeastOneOfAttributeValidator struct { } // AtLeastOneOf checks that of a set of path.Expression, -// including the attribute it's applied to, at least one attribute out of all specified is configured. +// including the attribute it's applied to, +// at least one attribute out of all specified is has a non-null value. +// +// This implements the validation logic declaratively within the tfsdk.Schema. // // Any relative path.Expression will be resolved against the attribute with this validator. func AtLeastOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { @@ -35,8 +38,7 @@ func (av atLeastOneOfAttributeValidator) MarkdownDescription(_ context.Context) } func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // If attribute configuration is not null, - // then we already know this validator is going to succeed. + // If attribute configuration is not null, validator already succeeded. if !req.AttributeConfig.IsNull() { return } @@ -56,8 +58,7 @@ func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk return } - // Delay validation until all involved attribute - // have a known value + // Delay validation until all involved attribute have a known value if mpVal.IsUnknown() { return } diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go index 28c7a20c..94853034 100644 --- a/schemavalidator/conflicts_with.go +++ b/schemavalidator/conflicts_with.go @@ -17,7 +17,8 @@ type conflictsWithAttributeValidator struct { } // ConflictsWith checks that a set of path.Expression, -// including the attribute it's applied to, are not set simultaneously. +// including the attribute it's applied to, do not have a value simultaneously. +// // This implements the validation logic declaratively within the tfsdk.Schema. // // Relative path.Expression will be resolved against the validated attribute. @@ -36,8 +37,7 @@ func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) } func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - // If attribute configuration is null, - // it cannot conflict with others + // If attribute configuration is null, it cannot conflict with others if req.AttributeConfig.IsNull() { return } @@ -63,8 +63,7 @@ func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsd return } - // Delay validation until all involved attribute - // have a known value + // Delay validation until all involved attribute have a known value if mpVal.IsUnknown() { return } diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go index 41162bc6..7578be77 100644 --- a/schemavalidator/exactly_one_of.go +++ b/schemavalidator/exactly_one_of.go @@ -17,9 +17,12 @@ type exactlyOneOfAttributeValidator struct { } // ExactlyOneOf checks that of a set of path.Expression, -// including the attribute it's applied to, one and only one attribute out of all specified is configured. +// including the attribute it's applied to, +// one and only one attribute out of all specified has a value. // It will also cause a validation error if none are specified. // +// This implements the validation logic declaratively within the tfsdk.Schema. +// // Relative path.Expression will be resolved against the validated attribute. func ExactlyOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { return &exactlyOneOfAttributeValidator{attributePaths} @@ -52,8 +55,7 @@ func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk return } - // Delay validation until all involved attribute - // have a known value + // Delay validation until all involved attribute have a known value if mpVal.IsUnknown() { return } From ee380425a97147ccf6a8498b443dc58d2d404272 Mon Sep 17 00:00:00 2001 From: Ivan De Marino Date: Fri, 8 Jul 2022 17:18:29 +0100 Subject: [PATCH 18/20] Making use of the new `.Append()` method added to `path.Paths` and `path.Expressions` --- go.mod | 4 ++-- go.sum | 13 ++++--------- helpers/pathutils/merge.go | 13 ++++--------- helpers/pathutils/resolve.go | 2 +- listvalidator/type_validation.go | 2 +- mapvalidator/type_validation.go | 2 +- setvalidator/type_validation.go | 2 +- 7 files changed, 14 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 530b7c77..d8050b69 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.17 require ( github.com/google/go-cmp v0.5.8 - github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220629230053-34bd9b67bff9 - github.com/hashicorp/terraform-plugin-go v0.10.0 + github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220708153747-90e2017163f1 + github.com/hashicorp/terraform-plugin-go v0.11.0 ) require ( diff --git a/go.sum b/go.sum index b0788268..0bc5035a 100644 --- a/go.sum +++ b/go.sum @@ -57,21 +57,17 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220629230053-34bd9b67bff9 h1:VqgfzFc6Vv9kcw5A6rwZuicOjzXl2bqJv7H+suDtysY= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220629230053-34bd9b67bff9/go.mod h1:ActelD2V6yt2m0MwIX4jESGDYJ573rAvZswGjSGm1rY= -github.com/hashicorp/terraform-plugin-go v0.9.1/go.mod h1:ItjVSlQs70otlzcCwlPcU8FRXLdO973oYFRZwAOxy8M= -github.com/hashicorp/terraform-plugin-go v0.10.0 h1:FIQDt/AZDSOXnN+znBnLLZA9aFk4/GwL40rwMLnvuTk= -github.com/hashicorp/terraform-plugin-go v0.10.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= -github.com/hashicorp/terraform-plugin-log v0.4.0/go.mod h1:9KclxdunFownr4pIm1jdmwKRmE4d6HVG2c9XDq47rpg= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220708153747-90e2017163f1 h1:r65/pC0nOeTmp7MJRgi2Ze2ZhyLBWaQgPXy3e4fq3mE= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220708153747-90e2017163f1/go.mod h1:+H4ieVu7X4bfYlLB/zytek48e4CjcG+gjKdVOjVY1PU= +github.com/hashicorp/terraform-plugin-go v0.11.0 h1:YXsvSCx7GbQO5jIUQd77FesqmIBxgSvYAtAX1NqErTk= +github.com/hashicorp/terraform-plugin-go v0.11.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= github.com/hashicorp/terraform-plugin-log v0.4.1 h1:xpbmVhvuU3mgHzLetOmx9pkOL2rmgpu302XxddON6eo= github.com/hashicorp/terraform-plugin-log v0.4.1/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= -github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -191,7 +187,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/helpers/pathutils/merge.go b/helpers/pathutils/merge.go index da01d32f..3170fbfa 100644 --- a/helpers/pathutils/merge.go +++ b/helpers/pathutils/merge.go @@ -14,16 +14,11 @@ func MergeExpressionsWithAttribute(pathExps path.Expressions, attrPathExp path.E result := make(path.Expressions, 0, len(pathExps)+1) // First, add the attribute own path expression to the result - result = append(result, attrPathExp) - + result.Append(attrPathExp) + // Then, add all the other path expressions, + // after they have been merged to the attribute own path for _, pe := range pathExps { - mpe := attrPathExp.Merge(pe) - - // Include the merged path expression, - // only if it's not the same as the attribute - if !mpe.Equal(attrPathExp) { - result = append(result, mpe) - } + result.Append(attrPathExp.Merge(pe)) } return result diff --git a/helpers/pathutils/resolve.go b/helpers/pathutils/resolve.go index 15e91911..fe971ceb 100644 --- a/helpers/pathutils/resolve.go +++ b/helpers/pathutils/resolve.go @@ -40,7 +40,7 @@ func PathMatchExpressionsAgainstAttributeConfig(ctx context.Context, pathExps pa resDiags.Append(validatordiag.BugInProviderDiagnostic(fmt.Sprintf("Path expression %q matches no attribute", pe))) } - resPaths = append(resPaths, matchingPaths...) + resPaths.Append(matchingPaths...) } return resPaths, resDiags diff --git a/listvalidator/type_validation.go b/listvalidator/type_validation.go index 4ebeae23..462edf1c 100644 --- a/listvalidator/type_validation.go +++ b/listvalidator/type_validation.go @@ -15,7 +15,7 @@ func validateList(ctx context.Context, request tfsdk.ValidateAttributeRequest, r diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &l) if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) + response.Diagnostics.Append(diags...) return nil, false } diff --git a/mapvalidator/type_validation.go b/mapvalidator/type_validation.go index e6448f0c..32450618 100644 --- a/mapvalidator/type_validation.go +++ b/mapvalidator/type_validation.go @@ -15,7 +15,7 @@ func validateMap(ctx context.Context, request tfsdk.ValidateAttributeRequest, re diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &m) if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) + response.Diagnostics.Append(diags...) return nil, false } diff --git a/setvalidator/type_validation.go b/setvalidator/type_validation.go index 60ba6613..e261af2a 100644 --- a/setvalidator/type_validation.go +++ b/setvalidator/type_validation.go @@ -15,7 +15,7 @@ func validateSet(ctx context.Context, request tfsdk.ValidateAttributeRequest, re diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &s) if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) + response.Diagnostics.Append(diags...) return nil, false } From 259abd41bd21fe5bbd419475f9545f54a635e5ad Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 11 Jul 2022 14:28:39 -0400 Subject: [PATCH 19/20] deps: Update github.com/hashicorp/terraform-plugin-framework@main --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d8050b69..dc60f2e9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( github.com/google/go-cmp v0.5.8 - github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220708153747-90e2017163f1 + github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707 github.com/hashicorp/terraform-plugin-go v0.11.0 ) diff --git a/go.sum b/go.sum index 0bc5035a..2265c03e 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220708153747-90e2017163f1 h1:r65/pC0nOeTmp7MJRgi2Ze2ZhyLBWaQgPXy3e4fq3mE= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220708153747-90e2017163f1/go.mod h1:+H4ieVu7X4bfYlLB/zytek48e4CjcG+gjKdVOjVY1PU= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707 h1:4wAdubsgZ/eAbCjW2At8w6UDeVCel76jWDHsWvbc8Pk= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707/go.mod h1:+H4ieVu7X4bfYlLB/zytek48e4CjcG+gjKdVOjVY1PU= github.com/hashicorp/terraform-plugin-go v0.11.0 h1:YXsvSCx7GbQO5jIUQd77FesqmIBxgSvYAtAX1NqErTk= github.com/hashicorp/terraform-plugin-go v0.11.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= github.com/hashicorp/terraform-plugin-log v0.4.1 h1:xpbmVhvuU3mgHzLetOmx9pkOL2rmgpu302XxddON6eo= From a357afd3cb138f34e399b717c6df0fe535ea8ea7 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 11 Jul 2022 14:29:39 -0400 Subject: [PATCH 20/20] schemavalidator: Updates to use (Expression).MergeExpressions() --- helpers/pathutils/merge.go | 25 ---------- helpers/pathutils/resolve.go | 47 ------------------- schemavalidator/also_requires.go | 57 +++++++++++++---------- schemavalidator/at_least_one_of.go | 43 ++++++++++------- schemavalidator/at_least_one_of_test.go | 2 +- schemavalidator/conflicts_with.go | 57 +++++++++++++---------- schemavalidator/exactly_one_of.go | 62 ++++++++++++++++++------- schemavalidator/exactly_one_of_test.go | 4 +- 8 files changed, 136 insertions(+), 161 deletions(-) delete mode 100644 helpers/pathutils/merge.go delete mode 100644 helpers/pathutils/resolve.go diff --git a/helpers/pathutils/merge.go b/helpers/pathutils/merge.go deleted file mode 100644 index 3170fbfa..00000000 --- a/helpers/pathutils/merge.go +++ /dev/null @@ -1,25 +0,0 @@ -package pathutils - -import ( - "github.com/hashicorp/terraform-plugin-framework/path" -) - -// MergeExpressionsWithAttribute returns the given path.Expressions, -// but each has been merged with the given attribute path.Expression, -// and then resolved. -// -// Additionally, if the attribute path.Expression was not part of the initial slice, -// it is added to the result. -func MergeExpressionsWithAttribute(pathExps path.Expressions, attrPathExp path.Expression) path.Expressions { - result := make(path.Expressions, 0, len(pathExps)+1) - - // First, add the attribute own path expression to the result - result.Append(attrPathExp) - // Then, add all the other path expressions, - // after they have been merged to the attribute own path - for _, pe := range pathExps { - result.Append(attrPathExp.Merge(pe)) - } - - return result -} diff --git a/helpers/pathutils/resolve.go b/helpers/pathutils/resolve.go deleted file mode 100644 index fe971ceb..00000000 --- a/helpers/pathutils/resolve.go +++ /dev/null @@ -1,47 +0,0 @@ -package pathutils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" -) - -// PathMatchExpressionsAgainstAttributeConfig returns the path.Paths matching the given path.Expressions. -// -// Each path.Expression has been merged with the given attribute path.Expression -// (likely from the tfsdk.ValidateAttributeRequest), resolved, -// and then matched against the given attribute tfsdk.Config (also from the tfsdk.ValidateAttributeRequest). -// -// This is useful for tfsdk.AttributeValidator that accept path.Expressions, and validate the attributes matching -// to the expressions, in relation to the attribute the validator is applied to. -// For example usage, please look at the `schemavalidator` package in this repository. -func PathMatchExpressionsAgainstAttributeConfig(ctx context.Context, pathExps path.Expressions, attrPathExp path.Expression, attrConfig tfsdk.Config) (path.Paths, diag.Diagnostics) { - var resDiags diag.Diagnostics - - pathExpressions := MergeExpressionsWithAttribute(pathExps, attrPathExp) - - resPaths := make(path.Paths, 0, len(pathExpressions)) - - for _, pe := range pathExpressions { - // Retrieve all the attribute paths that match the given expressions - matchingPaths, diags := attrConfig.PathMatches(ctx, pe) - resDiags.Append(diags...) - if diags.HasError() { - return nil, resDiags - } - - // Confirm at least one attribute was matched. - // If not, collect errors so that the callee can bubble the bugs up. - if len(matchingPaths) == 0 { - resDiags.Append(validatordiag.BugInProviderDiagnostic(fmt.Sprintf("Path expression %q matches no attribute", pe))) - } - - resPaths.Append(matchingPaths...) - } - - return resPaths, resDiags -} diff --git a/schemavalidator/also_requires.go b/schemavalidator/also_requires.go index e36497d7..92452fce 100644 --- a/schemavalidator/also_requires.go +++ b/schemavalidator/also_requires.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" @@ -42,37 +41,45 @@ func (av alsoRequiresAttributeValidator) Validate(ctx context.Context, req tfsdk return } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) - res.Diagnostics.Append(diags...) - if diags.HasError() { - return - } + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) - // Validate values at the matching paths - for _, mp := range matchingPaths { - // If the user specifies the same attribute this validator is applied to, - // also as part of the input, skip it. - if mp.Equal(req.AttributePath) { - continue - } + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) - var mpVal attr.Value - diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) + + // Collect all errors if diags.HasError() { - return + continue } - // Delay validation until all involved attribute have a known value - if mpVal.IsUnknown() { - return - } + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.AttributePath) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } - if mpVal.IsNull() { - res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( - req.AttributePath, - fmt.Sprintf("Attribute %q must be specified when %q is specified", mp, req.AttributePath), - )) + if mpVal.IsNull() { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("Attribute %q must be specified when %q is specified", mp, req.AttributePath), + )) + } } } } diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go index 19eb1a6e..c87aa981 100644 --- a/schemavalidator/at_least_one_of.go +++ b/schemavalidator/at_least_one_of.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" @@ -43,33 +42,41 @@ func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk return } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) - res.Diagnostics.Append(diags...) - if diags.HasError() { - return - } + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) + + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) - // Validate values at the matching paths - for _, mp := range matchingPaths { - var mpVal attr.Value - diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) + + // Collect all errors if diags.HasError() { - return + continue } - // Delay validation until all involved attribute have a known value - if mpVal.IsUnknown() { - return - } + for _, mp := range matchedPaths { + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } - if !mpVal.IsNull() { - return + if !mpVal.IsNull() { + return + } } } res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("At least one attribute out of %q must be specified", matchingPaths), + fmt.Sprintf("At least one attribute out of %s must be specified", expressions), )) } diff --git a/schemavalidator/at_least_one_of_test.go b/schemavalidator/at_least_one_of_test.go index 1c28d181..45a1dfd8 100644 --- a/schemavalidator/at_least_one_of_test.go +++ b/schemavalidator/at_least_one_of_test.go @@ -263,7 +263,7 @@ func TestAtLeastOneOfValidator(t *testing.T) { in: path.Expressions{ path.MatchRoot("fooz"), }, - expErrors: 1, + expErrors: 2, }, } diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go index 94853034..306b5d58 100644 --- a/schemavalidator/conflicts_with.go +++ b/schemavalidator/conflicts_with.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" @@ -42,37 +41,45 @@ func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsd return } - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) - res.Diagnostics.Append(diags...) - if diags.HasError() { - return - } + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) - // Validate values at the matching paths - for _, mp := range matchingPaths { - // If the user specifies the same attribute this validator is applied to, - // also as part of the input, skip it - if mp.Equal(req.AttributePath) { - continue - } + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) - var mpVal attr.Value - diags := req.Config.GetAttribute(ctx, mp, &mpVal) res.Diagnostics.Append(diags...) + + // Collect all errors if diags.HasError() { - return + continue } - // Delay validation until all involved attribute have a known value - if mpVal.IsUnknown() { - return - } + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.AttributePath) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } - if !mpVal.IsNull() { - res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( - req.AttributePath, - fmt.Sprintf("Attribute %q cannot be specified when %q is specified", mp, req.AttributePath), - )) + if !mpVal.IsNull() { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("Attribute %q cannot be specified when %q is specified", mp, req.AttributePath), + )) + } } } } diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go index 7578be77..8b098bcb 100644 --- a/schemavalidator/exactly_one_of.go +++ b/schemavalidator/exactly_one_of.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/pathutils" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" @@ -39,43 +38,70 @@ func (av exactlyOneOfAttributeValidator) MarkdownDescription(_ context.Context) } func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { - matchingPaths, diags := pathutils.PathMatchExpressionsAgainstAttributeConfig(ctx, av.pathExpressions, req.AttributePathExpression, req.Config) - res.Diagnostics.Append(diags...) - if diags.HasError() { + count := 0 + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) + + // If current attribute is unknown, delay validation + if req.AttributeConfig.IsUnknown() { return } - // Validate values at the matching paths - count := 0 - for _, mp := range matchingPaths { - var mpVal attr.Value - diags := req.Config.GetAttribute(ctx, mp, &mpVal) + // Now that we know the current attribute is known, check whether it is + // null to determine if it should contribute to the count. Later logic + // will remove a duplicate matching path, should it be included in the + // given expressions. + if !req.AttributeConfig.IsNull() { + count++ + } + + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + res.Diagnostics.Append(diags...) + + // Collect all errors if diags.HasError() { - return + continue } - // Delay validation until all involved attribute have a known value - if mpVal.IsUnknown() { - return - } + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.AttributePath) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } - if !mpVal.IsNull() { - count++ + if !mpVal.IsNull() { + count++ + } } } if count == 0 { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("No attribute specified when one (and only one) of %q is required", matchingPaths), + fmt.Sprintf("No attribute specified when one (and only one) of %s is required", expressions), )) } if count > 1 { res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( req.AttributePath, - fmt.Sprintf("%d attributes specified when one (and only one) of %q is required", count, matchingPaths), + fmt.Sprintf("%d attributes specified when one (and only one) of %s is required", count, expressions), )) } } diff --git a/schemavalidator/exactly_one_of_test.go b/schemavalidator/exactly_one_of_test.go index 6973a47f..d05fef5d 100644 --- a/schemavalidator/exactly_one_of_test.go +++ b/schemavalidator/exactly_one_of_test.go @@ -124,7 +124,7 @@ func TestExactlyOneOfValidator(t *testing.T) { }, "error_too-few": { req: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "bar value"}, + AttributeConfig: types.String{Null: true}, AttributePath: path.Root("bar"), AttributePathExpression: path.MatchRoot("bar"), Config: tfsdk.Config{ @@ -258,7 +258,7 @@ func TestExactlyOneOfValidator(t *testing.T) { }, }, map[string]tftypes.Value{ "foo": tftypes.NewValue(tftypes.Number, 42), - "bar": tftypes.NewValue(tftypes.String, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), }), }, },