Skip to content

Commit 655c307

Browse files
Generate validation patterns for enums (#3277)
## Changes This PR generates a map containing a mapping of fields to their acceptable values. A follow-up PR will use this map to perform actual validation. ## Tests Tested end to end validation using these patterns in: #3279
1 parent f8321b7 commit 655c307

File tree

7 files changed

+415
-12
lines changed

7 files changed

+415
-12
lines changed

.codegen.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"echo 'bundle/internal/tf/schema/\\*.go linguist-generated=true' >> ./.gitattributes",
2828
"echo 'go.sum linguist-generated=true' >> ./.gitattributes",
2929
"echo 'bundle/schema/jsonschema.json linguist-generated=true' >> ./.gitattributes",
30-
"echo 'bundle/internal/validation/generated/required_fields.go linguist-generated=true' >> ./.gitattributes"
3130
]
3231
}
3332
}

bundle/internal/validation/enum.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"reflect"
10+
"sort"
11+
"text/template"
12+
13+
"github.com/databricks/cli/bundle/config"
14+
"github.com/databricks/cli/libs/structdiff/structpath"
15+
"github.com/databricks/cli/libs/structwalk"
16+
)
17+
18+
type EnumPatternInfo struct {
19+
// The pattern for which the enum values in Values are applicable.
20+
// This is a string representation of [dyn.Pattern].
21+
Pattern string
22+
23+
// List of valid enum values for the pattern. This field will be a string of the
24+
// form `{value1, value2, ...}` representing a Go slice literal.
25+
Values string
26+
}
27+
28+
// isEnumType checks if a type is an enum (string type with a Values() method)
29+
func isEnumType(typ reflect.Type) bool {
30+
// Must be a string type
31+
if typ.Kind() != reflect.String {
32+
return false
33+
}
34+
35+
// Check for Values() method on both the type and its pointer type
36+
var valuesMethod reflect.Method
37+
var hasValues bool
38+
39+
// First check on the type itself
40+
valuesMethod, hasValues = typ.MethodByName("Values")
41+
if !hasValues {
42+
// Then check on the pointer type
43+
ptrType := reflect.PointerTo(typ)
44+
valuesMethod, hasValues = ptrType.MethodByName("Values")
45+
}
46+
47+
if !hasValues {
48+
return false
49+
}
50+
51+
// Check method signature: func (T) Values() []T or func (*T) Values() []T
52+
methodType := valuesMethod.Type
53+
if methodType.NumIn() != 1 || methodType.NumOut() != 1 {
54+
return false
55+
}
56+
57+
// Check return type is slice of the enum type
58+
returnType := methodType.Out(0)
59+
if returnType.Kind() != reflect.Slice {
60+
return false
61+
}
62+
63+
elementType := returnType.Elem()
64+
return elementType == typ
65+
}
66+
67+
// getEnumValues extracts enum values by calling the Values() method
68+
func getEnumValues(typ reflect.Type) ([]string, error) {
69+
if !isEnumType(typ) {
70+
return nil, fmt.Errorf("type %s is not an enum type", typ.Name())
71+
}
72+
73+
// Create a zero value of the type
74+
enumValue := reflect.Zero(typ)
75+
76+
// Check if the method is on the type or pointer type
77+
valuesMethod := enumValue.MethodByName("Values")
78+
if !valuesMethod.IsValid() {
79+
// Try on pointer type
80+
enumPtr := reflect.New(typ)
81+
enumPtr.Elem().Set(enumValue)
82+
valuesMethod = enumPtr.MethodByName("Values")
83+
if !valuesMethod.IsValid() {
84+
return nil, fmt.Errorf("Values() method not found on type %s", typ.Name())
85+
}
86+
}
87+
88+
results := valuesMethod.Call(nil)
89+
if len(results) != 1 {
90+
return nil, errors.New("Values() method should return exactly one value")
91+
}
92+
93+
valuesSlice := results[0]
94+
if valuesSlice.Kind() != reflect.Slice {
95+
return nil, errors.New("Values() method should return a slice")
96+
}
97+
98+
// Extract string values from the slice
99+
var enumStrings []string
100+
for i := range valuesSlice.Len() {
101+
value := valuesSlice.Index(i)
102+
enumStrings = append(enumStrings, value.String())
103+
}
104+
105+
return enumStrings, nil
106+
}
107+
108+
// extractEnumFields walks through a struct type and extracts enum field patterns
109+
func extractEnumFields(typ reflect.Type) ([]EnumPatternInfo, error) {
110+
fieldsByPattern := make(map[string][]string)
111+
112+
err := structwalk.WalkType(typ, func(path *structpath.PathNode, fieldType reflect.Type) bool {
113+
if path == nil {
114+
return true
115+
}
116+
117+
// Do not generate enum validation code for fields that are internal or readonly.
118+
bundleTag := path.BundleTag()
119+
if bundleTag.Internal() || bundleTag.ReadOnly() {
120+
return false
121+
}
122+
123+
// Check if this field type is an enum
124+
if isEnumType(fieldType) {
125+
// Get enum values
126+
enumValues, err := getEnumValues(fieldType)
127+
if err != nil {
128+
// Skip this field if we can't get enum values
129+
return true
130+
}
131+
132+
fieldPath := path.DynPath()
133+
fieldsByPattern[fieldPath] = enumValues
134+
}
135+
return true
136+
})
137+
138+
return buildEnumPatternInfos(fieldsByPattern), err
139+
}
140+
141+
// buildEnumPatternInfos converts the field map to EnumPatternInfo slice
142+
func buildEnumPatternInfos(fieldsByPattern map[string][]string) []EnumPatternInfo {
143+
patterns := make([]EnumPatternInfo, 0, len(fieldsByPattern))
144+
145+
for pattern, values := range fieldsByPattern {
146+
patterns = append(patterns, EnumPatternInfo{
147+
Pattern: pattern,
148+
Values: formatSliceToString(values),
149+
})
150+
}
151+
152+
return patterns
153+
}
154+
155+
// groupPatternsByKey groups patterns by their logical grouping key
156+
func groupPatternsByKeyEnum(patterns []EnumPatternInfo) map[string][]EnumPatternInfo {
157+
groupedPatterns := make(map[string][]EnumPatternInfo)
158+
159+
for _, pattern := range patterns {
160+
key := getGroupingKey(pattern.Pattern)
161+
groupedPatterns[key] = append(groupedPatterns[key], pattern)
162+
}
163+
164+
return groupedPatterns
165+
}
166+
167+
func filterTargetsAndEnvironmentsEnum(patterns map[string][]EnumPatternInfo) map[string][]EnumPatternInfo {
168+
filtered := make(map[string][]EnumPatternInfo)
169+
for key, patterns := range patterns {
170+
if key == "targets" || key == "environments" {
171+
continue
172+
}
173+
filtered[key] = patterns
174+
}
175+
return filtered
176+
}
177+
178+
// sortGroupedPatterns sorts patterns within each group and returns them as a sorted slice
179+
func sortGroupedPatternsEnum(groupedPatterns map[string][]EnumPatternInfo) [][]EnumPatternInfo {
180+
// Get sorted group keys
181+
groupKeys := make([]string, 0, len(groupedPatterns))
182+
for key := range groupedPatterns {
183+
groupKeys = append(groupKeys, key)
184+
}
185+
sort.Strings(groupKeys)
186+
187+
// Build sorted result
188+
result := make([][]EnumPatternInfo, 0, len(groupKeys))
189+
for _, key := range groupKeys {
190+
patterns := groupedPatterns[key]
191+
192+
// Sort patterns within each group by pattern
193+
sort.Slice(patterns, func(i, j int) bool {
194+
return patterns[i].Pattern < patterns[j].Pattern
195+
})
196+
197+
result = append(result, patterns)
198+
}
199+
200+
return result
201+
}
202+
203+
// enumFields returns grouped enum field patterns for validation
204+
func enumFields() ([][]EnumPatternInfo, error) {
205+
patterns, err := extractEnumFields(reflect.TypeOf(config.Root{}))
206+
if err != nil {
207+
return nil, err
208+
}
209+
groupedPatterns := groupPatternsByKeyEnum(patterns)
210+
filteredPatterns := filterTargetsAndEnvironmentsEnum(groupedPatterns)
211+
return sortGroupedPatternsEnum(filteredPatterns), nil
212+
}
213+
214+
// Generate creates a Go source file with enum field validation rules
215+
func generateEnumFields(outPath string) error {
216+
enumFields, err := enumFields()
217+
if err != nil {
218+
return fmt.Errorf("failed to generate enum fields: %w", err)
219+
}
220+
221+
// Ensure output directory exists
222+
if err := os.MkdirAll(outPath, 0o755); err != nil {
223+
return fmt.Errorf("failed to create output directory: %w", err)
224+
}
225+
226+
// Parse and execute template
227+
tmpl, err := template.New("enum_validation").Parse(enumValidationTemplate)
228+
if err != nil {
229+
return fmt.Errorf("failed to parse template: %w", err)
230+
}
231+
232+
var generatedCode bytes.Buffer
233+
if err := tmpl.Execute(&generatedCode, enumFields); err != nil {
234+
return fmt.Errorf("failed to execute template: %w", err)
235+
}
236+
237+
// Write generated code to file
238+
filePath := filepath.Join(outPath, "enum_fields.go")
239+
if err := os.WriteFile(filePath, generatedCode.Bytes(), 0o644); err != nil {
240+
return fmt.Errorf("failed to write generated code: %w", err)
241+
}
242+
243+
return nil
244+
}
245+
246+
// enumValidationTemplate is the Go text template for generating the enum validation map
247+
const enumValidationTemplate = `package generated
248+
249+
// THIS FILE IS AUTOGENERATED.
250+
// DO NOT EDIT THIS FILE DIRECTLY.
251+
252+
import (
253+
_ "github.com/databricks/cli/libs/dyn"
254+
)
255+
256+
// EnumFields maps [dyn.Pattern] to valid enum values they should have.
257+
var EnumFields = map[string][]string{
258+
{{- range . }}
259+
{{- range . }}
260+
"{{ .Pattern }}": {{ .Values }},
261+
{{- end }}
262+
{{ end -}}
263+
}
264+
`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.go linguist-generated=true

0 commit comments

Comments
 (0)