Skip to content
This repository has been archived by the owner on Dec 27, 2024. It is now read-only.

Commit

Permalink
feat: string/int64 validator OneOfWithDescriptionIfAttributeIsOneOf
Browse files Browse the repository at this point in the history
  • Loading branch information
azrod committed Dec 20, 2024
1 parent ccc6199 commit 86f35a3
Show file tree
Hide file tree
Showing 8 changed files with 606 additions and 0 deletions.
58 changes: 58 additions & 0 deletions docs/common/oneofwithdescriptionifattributeisoneof.md
Original file line number Diff line number Diff line change
@@ -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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/int64validator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/stringvalidator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 35 additions & 0 deletions int64validator/one_of_with_description_if_attribute_is_one_of.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
273 changes: 273 additions & 0 deletions internal/one_of_with_description_if_attribute_is_one_of.go
Original file line number Diff line number Diff line change
@@ -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<br>", 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...)
}
Loading

0 comments on commit 86f35a3

Please sign in to comment.