diff --git a/internal/pluginframework/tfschema/customizable_schema.go b/internal/pluginframework/tfschema/customizable_schema.go new file mode 100644 index 0000000000..90439c0ce0 --- /dev/null +++ b/internal/pluginframework/tfschema/customizable_schema.go @@ -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) +} diff --git a/internal/pluginframework/tfschema/struct_to_schema.go b/internal/pluginframework/tfschema/struct_to_schema.go new file mode 100644 index 0000000000..ee3c7082bd --- /dev/null +++ b/internal/pluginframework/tfschema/struct_to_schema.go @@ -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) + } +}