Skip to content

Commit 9130e48

Browse files
feat(stringvalidator): add cases validator (#38)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 2c4b180 commit 9130e48

16 files changed

+1009
-3
lines changed

.changelog/37.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:feature
2+
`stringvalidator` - Add new `Cases` validator to validate string against multiple cases (Disallow Uppercase/Lowercase/Number/Space)
3+
```

docs/stringvalidator/cases.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
hide:
3+
- navigation
4+
---
5+
# `Cases`
6+
7+
!!! quote inline end "Released in v1.12.0"
8+
9+
This validator is a generic validator for checking if the string is a valid network format.
10+
11+
## How to use it
12+
13+
The validator takes a list of CasesValidatorType.
14+
15+
The list can contain one or more of the following values:
16+
17+
* `CasesDisallowUpper` - Check if the string does not contain any uppercase characters.
18+
* `CasesDisallowLower` - Check if the string does not contain any lowercase characters.
19+
* `CasesDisallowSpace`- Check if the string does not contain any space characters.
20+
* `CasesDisallowNumber` - Check if the string does not contain any number characters.
21+
22+
### Example DisallowUpper and DisallowSpace
23+
24+
The following example will check if the string does not contain any uppercase characters and does not contain any space characters.
25+
26+
```go
27+
// Schema defines the schema for the resource.
28+
func (r *xResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
29+
resp.Schema = schema.Schema{
30+
(...)
31+
"user_name": schema.StringAttribute{
32+
Optional: true,
33+
MarkdownDescription: "Username for ...",
34+
Validators: []validator.String{
35+
fstringvalidator.IsNetwork([]fstringvalidator.CasesValidatorType{
36+
fstringvalidator.CasesDisallowUpper,
37+
fstringvalidator.CasesDisallowSpace,
38+
}, true)
39+
},
40+
},
41+
```

docs/stringvalidator/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
- [`IsURN`](isurn.md) - This validator is used to check if the string is a valid URN.
3737
- [`IsUUID`](isuuid.md) - This validator is used to check if the string is a valid UUID.
3838
- [`PrefixContains`](prefixcontains.md) - This validator is used to check if the string contains prefix in the given value.
39+
- [`Cases`](cases.md) - This validator is a generic validator for checking if the string respects a case.
3940

4041
### Special
4142

docs/stringvalidator/isnetwork.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ hide:
66

77
!!! quote inline end "Released in v1.8.0"
88

9-
This validator is a generic validator for checking if the string is a valid network format.
10-
11-
Some network formats are :
9+
This validator is a generic validator for checking if the string respects a case.
1210

1311
## How to use it
1412

stringvalidator/cases.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2025 Orange
3+
* SPDX-License-Identifier: Mozilla Public License 2.0
4+
*
5+
* This software is distributed under the MPL-2.0 license.
6+
* the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/
7+
* or see the "LICENSE" file for more details.
8+
*/
9+
10+
package stringvalidator
11+
12+
import (
13+
"context"
14+
"fmt"
15+
16+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
17+
18+
casesTypes "github.com/orange-cloudavenue/terraform-plugin-framework-validators/stringvalidator/cases"
19+
)
20+
21+
var _ validator.String = casesValidator{}
22+
23+
const (
24+
CasesDisallowUpper CasesValidatorType = "disallow_upper"
25+
CasesDisallowNumber CasesValidatorType = "disallow_number"
26+
CasesDisallowSpace CasesValidatorType = "disallow_space"
27+
CasesDisallowLower CasesValidatorType = "disallow_lower"
28+
)
29+
30+
var casesTypesFunc = map[CasesValidatorType]func() validator.String{
31+
CasesDisallowUpper: casesTypes.DisallowUpper,
32+
CasesDisallowNumber: casesTypes.DisallowNumber,
33+
CasesDisallowSpace: casesTypes.DisallowSpace,
34+
CasesDisallowLower: casesTypes.DisallowLower,
35+
}
36+
37+
type CasesValidatorType string
38+
39+
type casesValidator struct {
40+
CasesTypes []CasesValidatorType
41+
}
42+
43+
// Description describes the validation in plain text formatting.
44+
func (validatorCase casesValidator) Description(_ context.Context) string {
45+
description := ""
46+
47+
if len(validatorCase.CasesTypes) == 0 {
48+
description += "invalid configuration"
49+
}
50+
51+
switch {
52+
case len(validatorCase.CasesTypes) > 1:
53+
description += "The value must respect the following rules : "
54+
case len(validatorCase.CasesTypes) == 1:
55+
description += "The value must respect the following rule : "
56+
}
57+
58+
for i, caseType := range validatorCase.CasesTypes {
59+
if i == len(validatorCase.CasesTypes)-1 {
60+
description += casesTypesFunc[caseType]().Description(context.Background())
61+
} else {
62+
description += fmt.Sprintf("%s, ", casesTypesFunc[caseType]().Description(context.Background()))
63+
}
64+
}
65+
return description
66+
}
67+
68+
// MarkdownDescription describes the validation in Markdown formatting.
69+
func (validatorCase casesValidator) MarkdownDescription(ctx context.Context) string {
70+
return validatorCase.Description(ctx)
71+
}
72+
73+
// Validate performs the validation.
74+
func (validatorCase casesValidator) ValidateString(
75+
ctx context.Context,
76+
request validator.StringRequest,
77+
response *validator.StringResponse,
78+
) {
79+
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
80+
return
81+
}
82+
83+
if len(validatorCase.CasesTypes) == 0 {
84+
response.Diagnostics.AddError(
85+
fmt.Sprintf("Invalid configuration for attribute %s", request.Path),
86+
"Set at least one case type",
87+
)
88+
return
89+
}
90+
91+
for _, caseType := range validatorCase.CasesTypes {
92+
if _, ok := casesTypesFunc[caseType]; !ok {
93+
response.Diagnostics.AddError(
94+
"Invalid case type",
95+
fmt.Sprintf("invalid case type: %s", caseType),
96+
)
97+
continue
98+
}
99+
100+
resp := new(validator.StringResponse)
101+
casesTypesFunc[caseType]().ValidateString(ctx, request, resp)
102+
103+
if resp.Diagnostics.HasError() {
104+
response.Diagnostics.Append(resp.Diagnostics...)
105+
}
106+
}
107+
}
108+
109+
// Cases returns a new string validator that checks if the string matches any of the specified case types.
110+
//
111+
// Parameters:
112+
// - casesTypes: A slice of CasesValidatorType that specifies the types of cases to validate against.
113+
//
114+
// Returns:
115+
// - validator.String: A string validator that validates the string against the specified case types.
116+
func Cases(casesTypes []CasesValidatorType) validator.String {
117+
return &casesValidator{
118+
CasesTypes: casesTypes,
119+
}
120+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2025 Orange
3+
* SPDX-License-Identifier: Mozilla Public License 2.0
4+
*
5+
* This software is distributed under the MPL-2.0 license.
6+
* the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/
7+
* or see the "LICENSE" file for more details.
8+
*/
9+
10+
package cases
11+
12+
import (
13+
"context"
14+
"fmt"
15+
"unicode"
16+
17+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
18+
)
19+
20+
type validatorDisallowLower struct{}
21+
22+
// Description describes the validation in plain text formatting.
23+
func (validator validatorDisallowLower) Description(_ context.Context) string {
24+
return "disallow lowercase characters"
25+
}
26+
27+
// MarkdownDescription describes the validation in Markdown formatting.
28+
func (validator validatorDisallowLower) MarkdownDescription(_ context.Context) string {
29+
return "disallow lowercase characters"
30+
}
31+
32+
// Validate performs the validation.
33+
func (validator validatorDisallowLower) ValidateString(
34+
_ context.Context,
35+
request validator.StringRequest,
36+
response *validator.StringResponse,
37+
) {
38+
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
39+
return
40+
}
41+
42+
for _, r := range request.ConfigValue.ValueString() {
43+
if unicode.IsLower(r) {
44+
response.Diagnostics.AddAttributeError(
45+
request.Path,
46+
"lowercase characters are not allowed",
47+
fmt.Sprintf("invalid value: %s", request.ConfigValue.ValueString()),
48+
)
49+
return
50+
}
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2025 Orange
3+
* SPDX-License-Identifier: Mozilla Public License 2.0
4+
*
5+
* This software is distributed under the MPL-2.0 license.
6+
* the text of which is available at https://www.mozilla.org/en-US/MPL/2.0/
7+
* or see the "LICENSE" file for more details.
8+
*/
9+
10+
package cases_test
11+
12+
import (
13+
"context"
14+
"testing"
15+
16+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
19+
cases "github.com/orange-cloudavenue/terraform-plugin-framework-validators/stringvalidator/cases"
20+
)
21+
22+
func TestValidDisallowLowerValidator(t *testing.T) {
23+
t.Parallel()
24+
25+
type testCase struct {
26+
val types.String
27+
expectError bool
28+
}
29+
tests := map[string]testCase{
30+
"unknown": {
31+
val: types.StringUnknown(),
32+
},
33+
"null": {
34+
val: types.StringNull(),
35+
},
36+
"valid": {
37+
val: types.StringValue("ONLYUPPER"),
38+
},
39+
"invalid-lower-lower": {
40+
val: types.StringValue("lowerAndLOWER"),
41+
expectError: true,
42+
},
43+
"invalid-lower": {
44+
val: types.StringValue("onlylower"),
45+
expectError: true,
46+
},
47+
}
48+
49+
for name, test := range tests {
50+
t.Run(name, func(t *testing.T) {
51+
t.Parallel()
52+
request := validator.StringRequest{
53+
ConfigValue: test.val,
54+
}
55+
response := validator.StringResponse{}
56+
cases.DisallowLower().ValidateString(context.TODO(), request, &response)
57+
58+
if !response.Diagnostics.HasError() && test.expectError {
59+
t.Fatal("expected error, got no error")
60+
}
61+
62+
if response.Diagnostics.HasError() && !test.expectError {
63+
t.Fatalf("got unexpected error: %s", response.Diagnostics)
64+
}
65+
})
66+
}
67+
}
68+
69+
func TestValidDisallowLowerValidatorDescription(t *testing.T) {
70+
t.Parallel()
71+
72+
type testCase struct {
73+
description string
74+
}
75+
tests := map[string]testCase{
76+
"description": {
77+
description: "disallow lowercase characters",
78+
},
79+
}
80+
81+
for name, test := range tests {
82+
t.Run(name, func(t *testing.T) {
83+
t.Parallel()
84+
validator := cases.DisallowLower()
85+
if validator.Description(context.Background()) != test.description {
86+
t.Fatalf("got unexpected description: %s != %s", validator.Description(context.Background()), test.description)
87+
}
88+
})
89+
}
90+
}
91+
92+
func TestValidDisallowLowerValidatorMarkdownDescription(t *testing.T) {
93+
t.Parallel()
94+
95+
type testCase struct {
96+
description string
97+
}
98+
tests := map[string]testCase{
99+
"description": {
100+
description: "disallow lowercase characters",
101+
},
102+
}
103+
104+
for name, test := range tests {
105+
t.Run(name, func(t *testing.T) {
106+
t.Parallel()
107+
validator := cases.DisallowLower()
108+
if validator.MarkdownDescription(context.Background()) != test.description {
109+
t.Fatalf("got unexpected description: %s != %s", validator.MarkdownDescription(context.Background()), test.description)
110+
}
111+
})
112+
}
113+
}

0 commit comments

Comments
 (0)