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

Commit 86f35a3

Browse files
committed
feat: string/int64 validator OneOfWithDescriptionIfAttributeIsOneOf
1 parent ccc6199 commit 86f35a3

8 files changed

+606
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# `OneOfWithDescription`
2+
3+
!!! quote inline end "Released in v1.9.0"
4+
5+
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.
6+
7+
## How to use it
8+
9+
```go
10+
// Schema defines the schema for the resource.
11+
func (r *xResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
12+
resp.Schema = schema.Schema{
13+
(...)
14+
"foo": schema.StringAttribute{
15+
Optional: true,
16+
MarkdownDescription: "foo ...",
17+
Validators: []validator.String{
18+
fstringvalidator.OneOf("VM_NAME", "VM_TAGS"),
19+
},
20+
},
21+
"bar": schema.StringAttribute{
22+
Optional: true,
23+
MarkdownDescription: "bar of ...",
24+
Validators: []validator.String{
25+
fstringvalidator.OneOfWithDescriptionIfAttributeIsOneOf(
26+
path.MatchRelative().AtParent().AtName("foo"),
27+
[]attr.Value{types.StringValue("VM_NAME")},
28+
func() []fstringvalidator.OneOfWithDescriptionIfAttributeIsOneOfValues {
29+
return []fstringvalidator.OneOfWithDescriptionIfAttributeIsOneOfValues{
30+
{
31+
Value: "CONTAINS",
32+
Description: "The `value` must be contained in the VM name.",
33+
},
34+
{
35+
Value: "STARTS_WITH",
36+
Description: "The VM name must start with the `value`.",
37+
},
38+
{
39+
Value: "ENDS_WITH",
40+
Description: "The VM name must end with the `value`.",
41+
},
42+
{
43+
Value: "EQUALS",
44+
Description: "The VM name must be equal to the `value`.",
45+
},
46+
}
47+
}()...),
48+
},
49+
},
50+
```
51+
52+
## Description and Markdown description
53+
54+
* **Description:**
55+
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`.)
56+
* **Markdown description:**
57+
58+
![oneofwithdescriptionifattributeisoneof](oneofwithdescriptionifattributeisoneof.png)
34 KB
Loading

docs/int64validator/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
- [`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.
1919
- [`NullIfAttributeIsSet`](../common/null_if_attribute_is_set.md) - This validator is used to verify the attribute value is null if another attribute is set.
2020
- [`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.
21+
- [`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.
2122
- [`AttributeIsDivisibleByAnInteger`](attribute_is_divisible_by_an_integer.md) - This validator is used to validate that the attribute is divisible by an integer.
2223
- [`ZeroRemainder`](zero_remainder.md) - This validator checks if the configured attribute is divisible by a specified integer X, and has zero remainder.
2324

docs/stringvalidator/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
- [`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.
1919
- [`NullIfAttributeIsSet`](../common/null_if_attribute_is_set.md) - This validator is used to verify the attribute value is null if another attribute is set.
2020
- [`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.
21+
- [`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.
2122

2223
### Network
2324

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package int64validator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/attr"
5+
"github.com/hashicorp/terraform-plugin-framework/path"
6+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
7+
"github.com/hashicorp/terraform-plugin-framework/types"
8+
9+
"github.com/FrangipaneTeam/terraform-plugin-framework-validators/internal"
10+
)
11+
12+
type OneOfWithDescriptionIfAttributeIsOneOfValues struct {
13+
Value int64
14+
Description string
15+
}
16+
17+
// OneOfWithDescriptionIfAttributeIsOneOf checks that the value is one of the expected values if the attribute is one of the exceptedValue.
18+
// The description of the value is used to generate advanced
19+
// Description and MarkdownDescription messages.
20+
func OneOfWithDescriptionIfAttributeIsOneOf(path path.Expression, exceptedValue []attr.Value, values ...OneOfWithDescriptionIfAttributeIsOneOfValues) validator.String {
21+
frameworkValues := make([]internal.OneOfWithDescriptionIfAttributeIsOneOf, 0, len(values))
22+
23+
for _, v := range values {
24+
frameworkValues = append(frameworkValues, internal.OneOfWithDescriptionIfAttributeIsOneOf{
25+
Value: types.Int64Value(v.Value),
26+
Description: v.Description,
27+
})
28+
}
29+
30+
return internal.OneOfWithDescriptionIfAttributeIsOneOfValidator{
31+
Values: frameworkValues,
32+
ExceptedValues: exceptedValue,
33+
PathExpression: path,
34+
}
35+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
12+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
13+
)
14+
15+
const oneOfWithDescriptionIfAttributeIsOneOfValidatorDescription = "Value must be one of:"
16+
17+
// This type of validator must satisfy all types.
18+
var (
19+
_ validator.Float64 = OneOfWithDescriptionValidator{}
20+
_ validator.Int64 = OneOfWithDescriptionValidator{}
21+
_ validator.List = OneOfWithDescriptionValidator{}
22+
_ validator.Map = OneOfWithDescriptionValidator{}
23+
_ validator.Number = OneOfWithDescriptionValidator{}
24+
_ validator.Set = OneOfWithDescriptionValidator{}
25+
_ validator.String = OneOfWithDescriptionValidator{}
26+
)
27+
28+
type OneOfWithDescriptionIfAttributeIsOneOf struct {
29+
Value attr.Value
30+
Description string
31+
}
32+
33+
// OneOfWithDescriptionValidator validates that the value matches one of expected values.
34+
type OneOfWithDescriptionIfAttributeIsOneOfValidator struct {
35+
PathExpression path.Expression
36+
Values []OneOfWithDescriptionIfAttributeIsOneOf
37+
ExceptedValues []attr.Value
38+
}
39+
40+
type OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest struct {
41+
Config tfsdk.Config
42+
ConfigValue attr.Value
43+
Path path.Path
44+
PathExpression path.Expression
45+
Values []OneOfWithDescriptionIfAttributeIsOneOf
46+
ExceptedValues []attr.Value
47+
}
48+
49+
type OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse struct {
50+
Diagnostics diag.Diagnostics
51+
}
52+
53+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) Description(_ context.Context) string {
54+
var expectedValueDescritpion string
55+
for i, expectedValue := range v.ExceptedValues {
56+
// remove the quotes around the string
57+
if i == len(v.ExceptedValues)-1 {
58+
expectedValueDescritpion += expectedValue.String()
59+
break
60+
}
61+
expectedValueDescritpion += fmt.Sprintf("%s, ", expectedValue.String())
62+
}
63+
64+
var valuesDescription string
65+
for i, value := range v.Values {
66+
if i == len(v.Values)-1 {
67+
valuesDescription += fmt.Sprintf("%s (%s)", value.Value.String(), value.Description)
68+
break
69+
}
70+
valuesDescription += fmt.Sprintf("%s (%s), ", value.Value.String(), value.Description)
71+
}
72+
73+
switch len(v.ExceptedValues) {
74+
case 1:
75+
return fmt.Sprintf("If the value of attribute %s is %s the allowed values are : %s", v.PathExpression.String(), expectedValueDescritpion, valuesDescription)
76+
default:
77+
return fmt.Sprintf("If the value of attribute %s is one of %s the allowed are : %s", v.PathExpression.String(), expectedValueDescritpion, valuesDescription)
78+
}
79+
}
80+
81+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) MarkdownDescription(_ context.Context) string {
82+
var expectedValueDescritpion string
83+
for i, expectedValue := range v.ExceptedValues {
84+
// remove the quotes around the string
85+
x := strings.Trim(expectedValue.String(), "\"")
86+
87+
switch i {
88+
case len(v.ExceptedValues) - 1:
89+
expectedValueDescritpion += fmt.Sprintf("`%s`", x)
90+
case len(v.ExceptedValues) - 2:
91+
expectedValueDescritpion += fmt.Sprintf("`%s` or ", x)
92+
default:
93+
expectedValueDescritpion += fmt.Sprintf("`%s`, ", x)
94+
}
95+
}
96+
97+
valuesDescription := ""
98+
for _, value := range v.Values {
99+
valuesDescription += fmt.Sprintf("- `%s` - %s<br>", value.Value.String(), value.Description)
100+
}
101+
102+
switch len(v.ExceptedValues) {
103+
case 1:
104+
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)
105+
default:
106+
return fmt.Sprintf("\n\n-> **If the value of the attribute [`%s`](#%s) is one of %s** : %s", v.PathExpression, v.PathExpression, expectedValueDescritpion, valuesDescription)
107+
}
108+
}
109+
110+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) Validate(ctx context.Context, req OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest, res *OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse) {
111+
// Here attribute configuration is null or unknown, so we need to check if attribute in the path
112+
// is equal to one of the excepted values
113+
paths, diags := req.Config.PathMatches(ctx, req.PathExpression.Merge(v.PathExpression))
114+
if diags.HasError() {
115+
res.Diagnostics.Append(diags...)
116+
return
117+
}
118+
119+
if len(paths) == 0 {
120+
res.Diagnostics.AddError(
121+
fmt.Sprintf("Invalid configuration for attribute %s", req.Path),
122+
"Path must be set",
123+
)
124+
return
125+
}
126+
127+
res.Diagnostics.AddWarning("Paths", fmt.Sprintf("%v", paths))
128+
129+
path := paths[0]
130+
131+
// mpVal is the value of the attribute in the path
132+
var mpVal attr.Value
133+
res.Diagnostics.Append(req.Config.GetAttribute(ctx, path, &mpVal)...)
134+
if res.Diagnostics.HasError() {
135+
res.Diagnostics.AddError(
136+
fmt.Sprintf("Invalid configuration for attribute %s", req.Path),
137+
fmt.Sprintf("Unable to retrieve attribute path: %q", path),
138+
)
139+
return
140+
}
141+
142+
// If the target attribute configuration is unknown or null, there is nothing else to validate
143+
if mpVal.IsNull() || mpVal.IsUnknown() {
144+
return
145+
}
146+
147+
for _, expectedValue := range v.ExceptedValues {
148+
// 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
149+
if mpVal.Equal(expectedValue) || mpVal.String() == expectedValue.String() {
150+
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
151+
res.Diagnostics.AddAttributeError(
152+
path,
153+
fmt.Sprintf("Invalid configuration for attribute %s", req.Path),
154+
fmt.Sprintf("Value is empty. %s", v.Description(ctx)),
155+
)
156+
return
157+
}
158+
159+
for _, value := range v.Values {
160+
if req.ConfigValue.Equal(value.Value) {
161+
// Ok the value is valid
162+
return
163+
}
164+
}
165+
166+
// The value is not valid
167+
res.Diagnostics.AddAttributeError(
168+
path,
169+
fmt.Sprintf("Invalid configuration for attribute %s", req.Path),
170+
fmt.Sprintf("Invalid value %s. %s", req.ConfigValue.String(), v.Description(ctx)),
171+
)
172+
return
173+
}
174+
}
175+
}
176+
177+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
178+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
179+
Config: req.Config,
180+
ConfigValue: req.ConfigValue,
181+
Path: req.Path,
182+
PathExpression: req.PathExpression,
183+
}
184+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
185+
186+
v.Validate(ctx, validateReq, validateResp)
187+
188+
resp.Diagnostics.Append(validateResp.Diagnostics...)
189+
}
190+
191+
// Float64 validates that the value matches one of expected values.
192+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateFloat64(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) {
193+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
194+
Config: req.Config,
195+
ConfigValue: req.ConfigValue,
196+
Path: req.Path,
197+
}
198+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
199+
200+
v.Validate(ctx, validateReq, validateResp)
201+
202+
resp.Diagnostics.Append(validateResp.Diagnostics...)
203+
}
204+
205+
// Int64 validates that the value matches one of expected values.
206+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) {
207+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
208+
Config: req.Config,
209+
ConfigValue: req.ConfigValue,
210+
Path: req.Path,
211+
}
212+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
213+
214+
v.Validate(ctx, validateReq, validateResp)
215+
216+
resp.Diagnostics.Append(validateResp.Diagnostics...)
217+
}
218+
219+
// Number validates that the value matches one of expected values.
220+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateNumber(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) {
221+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
222+
Config: req.Config,
223+
ConfigValue: req.ConfigValue,
224+
Path: req.Path,
225+
}
226+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
227+
228+
v.Validate(ctx, validateReq, validateResp)
229+
230+
resp.Diagnostics.Append(validateResp.Diagnostics...)
231+
}
232+
233+
// List validates that the value matches one of expected values.
234+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
235+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
236+
Config: req.Config,
237+
ConfigValue: req.ConfigValue,
238+
Path: req.Path,
239+
}
240+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
241+
242+
v.Validate(ctx, validateReq, validateResp)
243+
244+
resp.Diagnostics.Append(validateResp.Diagnostics...)
245+
}
246+
247+
// Set validates that the value matches one of expected values.
248+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) {
249+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
250+
Config: req.Config,
251+
ConfigValue: req.ConfigValue,
252+
Path: req.Path,
253+
}
254+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
255+
256+
v.Validate(ctx, validateReq, validateResp)
257+
258+
resp.Diagnostics.Append(validateResp.Diagnostics...)
259+
}
260+
261+
// Map validates that the value matches one of expected values.
262+
func (v OneOfWithDescriptionIfAttributeIsOneOfValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) {
263+
validateReq := OneOfWithDescriptionIfAttributeIsOneOfValidatorRequest{
264+
Config: req.Config,
265+
ConfigValue: req.ConfigValue,
266+
Path: req.Path,
267+
}
268+
validateResp := &OneOfWithDescriptionIfAttributeIsOneOfValidatorResponse{}
269+
270+
v.Validate(ctx, validateReq, validateResp)
271+
272+
resp.Diagnostics.Append(validateResp.Diagnostics...)
273+
}

0 commit comments

Comments
 (0)