Skip to content

Commit f833cf2

Browse files
[Internal] Add StructToSchema for Plugin Framework (#3928)
## Changes <!-- Summary of your changes that are easy to understand --> - Addressed comments in #3893 - Adding `StructToSchema` and corresponding unit tests, as well as tests for `CustomizableSchema` ## Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [x] `make test` run locally - [x] relevant change in `docs/` folder - [x] covered with integration tests in `internal/acceptance` - [x] relevant acceptance tests are passing - [x] using Go SDK
1 parent b9f5e90 commit f833cf2

File tree

3 files changed

+515
-0
lines changed

3 files changed

+515
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package tfschema
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
type TestTfSdk struct {
15+
Description types.String `tfsdk:"description" tf:""`
16+
Nested *NestedTfSdk `tfsdk:"nested" tf:"optional"`
17+
Map map[string]types.String `tfsdk:"map" tf:"optional"`
18+
}
19+
20+
type NestedTfSdk struct {
21+
Name types.String `tfsdk:"name" tf:"optional"`
22+
Enabled types.Bool `tfsdk:"enabled" tf:"optional"`
23+
}
24+
25+
type stringLengthBetweenValidator struct {
26+
Max int
27+
Min int
28+
}
29+
30+
// Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact.
31+
func (v stringLengthBetweenValidator) Description(ctx context.Context) string {
32+
return fmt.Sprintf("string length must be between %d and %d", v.Min, v.Max)
33+
}
34+
35+
// MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact.
36+
func (v stringLengthBetweenValidator) MarkdownDescription(ctx context.Context) string {
37+
return fmt.Sprintf("string length must be between `%d` and `%d`", v.Min, v.Max)
38+
}
39+
40+
// Validate runs the main validation logic of the validator, reading configuration data out of `req` and updating `resp` with diagnostics.
41+
func (v stringLengthBetweenValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
42+
// If the value is unknown or null, there is nothing to validate.
43+
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
44+
return
45+
}
46+
47+
strLen := len(req.ConfigValue.ValueString())
48+
49+
if strLen < v.Min || strLen > v.Max {
50+
resp.Diagnostics.AddAttributeError(
51+
req.Path,
52+
"Invalid String Length",
53+
fmt.Sprintf("String length must be between %d and %d, got: %d.", v.Min, v.Max, strLen),
54+
)
55+
56+
return
57+
}
58+
}
59+
60+
func TestCustomizeSchemaSetRequired(t *testing.T) {
61+
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
62+
c.SetRequired("nested", "enabled")
63+
return c
64+
})
65+
66+
assert.True(t, scm.Attributes["nested"].(schema.SingleNestedAttribute).Attributes["enabled"].IsRequired())
67+
}
68+
69+
func TestCustomizeSchemaSetOptional(t *testing.T) {
70+
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
71+
c.SetOptional("description")
72+
return c
73+
})
74+
75+
assert.True(t, scm.Attributes["description"].IsOptional())
76+
}
77+
78+
func TestCustomizeSchemaSetSensitive(t *testing.T) {
79+
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
80+
c.SetSensitive("nested", "name")
81+
return c
82+
})
83+
84+
assert.True(t, scm.Attributes["nested"].(schema.SingleNestedAttribute).Attributes["name"].IsSensitive())
85+
}
86+
87+
func TestCustomizeSchemaSetDeprecated(t *testing.T) {
88+
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
89+
c.SetDeprecated("deprecated", "map")
90+
return c
91+
})
92+
93+
assert.True(t, scm.Attributes["map"].GetDeprecationMessage() == "deprecated")
94+
}
95+
96+
func TestCustomizeSchemaSetReadOnly(t *testing.T) {
97+
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
98+
c.SetReadOnly("map")
99+
return c
100+
})
101+
assert.True(t, !scm.Attributes["map"].IsOptional())
102+
assert.True(t, !scm.Attributes["map"].IsRequired())
103+
assert.True(t, scm.Attributes["map"].IsComputed())
104+
}
105+
106+
func TestCustomizeSchemaAddValidator(t *testing.T) {
107+
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
108+
c.AddValidator(stringLengthBetweenValidator{}, "description")
109+
return c
110+
})
111+
112+
assert.True(t, len(scm.Attributes["description"].(schema.StringAttribute).Validators) == 1)
113+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package tfschema
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/databricks/terraform-provider-databricks/common"
9+
"github.com/databricks/terraform-provider-databricks/internal/tfreflect"
10+
dataschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
)
14+
15+
func typeToSchema(v reflect.Value) map[string]AttributeBuilder {
16+
scm := map[string]AttributeBuilder{}
17+
rk := v.Kind()
18+
if rk == reflect.Ptr {
19+
v = v.Elem()
20+
rk = v.Kind()
21+
}
22+
if rk != reflect.Struct {
23+
panic(fmt.Errorf("schema value of Struct is expected, but got %s: %#v. %s", rk.String(), v, common.TerraformBugErrorMessage))
24+
}
25+
fields := tfreflect.ListAllFields(v)
26+
for _, field := range fields {
27+
typeField := field.StructField
28+
fieldName := typeField.Tag.Get("tfsdk")
29+
if fieldName == "-" {
30+
continue
31+
}
32+
isOptional := fieldIsOptional(typeField)
33+
kind := typeField.Type.Kind()
34+
value := field.Value
35+
typeFieldType := typeField.Type
36+
if kind == reflect.Ptr {
37+
typeFieldType = typeFieldType.Elem()
38+
kind = typeFieldType.Kind()
39+
value = reflect.New(typeFieldType).Elem()
40+
}
41+
if kind == reflect.Slice {
42+
elemType := typeFieldType.Elem()
43+
if elemType.Kind() == reflect.Ptr {
44+
elemType = elemType.Elem()
45+
}
46+
if elemType.Kind() != reflect.Struct {
47+
panic(fmt.Errorf("unsupported slice value for %s: %s. %s", fieldName, elemType.Kind().String(), common.TerraformBugErrorMessage))
48+
}
49+
switch elemType {
50+
case reflect.TypeOf(types.Bool{}):
51+
scm[fieldName] = ListAttributeBuilder{ElementType: types.BoolType, Optional: isOptional, Required: !isOptional}
52+
case reflect.TypeOf(types.Int64{}):
53+
scm[fieldName] = ListAttributeBuilder{ElementType: types.Int64Type, Optional: isOptional, Required: !isOptional}
54+
case reflect.TypeOf(types.Float64{}):
55+
scm[fieldName] = ListAttributeBuilder{ElementType: types.Float64Type, Optional: isOptional, Required: !isOptional}
56+
case reflect.TypeOf(types.String{}):
57+
scm[fieldName] = ListAttributeBuilder{ElementType: types.StringType, Optional: isOptional, Required: !isOptional}
58+
default:
59+
// Nested struct
60+
nestedScm := typeToSchema(reflect.New(elemType).Elem())
61+
scm[fieldName] = ListNestedAttributeBuilder{NestedObject: NestedAttributeObject{Attributes: nestedScm}, Optional: isOptional, Required: !isOptional}
62+
}
63+
} else if kind == reflect.Map {
64+
elemType := typeFieldType.Elem()
65+
if elemType.Kind() == reflect.Ptr {
66+
elemType = elemType.Elem()
67+
}
68+
if elemType.Kind() != reflect.Struct {
69+
panic(fmt.Errorf("unsupported map value for %s: %s. %s", fieldName, elemType.Kind().String(), common.TerraformBugErrorMessage))
70+
}
71+
switch elemType {
72+
case reflect.TypeOf(types.Bool{}):
73+
scm[fieldName] = MapAttributeBuilder{ElementType: types.BoolType, Optional: isOptional, Required: !isOptional}
74+
case reflect.TypeOf(types.Int64{}):
75+
scm[fieldName] = MapAttributeBuilder{ElementType: types.Int64Type, Optional: isOptional, Required: !isOptional}
76+
case reflect.TypeOf(types.Float64{}):
77+
scm[fieldName] = MapAttributeBuilder{ElementType: types.Float64Type, Optional: isOptional, Required: !isOptional}
78+
case reflect.TypeOf(types.String{}):
79+
scm[fieldName] = MapAttributeBuilder{ElementType: types.StringType, Optional: isOptional, Required: !isOptional}
80+
default:
81+
// Nested struct
82+
nestedScm := typeToSchema(reflect.New(elemType).Elem())
83+
scm[fieldName] = MapNestedAttributeBuilder{NestedObject: NestedAttributeObject{Attributes: nestedScm}, Optional: isOptional, Required: !isOptional}
84+
}
85+
} else if kind == reflect.Struct {
86+
switch value.Interface().(type) {
87+
case types.Bool:
88+
scm[fieldName] = BoolAttributeBuilder{Optional: isOptional, Required: !isOptional}
89+
case types.Int64:
90+
scm[fieldName] = Int64AttributeBuilder{Optional: isOptional, Required: !isOptional}
91+
case types.Float64:
92+
scm[fieldName] = Float64AttributeBuilder{Optional: isOptional, Required: !isOptional}
93+
case types.String:
94+
scm[fieldName] = StringAttributeBuilder{Optional: isOptional, Required: !isOptional}
95+
case types.List:
96+
panic(fmt.Errorf("types.List should never be used in tfsdk structs. %s", common.TerraformBugErrorMessage))
97+
case types.Map:
98+
panic(fmt.Errorf("types.Map should never be used in tfsdk structs. %s", common.TerraformBugErrorMessage))
99+
default:
100+
// If it is a real stuct instead of a tfsdk type, recursively resolve it.
101+
elem := typeFieldType
102+
sv := reflect.New(elem)
103+
nestedScm := typeToSchema(sv)
104+
scm[fieldName] = SingleNestedAttributeBuilder{Attributes: nestedScm, Optional: isOptional, Required: !isOptional}
105+
}
106+
} else {
107+
panic(fmt.Errorf("unknown type for field: %s. %s", typeField.Name, common.TerraformBugErrorMessage))
108+
}
109+
}
110+
return scm
111+
}
112+
113+
func fieldIsOptional(field reflect.StructField) bool {
114+
tagValue := field.Tag.Get("tf")
115+
return strings.Contains(tagValue, "optional")
116+
}
117+
118+
// ResourceStructToSchema builds a resource schema from a tfsdk struct, with custoimzations applied.
119+
func ResourceStructToSchema(v any, customizeSchema func(CustomizableSchema) CustomizableSchema) schema.Schema {
120+
attributes := ResourceStructToSchemaMap(v, customizeSchema)
121+
return schema.Schema{Attributes: attributes}
122+
}
123+
124+
// DataSourceStructToSchema builds a data source schema from a tfsdk struct, with custoimzations applied.
125+
func DataSourceStructToSchema(v any, customizeSchema func(CustomizableSchema) CustomizableSchema) dataschema.Schema {
126+
attributes := DataSourceStructToSchemaMap(v, customizeSchema)
127+
return dataschema.Schema{Attributes: attributes}
128+
}
129+
130+
// ResourceStructToSchemaMap returns a map from string to resource schema attributes using a tfsdk struct, with custoimzations applied.
131+
func ResourceStructToSchemaMap(v any, customizeSchema func(CustomizableSchema) CustomizableSchema) map[string]schema.Attribute {
132+
attributes := typeToSchema(reflect.ValueOf(v))
133+
134+
if customizeSchema != nil {
135+
cs := customizeSchema(*ConstructCustomizableSchema(attributes))
136+
return BuildResourceAttributeMap(cs.ToAttributeMap())
137+
} else {
138+
return BuildResourceAttributeMap(attributes)
139+
}
140+
}
141+
142+
// DataSourceStructToSchemaMap returns a map from string to data source schema attributes using a tfsdk struct, with custoimzations applied.
143+
func DataSourceStructToSchemaMap(v any, customizeSchema func(CustomizableSchema) CustomizableSchema) map[string]dataschema.Attribute {
144+
attributes := typeToSchema(reflect.ValueOf(v))
145+
146+
if customizeSchema != nil {
147+
cs := customizeSchema(*ConstructCustomizableSchema(attributes))
148+
return BuildDataSourceAttributeMap(cs.ToAttributeMap())
149+
} else {
150+
return BuildDataSourceAttributeMap(attributes)
151+
}
152+
}

0 commit comments

Comments
 (0)