Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Internal] Add struct to schema for plugin framework #3926

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions internal/pluginframework/tfschema/customizable_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package tfschema

import (
"fmt"

"github.com/databricks/terraform-provider-databricks/common"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

type CustomizableSchemaPluginFramework struct {
attr AttributeBuilder
}

func ConstructCustomizableSchema(attributes map[string]AttributeBuilder) *CustomizableSchemaPluginFramework {
attr := AttributeBuilder(SingleNestedAttributeBuilder{Attributes: attributes})
return &CustomizableSchemaPluginFramework{attr: attr}
}

// Converts CustomizableSchema into a map from string to Attribute.
func (s *CustomizableSchemaPluginFramework) ToAttributeMap() map[string]AttributeBuilder {
return attributeToMap(&s.attr)
}

func attributeToMap(attr *AttributeBuilder) map[string]AttributeBuilder {
var m map[string]AttributeBuilder
switch attr := (*attr).(type) {
case SingleNestedAttributeBuilder:
m = attr.Attributes
case ListNestedAttributeBuilder:
m = attr.NestedObject.Attributes
case MapNestedAttributeBuilder:
m = attr.NestedObject.Attributes
default:
panic(fmt.Errorf("cannot convert to map, attribute is not nested"))
}

return m
}

func (s *CustomizableSchemaPluginFramework) AddValidator(v any, path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
switch a := attr.(type) {
case BoolAttributeBuilder:
return a.AddValidator(v.(validator.Bool))
case Float64AttributeBuilder:
return a.AddValidator(v.(validator.Float64))
case Int64AttributeBuilder:
return a.AddValidator(v.(validator.Int64))
case ListAttributeBuilder:
return a.AddValidator(v.(validator.List))
case ListNestedAttributeBuilder:
return a.AddValidator(v.(validator.List))
case MapAttributeBuilder:
return a.AddValidator(v.(validator.Map))
case MapNestedAttributeBuilder:
return a.AddValidator(v.(validator.Map))
case SingleNestedAttributeBuilder:
return a.AddValidator(v.(validator.Object))
case StringAttributeBuilder:
return a.AddValidator(v.(validator.String))
default:
panic(fmt.Errorf("cannot add validator, attribute builder type is invalid. %s", common.TerraformBugErrorMessage))
}
}

navigateSchemaWithCallback(&s.attr, cb, path...)

return s
}

func (s *CustomizableSchemaPluginFramework) SetOptional(path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
return attr.SetOptional()
}

navigateSchemaWithCallback(&s.attr, cb, path...)

return s
}

func (s *CustomizableSchemaPluginFramework) SetRequired(path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
return attr.SetRequired()
}

navigateSchemaWithCallback(&s.attr, cb, path...)

return s
}

func (s *CustomizableSchemaPluginFramework) SetSensitive(path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
return attr.SetSensitive()
}

navigateSchemaWithCallback(&s.attr, cb, path...)
return s
}

func (s *CustomizableSchemaPluginFramework) SetDeprecated(msg string, path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
return attr.SetDeprecated(msg)
}

navigateSchemaWithCallback(&s.attr, cb, path...)

return s
}

func (s *CustomizableSchemaPluginFramework) SetComputed(path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
return attr.SetComputed()
}

navigateSchemaWithCallback(&s.attr, cb, path...)
return s
}

// SetReadOnly sets the schema to be read-only (i.e. computed, non-optional).
// This should be used for fields that are not user-configurable but are returned
// by the platform.
func (s *CustomizableSchemaPluginFramework) SetReadOnly(path ...string) *CustomizableSchemaPluginFramework {
cb := func(attr AttributeBuilder) AttributeBuilder {
return attr.SetReadOnly()
}

navigateSchemaWithCallback(&s.attr, cb, path...)

return s
}

// Helper function for navigating through schema attributes, panics if path does not exist or invalid.
func navigateSchemaWithCallback(s *AttributeBuilder, cb func(AttributeBuilder) AttributeBuilder, path ...string) (AttributeBuilder, error) {
current_scm := s
for i, p := range path {
m := attributeToMap(current_scm)

v, ok := m[p]
if !ok {
return nil, fmt.Errorf("missing key %s", p)
}

if i == len(path)-1 {
m[p] = cb(v)
return m[p], nil
}
current_scm = &v
}
return nil, fmt.Errorf("path %v is incomplete", path)
}
149 changes: 149 additions & 0 deletions internal/pluginframework/tfschema/struct_to_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package tfschema

import (
"fmt"
"reflect"
"strings"

"github.com/databricks/terraform-provider-databricks/internal/tfreflect"
dataschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func pluginFrameworkTypeToSchema(v reflect.Value) map[string]AttributeBuilder {
scm := map[string]AttributeBuilder{}
rk := v.Kind()
if rk == reflect.Ptr {
v = v.Elem()
rk = v.Kind()
}
if rk != reflect.Struct {
panic(fmt.Errorf("schema value of Struct is expected, but got %s: %#v", rk.String(), v))
}
fields := tfreflect.ListAllFields(v)
for _, field := range fields {
typeField := field.StructField
fieldName := typeField.Tag.Get("tfsdk")
if fieldName == "-" {
continue
}
isOptional := fieldIsOptional(typeField)
kind := typeField.Type.Kind()
if kind == reflect.Ptr {
elem := typeField.Type.Elem()
sv := reflect.New(elem).Elem()
nestedScm := pluginFrameworkTypeToSchema(sv)
scm[fieldName] = SingleNestedAttributeBuilder{Attributes: nestedScm, Optional: isOptional, Required: !isOptional}
} else if kind == reflect.Slice {
elemType := typeField.Type.Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
if elemType.Kind() != reflect.Struct {
panic(fmt.Errorf("unsupported slice value for %s: %s", fieldName, elemType.Kind().String()))
}
switch elemType {
case reflect.TypeOf(types.Bool{}):
scm[fieldName] = ListAttributeBuilder{ElementType: types.BoolType, Optional: isOptional, Required: !isOptional}
case reflect.TypeOf(types.Int64{}):
scm[fieldName] = ListAttributeBuilder{ElementType: types.Int64Type, Optional: isOptional, Required: !isOptional}
case reflect.TypeOf(types.Float64{}):
scm[fieldName] = ListAttributeBuilder{ElementType: types.Float64Type, Optional: isOptional, Required: !isOptional}
case reflect.TypeOf(types.String{}):
scm[fieldName] = ListAttributeBuilder{ElementType: types.StringType, Optional: isOptional, Required: !isOptional}
default:
// Nested struct
nestedScm := pluginFrameworkTypeToSchema(reflect.New(elemType).Elem())
scm[fieldName] = ListNestedAttributeBuilder{NestedObject: NestedAttributeObject{Attributes: nestedScm}, Optional: isOptional, Required: !isOptional}
}
} else if kind == reflect.Map {
elemType := typeField.Type.Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
if elemType.Kind() != reflect.Struct {
panic(fmt.Errorf("unsupported map value for %s: %s", fieldName, elemType.Kind().String()))
}
switch elemType {
case reflect.TypeOf(types.Bool{}):
scm[fieldName] = MapAttributeBuilder{ElementType: types.BoolType, Optional: isOptional, Required: !isOptional}
case reflect.TypeOf(types.Int64{}):
scm[fieldName] = MapAttributeBuilder{ElementType: types.Int64Type, Optional: isOptional, Required: !isOptional}
case reflect.TypeOf(types.Float64{}):
scm[fieldName] = MapAttributeBuilder{ElementType: types.Float64Type, Optional: isOptional, Required: !isOptional}
case reflect.TypeOf(types.String{}):
scm[fieldName] = MapAttributeBuilder{ElementType: types.StringType, Optional: isOptional, Required: !isOptional}
default:
// Nested struct
nestedScm := pluginFrameworkTypeToSchema(reflect.New(elemType).Elem())
scm[fieldName] = MapNestedAttributeBuilder{NestedObject: NestedAttributeObject{Attributes: nestedScm}, Optional: isOptional, Required: !isOptional}
}
} else if kind == reflect.Struct {
switch field.Value.Interface().(type) {
case types.Bool:
scm[fieldName] = BoolAttributeBuilder{Optional: isOptional, Required: !isOptional}
case types.Int64:
scm[fieldName] = Int64AttributeBuilder{Optional: isOptional, Required: !isOptional}
case types.Float64:
scm[fieldName] = Float64AttributeBuilder{Optional: isOptional, Required: !isOptional}
case types.String:
scm[fieldName] = StringAttributeBuilder{Optional: isOptional, Required: !isOptional}
case types.List:
panic("types.List should never be used in tfsdk structs")
case types.Map:
panic("types.Map should never be used in tfsdk structs")
default:
// If it is a real stuct instead of a tfsdk type, recursively resolve it.
elem := typeField.Type
sv := reflect.New(elem)
nestedScm := pluginFrameworkTypeToSchema(sv)
scm[fieldName] = SingleNestedAttributeBuilder{Attributes: nestedScm, Optional: isOptional, Required: !isOptional}
}
} else if kind == reflect.String {
// This case is for Enum types.
// TODO: Add automatic enum validation.
scm[fieldName] = StringAttributeBuilder{Optional: isOptional, Required: !isOptional}
} else {
panic(fmt.Errorf("unknown type for field: %s", typeField.Name))
}
}
return scm
}

func fieldIsOptional(field reflect.StructField) bool {
tagValue := field.Tag.Get("tf")
return strings.Contains(tagValue, "optional")
}

func PluginFrameworkResourceStructToSchema(v any, customizeSchema func(CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework) schema.Schema {
attributes := PluginFrameworkResourceStructToSchemaMap(v, customizeSchema)
return schema.Schema{Attributes: attributes}
}

func PluginFrameworkDataSourceStructToSchema(v any, customizeSchema func(CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework) dataschema.Schema {
attributes := PluginFrameworkDataSourceStructToSchemaMap(v, customizeSchema)
return dataschema.Schema{Attributes: attributes}
}

func PluginFrameworkResourceStructToSchemaMap(v any, customizeSchema func(CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework) map[string]schema.Attribute {
attributes := pluginFrameworkTypeToSchema(reflect.ValueOf(v))

if customizeSchema != nil {
cs := customizeSchema(*ConstructCustomizableSchema(attributes))
return BuildResourceAttributeMap(cs.ToAttributeMap())
} else {
return BuildResourceAttributeMap(attributes)
}
}

func PluginFrameworkDataSourceStructToSchemaMap(v any, customizeSchema func(CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework) map[string]dataschema.Attribute {
attributes := pluginFrameworkTypeToSchema(reflect.ValueOf(v))

if customizeSchema != nil {
cs := customizeSchema(*ConstructCustomizableSchema(attributes))
return BuildDataSourceAttributeMap(cs.ToAttributeMap())
} else {
return BuildDataSourceAttributeMap(attributes)
}
}
Loading