diff --git a/.golangci.yml b/.golangci.yml index a84ba0f..2c4b25c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,31 +1,22 @@ -# Visit https://golangci-lint.run/ for usage documentation -# and information on other useful linters -issues: - max-per-linter: 0 - max-same-issues: 0 linters: enable: - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] - bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false] - - deadcode # Finds unused code [fast: false, auto-fix: false] - decorder # check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false] - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false] - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. [fast: false, auto-fix: false] - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] - - execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds [fast: false, auto-fix: false] - - exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] + - copyloopvar # Checks for loop variables that are used in a closure [fast: true, auto-fix: false] - forbidigo # Forbids identifiers [fast: true, auto-fix: false] - gci # Gci controls golang package import order and makes it always deterministic. [fast: true, auto-fix: false] - - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - gocritic # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] - goheader # Checks is file header matches to pattern [fast: true, auto-fix: false] - gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true] - - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true] - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] - goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] @@ -33,12 +24,10 @@ linters: - gosimple #(megacheck): Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] - govet #(vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] - grouper # An analyzer to analyze expression groups. [fast: true, auto-fix: false] - - ifshort # Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false] - importas # Enforces consistent import aliases [fast: false, auto-fix: false] - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false] - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] @@ -48,40 +37,41 @@ linters: - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] - rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false] - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false] - - structcheck # Finds unused struct fields [fast: false, auto-fix: false] - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] - tagliatelle # Checks the struct tags. [fast: true, auto-fix: false] - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false] - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false] - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false] - - varcheck # Finds unused global variables and constants [fast: false, auto-fix: false] - wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] + - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false] + - godot # Check if comments end in a period [fast: true, auto-fix: true] + - unused #(megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] + - unparam # reports unused function parameters + - staticcheck #(megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] + - bodyclose # checks whether HTTP response body is closed successfully + - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] disable: - - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] - dupl # Tool for code clone detection [fast: true, auto-fix: false] + - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] + - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] + - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true] - containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false] - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] - - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] - funlen # Tool for detection of long functions [fast: true, auto-fix: false] - gochecknoglobals # check that no global variables exist [fast: true, auto-fix: false] - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] - - godot # Check if comments end in a period [fast: true, auto-fix: true] - godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] - - goerr113 # Golang linter to check the errors handling expressions [fast: false, auto-fix: false] - - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] - lll # Reports long lines [fast: true, auto-fix: false] - maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false] - nestif # Reports deeply nested if statements [fast: true, auto-fix: false] - - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false] - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] - - staticcheck #(megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] - - unused #(megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] - wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] @@ -140,6 +130,4 @@ linters-settings: allow: - $gostd - github.com/hashicorp - - github.com/FrangipaneTeam - # Packages that are not allowed where the value is a suggestion. - deny: \ No newline at end of file + - github.com/FrangipaneTeam \ No newline at end of file diff --git a/boolvalidator/require_if_attribute_is_set.go b/boolvalidator/require_if_attribute_is_set.go index 0b5dde2..4895a3b 100644 --- a/boolvalidator/require_if_attribute_is_set.go +++ b/boolvalidator/require_if_attribute_is_set.go @@ -7,7 +7,7 @@ import ( "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" ) -// RequireIfAttributeIsSet checks if the path.Path attribute is set +// RequireIfAttributeIsSet checks if the path.Path attribute is set. func RequireIfAttributeIsSet(path path.Expression) validator.Bool { return internal.RequireIfAttributeIsSet{ PathExpression: path, diff --git a/docs/common/oneofwithdescriptionifattributeisoneof.md b/docs/common/oneofwithdescriptionifattributeisoneof.md new file mode 100644 index 0000000..917d8b1 --- /dev/null +++ b/docs/common/oneofwithdescriptionifattributeisoneof.md @@ -0,0 +1,59 @@ +# `OneOfWithDescription` + +!!! quote inline end "Released in v1.9.0" + +This validator allows to check if the string is one of the values ​​of another attribute. +It also allows to format the description and markdown description accordingly + +## 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 0000000..3b63093 Binary files /dev/null and b/docs/common/oneofwithdescriptionifattributeisoneof.png differ 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/int64validator/require_if_attribute_is_set.go b/int64validator/require_if_attribute_is_set.go index b6a809b..499b4f9 100644 --- a/int64validator/require_if_attribute_is_set.go +++ b/int64validator/require_if_attribute_is_set.go @@ -7,7 +7,7 @@ import ( "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" ) -// RequireIfAttributeIsSet checks if the path.Path attribute is set +// RequireIfAttributeIsSet checks if the path.Path attribute is set. func RequireIfAttributeIsSet(path path.Expression) validator.Int64 { return internal.RequireIfAttributeIsSet{ PathExpression: path, diff --git a/internal/one_of_with_description.go b/internal/one_of_with_description.go index 593cc98..454ebd2 100644 --- a/internal/one_of_with_description.go +++ b/internal/one_of_with_description.go @@ -75,18 +75,6 @@ func (v OneOfWithDescriptionValidator) MarkdownDescription(_ context.Context) st return fmt.Sprintf("%s %s", oneOfWithDescriptionValidatorDescription, valuesDescription) } -func (v OneOfWithDescriptionValidator) valuesDescription(_ context.Context) 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) - } - return valuesDescription -} - func (v OneOfWithDescriptionValidator) Validate(ctx context.Context, req OneOfWithDescriptionValidatorRequest, res *OneOfWithDescriptionValidatorResponse) { // If attribute configuration is not null or unknown, there is nothing else to validate if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() || len(v.Values) == 0 { 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..ea0823a --- /dev/null +++ b/internal/one_of_with_description_if_attribute_is_one_of.go @@ -0,0 +1,269 @@ +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" +) + +// 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 + } + + 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..a9cee22 --- /dev/null +++ b/internal/one_of_with_description_if_attribute_is_one_of_test.go @@ -0,0 +1,199 @@ +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 + 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"), + 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"), + 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"), + 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/mapvalidator/require_if_attribute_is_set.go b/mapvalidator/require_if_attribute_is_set.go index 18a4035..b7d86af 100644 --- a/mapvalidator/require_if_attribute_is_set.go +++ b/mapvalidator/require_if_attribute_is_set.go @@ -7,7 +7,7 @@ import ( "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" ) -// RequireIfAttributeIsSet checks if the path.Path attribute is set +// RequireIfAttributeIsSet checks if the path.Path attribute is set. func RequireIfAttributeIsSet(path path.Expression) validator.Map { return internal.RequireIfAttributeIsSet{ PathExpression: path, diff --git a/setvalidator/require_if_attribute_is_set.go b/setvalidator/require_if_attribute_is_set.go index ec4f17e..13b9179 100644 --- a/setvalidator/require_if_attribute_is_set.go +++ b/setvalidator/require_if_attribute_is_set.go @@ -7,7 +7,7 @@ import ( "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" ) -// RequireIfAttributeIsSet checks if the path.Path attribute is set +// RequireIfAttributeIsSet checks if the path.Path attribute is set. func RequireIfAttributeIsSet(path path.Expression) validator.Set { return internal.RequireIfAttributeIsSet{ PathExpression: path, diff --git a/stringvalidator/networkTypes/type_ipv4_test.go b/stringvalidator/networkTypes/type_ipv4_test.go index 3c9bf3d..89e8bf2 100644 --- a/stringvalidator/networkTypes/type_ipv4_test.go +++ b/stringvalidator/networkTypes/type_ipv4_test.go @@ -63,7 +63,7 @@ func TestValidIPV4Validator(t *testing.T) { } } -// TestValidIPValidatorDescription +// TestValidIPValidatorDescription. func TestValidIPV4ValidatorDescription(t *testing.T) { t.Parallel() @@ -88,7 +88,7 @@ func TestValidIPV4ValidatorDescription(t *testing.T) { } } -// TestValidIPValidatorMarkdownDescription +// TestValidIPValidatorMarkdownDescription. func TestValidIPV4ValidatorMarkdownDescription(t *testing.T) { t.Parallel() diff --git a/stringvalidator/networkTypes/type_ipv4_with_cidr_test.go b/stringvalidator/networkTypes/type_ipv4_with_cidr_test.go index e410497..ee9cb25 100644 --- a/stringvalidator/networkTypes/type_ipv4_with_cidr_test.go +++ b/stringvalidator/networkTypes/type_ipv4_with_cidr_test.go @@ -79,7 +79,7 @@ func TestValidIPV4WithCIDRValidator(t *testing.T) { } } -// TestValidIPV4WithCIDRValidatorDescription +// TestValidIPV4WithCIDRValidatorDescription. func TestValidIPV4WithCIDRValidatorDescription(t *testing.T) { t.Parallel() @@ -104,7 +104,7 @@ func TestValidIPV4WithCIDRValidatorDescription(t *testing.T) { } } -// TestValidIPV4WithCIDRValidatorMarkdownDescription +// TestValidIPV4WithCIDRValidatorMarkdownDescription. func TestValidIPV4WithCIDRValidatorMarkdownDescription(t *testing.T) { t.Parallel() diff --git a/stringvalidator/networkTypes/type_ipv4_with_netmask_test.go b/stringvalidator/networkTypes/type_ipv4_with_netmask_test.go index 31b9050..e48d7c0 100644 --- a/stringvalidator/networkTypes/type_ipv4_with_netmask_test.go +++ b/stringvalidator/networkTypes/type_ipv4_with_netmask_test.go @@ -79,7 +79,7 @@ func TestValidIPV4WithNetmaskValidator(t *testing.T) { } } -// TestValidIPV4WithNetmaskValidatorDescription +// TestValidIPV4WithNetmaskValidatorDescription. func TestValidIPV4WithNetmaskValidatorDescription(t *testing.T) { t.Parallel() @@ -104,7 +104,7 @@ func TestValidIPV4WithNetmaskValidatorDescription(t *testing.T) { } } -// TestValidIPV4WithNetmaskValidatorMarkdownDescription +// TestValidIPV4WithNetmaskValidatorMarkdownDescription. func TestValidIPV4WithNetmaskValidatorMarkdownDescription(t *testing.T) { t.Parallel() diff --git a/stringvalidator/networkTypes/type_rfc1918_test.go b/stringvalidator/networkTypes/type_rfc1918_test.go index d8a7d2c..c1244f3 100644 --- a/stringvalidator/networkTypes/type_rfc1918_test.go +++ b/stringvalidator/networkTypes/type_rfc1918_test.go @@ -73,7 +73,7 @@ func TestValidRFC1918Validator(t *testing.T) { } } -// TestValidRFC1918ValidatorDescription +// TestValidRFC1918ValidatorDescription. func TestValidRFC1918ValidatorDescription(t *testing.T) { t.Parallel() @@ -98,7 +98,7 @@ func TestValidRFC1918ValidatorDescription(t *testing.T) { } } -// TestValidRFC1918ValidatorMarkdownDescription +// TestValidRFC1918ValidatorMarkdownDescription. func TestValidRFC1918ValidatorMarkdownDescription(t *testing.T) { t.Parallel() 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, + } +} diff --git a/stringvalidator/require_if_attribute_is_set.go b/stringvalidator/require_if_attribute_is_set.go index 910dc30..a904156 100644 --- a/stringvalidator/require_if_attribute_is_set.go +++ b/stringvalidator/require_if_attribute_is_set.go @@ -7,7 +7,7 @@ import ( "github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal" ) -// RequireIfAttributeIsSet checks if the path.Path attribute is set +// RequireIfAttributeIsSet checks if the path.Path attribute is set. func RequireIfAttributeIsSet(path path.Expression) validator.String { return internal.RequireIfAttributeIsSet{ PathExpression: path,