databricks-cli/bundle/internal/tf/codegen/generator/walker.go

151 lines
3.5 KiB
Go

package generator
import (
"fmt"
"strings"
tfjson "github.com/hashicorp/terraform-json"
"github.com/iancoleman/strcase"
"github.com/zclconf/go-cty/cty"
"golang.org/x/exp/slices"
)
type field struct {
Name string
Type string
Tag string
}
type structType struct {
Name string
Fields []field
}
// walker represents the set of types to declare to
// represent a [tfjson.SchemaBlock] as Go structs.
// See the [walk] function for usage.
type walker struct {
StructTypes []structType
}
func processAttributeType(typ cty.Type) string {
var out string
switch {
case typ.IsPrimitiveType():
switch {
case typ.Equals(cty.Bool):
out = "bool"
case typ.Equals(cty.Number):
out = "int"
case typ.Equals(cty.String):
out = "string"
default:
panic("No idea what to do for: " + typ.FriendlyName())
}
case typ.IsMapType():
out = "map[string]" + processAttributeType(*typ.MapElementType())
case typ.IsSetType():
out = "[]" + processAttributeType(*typ.SetElementType())
case typ.IsListType():
out = "[]" + processAttributeType(*typ.ListElementType())
case typ.IsObjectType():
out = "any"
default:
panic("No idea what to do for: " + typ.FriendlyName())
}
return out
}
func nestedBlockKeys(block *tfjson.SchemaBlock) []string {
keys := sortKeys(block.NestedBlocks)
// Remove TF specific "timeouts" block.
if i := slices.Index(keys, "timeouts"); i != -1 {
keys = slices.Delete(keys, i, i+1)
}
return keys
}
func (w *walker) walk(block *tfjson.SchemaBlock, name []string) error {
// Produce nested types before this block itself.
// This ensures types are defined before they are referenced.
for _, k := range nestedBlockKeys(block) {
v := block.NestedBlocks[k]
err := w.walk(v.Block, append(name, strcase.ToCamel(k)))
if err != nil {
return err
}
}
// Declare type.
typ := structType{
Name: strings.Join(name, ""),
}
// Declare attributes.
for _, k := range sortKeys(block.Attributes) {
v := block.Attributes[k]
// Assert the attribute type is always set.
if v.AttributeType == cty.NilType {
return fmt.Errorf("unexpected nil type for attribute %s", k)
}
// Collect field properties.
fieldName := strcase.ToCamel(k)
fieldType := processAttributeType(v.AttributeType)
fieldTag := k
if v.Required && v.Optional {
return fmt.Errorf("both required and optional are set for attribute %s", k)
}
if !v.Required {
fieldTag = fmt.Sprintf("%s,omitempty", fieldTag)
}
// Append to list of fields for type.
typ.Fields = append(typ.Fields, field{
Name: fieldName,
Type: fieldType,
Tag: fieldTag,
})
}
// Declare nested blocks.
for _, k := range nestedBlockKeys(block) {
v := block.NestedBlocks[k]
// Collect field properties.
fieldName := strcase.ToCamel(k)
fieldTypePrefix := ""
if v.MaxItems == 1 {
fieldTypePrefix = "*"
} else {
fieldTypePrefix = "[]"
}
fieldType := fmt.Sprintf("%s%s", fieldTypePrefix, strings.Join(append(name, strcase.ToCamel(k)), ""))
fieldTag := fmt.Sprintf("%s,omitempty", k)
// Append to list of fields for type.
typ.Fields = append(typ.Fields, field{
Name: fieldName,
Type: fieldType,
Tag: fieldTag,
})
}
// Append type to list of structs.
w.StructTypes = append(w.StructTypes, typ)
return nil
}
// walk recursively traverses [tfjson.SchemaBlock] and returns the
// set of types to declare to represents it as Go structs.
func walk(block *tfjson.SchemaBlock, name []string) (*walker, error) {
w := &walker{}
err := w.walk(block, name)
return w, err
}