Skip to content

Commit b39e80a

Browse files
feat: Add Automatic Validator Value Documentation (#6)
* feat: automatically append `stringvalidator.OneOf` values to string attribute descriptions. * feat: `description_validator` now correctly parses `stringvalidator.OneOf` arguments with commas and appends possible values to string attribute descriptions. * refactor: Replace regex-based parsing of `stringvalidator.OneOf` with `go/ast` for robust argument extraction.
1 parent de2ee7d commit b39e80a

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package convert
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/parser"
7+
"go/token"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
// AppendValidators parses stringvalidator.OneOf validators from the provided Validators object
13+
// and appends the possible values to the Description.
14+
//
15+
// It uses go/parser to safely extract values from the schema definition, handling
16+
// escaped quotes, commas, and other Go syntax correctly.
17+
func (d *Description) AppendValidators(v Validators) {
18+
if d.description == nil {
19+
empty := ""
20+
d.description = &empty
21+
}
22+
23+
for _, custom := range v.custom {
24+
if custom.SchemaDefinition == "" {
25+
continue
26+
}
27+
28+
// Parse the expression
29+
expr, err := parser.ParseExpr(custom.SchemaDefinition)
30+
if err != nil {
31+
// If we can't parse it, we can't extract values safely.
32+
// Just skip description augmentation.
33+
continue
34+
}
35+
36+
// Inspect the AST to find OneOf calls
37+
ast.Inspect(expr, func(n ast.Node) bool {
38+
// We look for function calls
39+
call, ok := n.(*ast.CallExpr)
40+
if !ok {
41+
return true
42+
}
43+
44+
// Check if function is stringvalidator.OneOf
45+
// This could be a SelectorExpr (pkg.Func)
46+
sel, ok := call.Fun.(*ast.SelectorExpr)
47+
if !ok {
48+
return true
49+
}
50+
51+
// Check package name
52+
id, ok := sel.X.(*ast.Ident)
53+
if !ok || id.Name != "stringvalidator" {
54+
return true
55+
}
56+
57+
// Check function name
58+
if sel.Sel.Name != "OneOf" {
59+
return true
60+
}
61+
62+
// Extract arguments
63+
var values []string
64+
for _, arg := range call.Args {
65+
// We expect string literals
66+
lit, ok := arg.(*ast.BasicLit)
67+
if !ok || lit.Kind != token.STRING {
68+
continue
69+
}
70+
71+
// Unquote the string value
72+
val, err := strconv.Unquote(lit.Value)
73+
if err != nil {
74+
continue
75+
}
76+
77+
values = append(values, fmt.Sprintf("`%s`", val))
78+
}
79+
80+
if len(values) > 0 {
81+
suffix := fmt.Sprintf("Possible values: %s", strings.Join(values, ", "))
82+
83+
// Avoid appending if already present
84+
if strings.Contains(*d.description, suffix) {
85+
return true
86+
}
87+
88+
if *d.description != "" {
89+
*d.description += "\n"
90+
}
91+
*d.description += suffix
92+
}
93+
94+
return true
95+
})
96+
}
97+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package convert
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
specschema "github.com/hashicorp/terraform-plugin-codegen-spec/schema"
8+
)
9+
10+
func TestDescription_AppendValidators(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
initialDesc *string
14+
validators Validators
15+
expectedDesc string
16+
}{
17+
{
18+
name: "Empty description, simple OneOf",
19+
initialDesc: nil,
20+
validators: NewValidators(ValidatorTypeString, specschema.CustomValidators{
21+
{
22+
SchemaDefinition: `stringvalidator.OneOf("a", "b")`,
23+
},
24+
}),
25+
expectedDesc: "Possible values: `a`, `b`",
26+
},
27+
{
28+
name: "Existing description, simple OneOf",
29+
initialDesc: stringPtr("Some description."),
30+
validators: NewValidators(ValidatorTypeString, specschema.CustomValidators{
31+
{
32+
SchemaDefinition: `stringvalidator.OneOf("foo", "bar")`,
33+
},
34+
}),
35+
expectedDesc: "Some description.\nPossible values: `foo`, `bar`",
36+
},
37+
{
38+
name: "Multiline OneOf",
39+
initialDesc: stringPtr("Desc"),
40+
validators: NewValidators(ValidatorTypeString, specschema.CustomValidators{
41+
{
42+
SchemaDefinition: `stringvalidator.OneOf(
43+
"val1",
44+
"val2",
45+
)`,
46+
},
47+
}),
48+
expectedDesc: "Desc\nPossible values: `val1`, `val2`",
49+
},
50+
{
51+
name: "Commas inside quotes",
52+
initialDesc: stringPtr("Desc"),
53+
validators: NewValidators(ValidatorTypeString, specschema.CustomValidators{
54+
{
55+
SchemaDefinition: `stringvalidator.OneOf("a,b", "c")`,
56+
},
57+
}),
58+
expectedDesc: "Desc\nPossible values: `a,b`, `c`",
59+
},
60+
{
61+
name: "No OneOf",
62+
initialDesc: stringPtr("Desc"),
63+
validators: NewValidators(ValidatorTypeString, specschema.CustomValidators{
64+
{
65+
SchemaDefinition: `stringvalidator.LengthAtLeast(1)`,
66+
},
67+
}),
68+
expectedDesc: "Desc",
69+
},
70+
{
71+
name: "Multiple OneOf (should append both)",
72+
initialDesc: stringPtr("Desc"),
73+
validators: NewValidators(ValidatorTypeString, specschema.CustomValidators{
74+
{
75+
SchemaDefinition: `stringvalidator.OneOf("a", "b")`,
76+
},
77+
{
78+
// This case is artificial, usually there's only one OneOf, but good to test behavior
79+
SchemaDefinition: `stringvalidator.OneOf("c", "d")`,
80+
},
81+
}),
82+
expectedDesc: "Desc\nPossible values: `a`, `b`\nPossible values: `c`, `d`",
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
d := NewDescription(tt.initialDesc)
89+
d.AppendValidators(tt.validators)
90+
91+
got := d.Description()
92+
if diff := cmp.Diff(tt.expectedDesc, got); diff != "" {
93+
t.Errorf("AppendValidators() mismatch (-want +got):\n%s", diff)
94+
}
95+
})
96+
}
97+
}
98+
99+
func stringPtr(s string) *string {
100+
return &s
101+
}

internal/datasource/string_attribute.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func NewGeneratorStringAttribute(name string, a *datasource.StringAttribute) (Ge
4141

4242
v := convert.NewValidators(convert.ValidatorTypeString, a.Validators.CustomValidators())
4343

44+
d.AppendValidators(v)
45+
4446
return GeneratorStringAttribute{
4547
AssociatedExternalType: schema.NewAssocExtType(a.AssociatedExternalType),
4648
ComputedOptionalRequired: c,

internal/provider/string_attribute.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func NewGeneratorStringAttribute(name string, a *provider.StringAttribute) (Gene
4141

4242
v := convert.NewValidators(convert.ValidatorTypeString, a.Validators.CustomValidators())
4343

44+
d.AppendValidators(v)
45+
4446
return GeneratorStringAttribute{
4547
AssociatedExternalType: schema.NewAssocExtType(a.AssociatedExternalType),
4648
OptionalRequired: c,

internal/resource/string_attribute.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ func NewGeneratorStringAttribute(name string, a *resource.StringAttribute) (Gene
4747

4848
v := convert.NewValidators(convert.ValidatorTypeString, a.Validators.CustomValidators())
4949

50+
d.AppendValidators(v)
51+
5052
return GeneratorStringAttribute{
5153
AssociatedExternalType: generatorschema.NewAssocExtType(a.AssociatedExternalType),
5254
ComputedOptionalRequired: c,

0 commit comments

Comments
 (0)