From 86f35a35e00935511b424ffe4ae299549bfaf499 Mon Sep 17 00:00:00 2001 From: Mickael Stanislas Date: Fri, 20 Dec 2024 10:57:15 +0100 Subject: [PATCH] feat: string/int64 validator OneOfWithDescriptionIfAttributeIsOneOf --- .../oneofwithdescriptionifattributeisoneof.md | 58 ++++ ...oneofwithdescriptionifattributeisoneof.png | Bin 0 -> 34769 bytes docs/int64validator/index.md | 1 + docs/stringvalidator/index.md | 1 + ...with_description_if_attribute_is_one_of.go | 35 +++ ...with_description_if_attribute_is_one_of.go | 273 ++++++++++++++++++ ...description_if_attribute_is_one_of_test.go | 203 +++++++++++++ ...with_description_if_attribute_is_one_of.go | 35 +++ 8 files changed, 606 insertions(+) create mode 100644 docs/common/oneofwithdescriptionifattributeisoneof.md create mode 100644 docs/common/oneofwithdescriptionifattributeisoneof.png create mode 100644 int64validator/one_of_with_description_if_attribute_is_one_of.go create mode 100644 internal/one_of_with_description_if_attribute_is_one_of.go create mode 100644 internal/one_of_with_description_if_attribute_is_one_of_test.go create mode 100644 stringvalidator/one_of_with_description_if_attribute_is_one_of.go diff --git a/docs/common/oneofwithdescriptionifattributeisoneof.md b/docs/common/oneofwithdescriptionifattributeisoneof.md new file mode 100644 index 0000000..c7b4ae4 --- /dev/null +++ b/docs/common/oneofwithdescriptionifattributeisoneof.md @@ -0,0 +1,58 @@ +# `OneOfWithDescription` + +!!! quote inline end "Released in v1.9.0" + +This validator is used to check if the string is one of the given values if the attribute is one of and format the description and the markdown description. + +## How to use it + +```go +// Schema defines the schema for the resource. +func (r *xResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + (...) + "foo": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "foo ...", + Validators: []validator.String{ + fstringvalidator.OneOf("VM_NAME", "VM_TAGS"), + }, + }, + "bar": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "bar of ...", + Validators: []validator.String{ + fstringvalidator.OneOfWithDescriptionIfAttributeIsOneOf( + path.MatchRelative().AtParent().AtName("foo"), + []attr.Value{types.StringValue("VM_NAME")}, + func() []fstringvalidator.OneOfWithDescriptionIfAttributeIsOneOfValues { + return []fstringvalidator.OneOfWithDescriptionIfAttributeIsOneOfValues{ + { + Value: "CONTAINS", + Description: "The `value` must be contained in the VM name.", + }, + { + Value: "STARTS_WITH", + Description: "The VM name must start with the `value`.", + }, + { + Value: "ENDS_WITH", + Description: "The VM name must end with the `value`.", + }, + { + Value: "EQUALS", + Description: "The VM name must be equal to the `value`.", + }, + } + }()...), + }, + }, +``` + +## Description and Markdown description + +* **Description:** +If the value of attribute <.type is "VM_NAME" the allowed values are : "CONTAINS" (The `value` must be contained in the VM name.), "STARTS_WITH" (The VM name must start with the `value`.), "ENDS_WITH" (The VM name must end with the `value`.), "EQUALS" (The VM name must be equal to the `value`.) +* **Markdown description:** + +![oneofwithdescriptionifattributeisoneof](oneofwithdescriptionifattributeisoneof.png) diff --git a/docs/common/oneofwithdescriptionifattributeisoneof.png b/docs/common/oneofwithdescriptionifattributeisoneof.png new file mode 100644 index 0000000000000000000000000000000000000000..3b63093051ddc729805786175b3f0bc82b22667a GIT binary patch literal 34769 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≶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

AMING_=A* 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 literal 0 HcmV?d00001 diff --git a/docs/int64validator/index.md b/docs/int64validator/index.md index ba2a908..a76b502 100644 --- a/docs/int64validator/index.md +++ b/docs/int64validator/index.md @@ -18,6 +18,7 @@ import ( - [`NullIfAttributeIsOneOf`](../common/null_if_attribute_is_one_of.md) - This validator is used to verify the attribute value is null if another attribute is one of the given values. - [`NullIfAttributeIsSet`](../common/null_if_attribute_is_set.md) - This validator is used to verify the attribute value is null if another attribute is set. - [`OneOfWithDescription`](oneofwithdescription.md) - This validator is used to check if the string is one of the given values and format the description and the markdown description. +- [`OneOfWithDescriptionIfAttributeIsOneOf`](../common/oneofwithdescriptionifattributeisoneof.md) - This validator is used to check if the string is one of the given values if the attribute is one of and format the description and the markdown description. - [`AttributeIsDivisibleByAnInteger`](attribute_is_divisible_by_an_integer.md) - This validator is used to validate that the attribute is divisible by an integer. - [`ZeroRemainder`](zero_remainder.md) - This validator checks if the configured attribute is divisible by a specified integer X, and has zero remainder. diff --git a/docs/stringvalidator/index.md b/docs/stringvalidator/index.md index 13126a8..7af6212 100644 --- a/docs/stringvalidator/index.md +++ b/docs/stringvalidator/index.md @@ -18,6 +18,7 @@ import ( - [`NullIfAttributeIsOneOf`](../common/null_if_attribute_is_one_of.md) - This validator is used to verify the attribute value is null if another attribute is one of the given values. - [`NullIfAttributeIsSet`](../common/null_if_attribute_is_set.md) - This validator is used to verify the attribute value is null if another attribute is set. - [`OneOfWithDescription`](oneofwithdescription.md) - This validator is used to check if the string is one of the given values and format the description and the markdown description. +- [`OneOfWithDescriptionIfAttributeIsOneOf`](../common/oneofwithdescriptionifattributeisoneof.md) - This validator is used to check if the string is one of the given values if the attribute is one of and format the description and the markdown description. ### Network diff --git a/int64validator/one_of_with_description_if_attribute_is_one_of.go b/int64validator/one_of_with_description_if_attribute_is_one_of.go new file mode 100644 index 0000000..d83e9ca --- /dev/null +++ b/int64validator/one_of_with_description_if_attribute_is_one_of.go @@ -0,0 +1,35 @@ +package int64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" +) + +type OneOfWithDescriptionIfAttributeIsOneOfValues struct { + Value int64 + Description string +} + +// OneOfWithDescriptionIfAttributeIsOneOf checks that the value is one of the expected values if the attribute is one of the exceptedValue. +// The description of the value is used to generate advanced +// Description and MarkdownDescription messages. +func OneOfWithDescriptionIfAttributeIsOneOf(path path.Expression, exceptedValue []attr.Value, values ...OneOfWithDescriptionIfAttributeIsOneOfValues) validator.String { + frameworkValues := make([]internal.OneOfWithDescriptionIfAttributeIsOneOf, 0, len(values)) + + for _, v := range values { + frameworkValues = append(frameworkValues, internal.OneOfWithDescriptionIfAttributeIsOneOf{ + Value: types.Int64Value(v.Value), + Description: v.Description, + }) + } + + return internal.OneOfWithDescriptionIfAttributeIsOneOfValidator{ + Values: frameworkValues, + ExceptedValues: exceptedValue, + PathExpression: path, + } +} diff --git a/internal/one_of_with_description_if_attribute_is_one_of.go b/internal/one_of_with_description_if_attribute_is_one_of.go new file mode 100644 index 0000000..ead8026 --- /dev/null +++ b/internal/one_of_with_description_if_attribute_is_one_of.go @@ -0,0 +1,273 @@ +package internal + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +const oneOfWithDescriptionIfAttributeIsOneOfValidatorDescription = "Value must be one of:" + +// This type of validator must satisfy all types. +var ( + _ validator.Float64 = OneOfWithDescriptionValidator{} + _ validator.Int64 = OneOfWithDescriptionValidator{} + _ validator.List = OneOfWithDescriptionValidator{} + _ validator.Map = OneOfWithDescriptionValidator{} + _ validator.Number = OneOfWithDescriptionValidator{} + _ validator.Set = OneOfWithDescriptionValidator{} + _ validator.String = OneOfWithDescriptionValidator{} +) + +type OneOfWithDescriptionIfAttributeIsOneOf struct { + Value attr.Value + Description string +} + +// OneOfWithDescriptionValidator validates that the value matches one of expected values. +type OneOfWithDescriptionIfAttributeIsOneOfValidator struct { + PathExpression path.Expression + Values []OneOfWithDescriptionIfAttributeIsOneOf + ExceptedValues []attr.Value +} + +type OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest struct { + Config tfsdk.Config + ConfigValue attr.Value + Path path.Path + PathExpression path.Expression + Values []OneOfWithDescriptionIfAttributeIsOneOf + ExceptedValues []attr.Value +} + +type OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse struct { + Diagnostics diag.Diagnostics +} + +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) Description(_ context.Context) string { + var expectedValueDescritpion string + for i, expectedValue := range v.ExceptedValues { + // remove the quotes around the string + if i == len(v.ExceptedValues)-1 { + expectedValueDescritpion += expectedValue.String() + break + } + expectedValueDescritpion += fmt.Sprintf("%s, ", expectedValue.String()) + } + + var valuesDescription string + for i, value := range v.Values { + if i == len(v.Values)-1 { + valuesDescription += fmt.Sprintf("%s (%s)", value.Value.String(), value.Description) + break + } + valuesDescription += fmt.Sprintf("%s (%s), ", value.Value.String(), value.Description) + } + + switch len(v.ExceptedValues) { + case 1: + return fmt.Sprintf("If the value of attribute %s is %s the allowed values are : %s", v.PathExpression.String(), expectedValueDescritpion, valuesDescription) + default: + return fmt.Sprintf("If the value of attribute %s is one of %s the allowed are : %s", v.PathExpression.String(), expectedValueDescritpion, valuesDescription) + } +} + +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) MarkdownDescription(_ context.Context) string { + var expectedValueDescritpion string + for i, expectedValue := range v.ExceptedValues { + // remove the quotes around the string + x := strings.Trim(expectedValue.String(), "\"") + + switch i { + case len(v.ExceptedValues) - 1: + expectedValueDescritpion += fmt.Sprintf("`%s`", x) + case len(v.ExceptedValues) - 2: + expectedValueDescritpion += fmt.Sprintf("`%s` or ", x) + default: + expectedValueDescritpion += fmt.Sprintf("`%s`, ", x) + } + } + + valuesDescription := "" + for _, value := range v.Values { + valuesDescription += fmt.Sprintf("- `%s` - %s
", value.Value.String(), value.Description) + } + + switch len(v.ExceptedValues) { + case 1: + return fmt.Sprintf("\n\n-> **If the value of the attribute [`%s`](#%s) is %s the value is one of** %s", v.PathExpression, v.PathExpression, expectedValueDescritpion, valuesDescription) + default: + return fmt.Sprintf("\n\n-> **If the value of the attribute [`%s`](#%s) is one of %s** : %s", v.PathExpression, v.PathExpression, expectedValueDescritpion, valuesDescription) + } +} + +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) Validate(ctx context.Context, req OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest, res *OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse) { + // Here attribute configuration is null or unknown, so we need to check if attribute in the path + // is equal to one of the excepted values + paths, diags := req.Config.PathMatches(ctx, req.PathExpression.Merge(v.PathExpression)) + if diags.HasError() { + res.Diagnostics.Append(diags...) + return + } + + if len(paths) == 0 { + res.Diagnostics.AddError( + fmt.Sprintf("Invalid configuration for attribute %s", req.Path), + "Path must be set", + ) + return + } + + res.Diagnostics.AddWarning("Paths", fmt.Sprintf("%v", paths)) + + path := paths[0] + + // mpVal is the value of the attribute in the path + var mpVal attr.Value + res.Diagnostics.Append(req.Config.GetAttribute(ctx, path, &mpVal)...) + if res.Diagnostics.HasError() { + res.Diagnostics.AddError( + fmt.Sprintf("Invalid configuration for attribute %s", req.Path), + fmt.Sprintf("Unable to retrieve attribute path: %q", path), + ) + return + } + + // If the target attribute configuration is unknown or null, there is nothing else to validate + if mpVal.IsNull() || mpVal.IsUnknown() { + return + } + + for _, expectedValue := range v.ExceptedValues { + // If the value of the target attribute is equal to one of the expected values, we need to validate the value of the current attribute + if mpVal.Equal(expectedValue) || mpVal.String() == expectedValue.String() { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + res.Diagnostics.AddAttributeError( + path, + fmt.Sprintf("Invalid configuration for attribute %s", req.Path), + fmt.Sprintf("Value is empty. %s", v.Description(ctx)), + ) + return + } + + for _, value := range v.Values { + if req.ConfigValue.Equal(value.Value) { + // Ok the value is valid + return + } + } + + // The value is not valid + res.Diagnostics.AddAttributeError( + path, + fmt.Sprintf("Invalid configuration for attribute %s", req.Path), + fmt.Sprintf("Invalid value %s. %s", req.ConfigValue.String(), v.Description(ctx)), + ) + return + } + } +} + +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// Float64 validates that the value matches one of expected values. +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateFloat64(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// Int64 validates that the value matches one of expected values. +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// Number validates that the value matches one of expected values. +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateNumber(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// List validates that the value matches one of expected values. +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// Set validates that the value matches one of expected values. +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +// Map validates that the value matches one of expected values. +func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + } + validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + v.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} diff --git a/internal/one_of_with_description_if_attribute_is_one_of_test.go b/internal/one_of_with_description_if_attribute_is_one_of_test.go new file mode 100644 index 0000000..295112e --- /dev/null +++ b/internal/one_of_with_description_if_attribute_is_one_of_test.go @@ -0,0 +1,203 @@ +package internal_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" +) + +func TestOneOfWithDescriptionIfAttributeIsOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req internal.OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest + in path.Expression + inPath path.Path + expectedValues []attr.Value + expError bool + } + + testCases := map[string]testCase{ + // If attrOther is set and the value is one of ExceptedValues the value of attrToCheck is one of Values + // This test case return an error because the value of attrOther is one of the + // expected values and the value of attrToCheck is not one of the Values + "baseString": { + req: internal.OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + ConfigValue: types.StringValue("another value"), + Path: path.Root("attrToCheck"), + PathExpression: path.MatchRoot("attrToCheck"), + Values: []internal.OneOfWithDescriptionIfAttributeIsOneOf{ + { + Value: types.StringValue("expected value"), + Description: "expected value", + }, + }, + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "attrToCheck": schema.StringAttribute{}, + "attrOther": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrToCheck": tftypes.String, + "attrOther": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrToCheck": tftypes.NewValue(tftypes.String, "another value"), + "attrOther": tftypes.NewValue(tftypes.String, "value"), + }), + }, + }, + in: path.MatchRoot("attrOther"), + inPath: path.Root("attrOther"), + expectedValues: []attr.Value{ + types.StringValue("value"), + }, + expError: true, + }, + "extendedString": { + req: internal.OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + ConfigValue: types.StringValue("another value"), + Path: path.Root("foobar").AtListIndex(0).AtName("attrToCheck"), + PathExpression: path.MatchRoot("foobar").AtListIndex(0).AtName("attrToCheck"), + Values: []internal.OneOfWithDescriptionIfAttributeIsOneOf{ + { + Value: types.StringValue("expected value"), + Description: "expected value", + }, + }, + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "foo": schema.StringAttribute{}, + "bar": schema.StringAttribute{}, + "foobar": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "attrOther": schema.StringAttribute{}, + "attrToCheck": schema.StringAttribute{}, + }, + }, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.String, + "foobar": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrOther": tftypes.String, + "attrToCheck": tftypes.String, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "foo value"), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "foobar": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrOther": tftypes.String, + "attrToCheck": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrOther": tftypes.String, + "attrToCheck": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrToCheck": tftypes.NewValue(tftypes.String, "another value"), + "attrOther": tftypes.NewValue(tftypes.String, "value"), + }), + }, + ), + }), + }, + }, + in: path.MatchRoot("foobar").AtListIndex(0).AtName("attrOther"), + inPath: path.Root("foobar").AtListIndex(0).AtName("attrOther"), + expectedValues: []attr.Value{ + types.StringValue("value"), + }, + expError: true, + }, + "baseInt64": { + req: internal.OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{ + ConfigValue: types.StringValue("another value"), + Path: path.Root("attrToCheck"), + PathExpression: path.MatchRoot("attrToCheck"), + Values: []internal.OneOfWithDescriptionIfAttributeIsOneOf{ + { + Value: types.Int64Value(20), + Description: "20 is better", + }, + }, + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "attrToCheck": schema.StringAttribute{}, + "attrOther": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrToCheck": tftypes.Number, + "attrOther": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrToCheck": tftypes.NewValue(tftypes.Number, int64(10)), + "attrOther": tftypes.NewValue(tftypes.String, "value"), + }), + }, + }, + in: path.MatchRoot("attrOther"), + inPath: path.Root("attrOther"), + expectedValues: []attr.Value{ + types.StringValue("value"), + }, + expError: true, + }, + } + + for name, test := range testCases { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + res := &internal.OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{} + + internal.OneOfWithDescriptionIfAttributeIsOneOfValidator{ + PathExpression: test.in, + ExceptedValues: test.expectedValues, + Values: test.req.Values, + }.Validate( + context.Background(), + test.req, + res, + ) + + if !test.expError && res.Diagnostics.HasError() { + t.Fatalf("expected no error, got %v", res.Diagnostics) + } + + if test.expError && !res.Diagnostics.HasError() { + t.Fatalf("expected error, got none") + } + }) + } +} diff --git a/stringvalidator/one_of_with_description_if_attribute_is_one_of.go b/stringvalidator/one_of_with_description_if_attribute_is_one_of.go new file mode 100644 index 0000000..a48b6bc --- /dev/null +++ b/stringvalidator/one_of_with_description_if_attribute_is_one_of.go @@ -0,0 +1,35 @@ +package stringvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" +) + +type OneOfWithDescriptionIfAttributeIsOneOfValues struct { + Value string + Description string +} + +// OneOfWithDescriptionIfAttributeIsOneOf checks that the String value is one of the expected values if the attribute is one of the exceptedValue. +// The description of the value is used to generate advanced +// Description and MarkdownDescription messages. +func OneOfWithDescriptionIfAttributeIsOneOf(path path.Expression, exceptedValue []attr.Value, values ...OneOfWithDescriptionIfAttributeIsOneOfValues) validator.String { + frameworkValues := make([]internal.OneOfWithDescriptionIfAttributeIsOneOf, 0, len(values)) + + for _, v := range values { + frameworkValues = append(frameworkValues, internal.OneOfWithDescriptionIfAttributeIsOneOf{ + Value: types.StringValue(v.Value), + Description: v.Description, + }) + } + + return internal.OneOfWithDescriptionIfAttributeIsOneOfValidator{ + Values: frameworkValues, + ExceptedValues: exceptedValue, + PathExpression: path, + } +}