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 ConvertToAttribute() to convert blocks in a resource/data source schema to attributes #4284

Merged
merged 8 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions internal/providers/pluginfw/tfschema/block_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import (
// This common interface prevents us from keeping two copies of StructToSchema and CustomizableSchema.
type BlockBuilder interface {
BaseSchemaBuilder

// ToAttribute converts a block to its corresponding attribute type. Currently, ResourceStructToSchema converts all
// nested struct fields and slices to blocks. This method is used to convert those blocks to their corresponding
// attribute type. The resulting attribute will not have any of the Computed/Optional/Required/Sensitive flags set.
ToAttribute() AttributeBuilder

BuildDataSourceBlock() dataschema.Block
BuildResourceBlock() schema.Block
}
Expand Down
66 changes: 61 additions & 5 deletions internal/providers/pluginfw/tfschema/customizable_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

// CustomizableSchema is a wrapper struct on top of BaseSchemaBuilder that can be used to navigate through nested schema add customizations.
// The methods of CustomizableSchema that modify the underlying schema should return the same CustomizableSchema object to allow chaining.
type CustomizableSchema struct {
attr BaseSchemaBuilder
}
Expand Down Expand Up @@ -199,8 +200,63 @@ func (s *CustomizableSchema) SetReadOnly(path ...string) *CustomizableSchema {
return s
}

// ConvertToAttribute converts the last element of the path from a block to an attribute.
// It panics if the path is empty, if the path does not exist in the schema, or if the path
// points to an attribute, not a block.
func (s *CustomizableSchema) ConvertToAttribute(path ...string) *CustomizableSchema {
mgyucht marked this conversation as resolved.
Show resolved Hide resolved
if len(path) == 0 {
panic(fmt.Errorf("ConvertToAttribute called on root schema. %s", common.TerraformBugErrorMessage))
}
field := path[len(path)-1]

cb := func(attr BaseSchemaBuilder) BaseSchemaBuilder {
switch a := attr.(type) {
case ListNestedBlockBuilder:
elem, ok := a.NestedObject.Blocks[field]
if !ok {
panic(fmt.Errorf("field %s does not exist in nested block", field))
}
if a.NestedObject.Attributes == nil {
a.NestedObject.Attributes = make(map[string]AttributeBuilder)
}
a.NestedObject.Attributes[field] = elem.ToAttribute()
delete(a.NestedObject.Blocks, field)
if len(a.NestedObject.Blocks) == 0 {
a.NestedObject.Blocks = nil
}
return a
case SingleNestedBlockBuilder:
elem, ok := a.NestedObject.Blocks[field]
if !ok {
panic(fmt.Errorf("field %s does not exist in nested block", field))
}
if a.NestedObject.Attributes == nil {
a.NestedObject.Attributes = make(map[string]AttributeBuilder)
}
a.NestedObject.Attributes[field] = elem.ToAttribute()
delete(a.NestedObject.Blocks, field)
if len(a.NestedObject.Blocks) == 0 {
a.NestedObject.Blocks = nil
}
return a
default:
panic(fmt.Errorf("ConvertToAttribute called on invalid attribute type: %s. %s", reflect.TypeOf(attr).String(), common.TerraformBugErrorMessage))
}
}

// We have to go only as far as the second-to-last entry, since we need to change the parent schema
// by moving the last entry from a block to an attribute.
if len(path) == 1 {
s.attr = cb(s.attr)
} else {
navigateSchemaWithCallback(&s.attr, cb, path[0:len(path)-1]...)
}

return s
}

// navigateSchemaWithCallback navigates through schema attributes and executes callback on the target, panics if path does not exist or invalid.
func navigateSchemaWithCallback(s *BaseSchemaBuilder, cb func(BaseSchemaBuilder) BaseSchemaBuilder, path ...string) (BaseSchemaBuilder, error) {
func navigateSchemaWithCallback(s *BaseSchemaBuilder, cb func(BaseSchemaBuilder) BaseSchemaBuilder, path ...string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this to not return anything to make it clear that it is updating a mutable BaseSchemaBuilder.

currentScm := s
for i, p := range path {
m := attributeToNestedBlockObject(currentScm)
Expand All @@ -211,22 +267,22 @@ func navigateSchemaWithCallback(s *BaseSchemaBuilder, cb func(BaseSchemaBuilder)
if i == len(path)-1 {
newV := cb(v).(AttributeBuilder)
mAttr[p] = newV
return mAttr[p], nil
return
}
castedV := v.(BaseSchemaBuilder)
currentScm = &castedV
} else if v, ok := mBlock[p]; ok {
if i == len(path)-1 {
newV := cb(v).(BlockBuilder)
mBlock[p] = newV
return mBlock[p], nil
return
}
castedV := v.(BaseSchemaBuilder)
currentScm = &castedV
} else {
return nil, fmt.Errorf("missing key %s", p)
panic(fmt.Errorf("missing key %s", p))
}

}
return nil, fmt.Errorf("path %v is incomplete", path)
panic(fmt.Errorf("path %v is incomplete", path))
}
217 changes: 217 additions & 0 deletions internal/providers/pluginfw/tfschema/customizable_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
Expand Down Expand Up @@ -175,3 +176,219 @@ func TestCustomizeSchema_SetComputed_PanicOnBlock(t *testing.T) {
})
})
}

type mockPlanModifier struct{}

// Description implements planmodifier.List.
func (m mockPlanModifier) Description(context.Context) string {
panic("unimplemented")
}

// MarkdownDescription implements planmodifier.List.
func (m mockPlanModifier) MarkdownDescription(context.Context) string {
panic("unimplemented")
}

// PlanModifyList implements planmodifier.List.
func (m mockPlanModifier) PlanModifyList(context.Context, planmodifier.ListRequest, *planmodifier.ListResponse) {
panic("unimplemented")
}

// PlanModifyList implements planmodifier.List.
func (m mockPlanModifier) PlanModifyObject(context.Context, planmodifier.ObjectRequest, *planmodifier.ObjectResponse) {
panic("unimplemented")
}

var _ planmodifier.List = mockPlanModifier{}
var _ planmodifier.Object = mockPlanModifier{}

type mockValidator struct{}

// Description implements validator.List.
func (m mockValidator) Description(context.Context) string {
panic("unimplemented")
}

// MarkdownDescription implements validator.List.
func (m mockValidator) MarkdownDescription(context.Context) string {
panic("unimplemented")
}

// ValidateList implements validator.List.
func (m mockValidator) ValidateList(context.Context, validator.ListRequest, *validator.ListResponse) {
panic("unimplemented")
}

// ValidateList implements validator.Object.
func (m mockValidator) ValidateObject(context.Context, validator.ObjectRequest, *validator.ObjectResponse) {
panic("unimplemented")
}

var _ validator.List = mockValidator{}
var _ validator.Object = mockValidator{}

func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
v := mockValidator{}
pm := mockPlanModifier{}
testCases := []struct {
name string
baseSchema NestedBlockObject
path []string
want NestedBlockObject
expectPanic bool
}{
{
name: "ListNestedBlock",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
DeprecationMessage: "deprecated",
Validators: []validator.List{v},
PlanModifiers: []planmodifier.List{pm},
},
},
},
path: []string{"list"},
want: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"list": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
DeprecationMessage: "deprecated",
Validators: []validator.List{v},
PlanModifiers: []planmodifier.List{pm},
},
},
},
},
{
name: "ListNestedBlock/CalledOnInnerBlock",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"nested_block": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
},
},
},
},
},
},
path: []string{"list", "nested_block"},
want: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"nested_block": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
},
},
},
},
},
},
},
{
name: "SingleNestedBlock",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"single": SingleNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
DeprecationMessage: "deprecated",
Validators: []validator.Object{v},
PlanModifiers: []planmodifier.Object{pm},
},
},
},
path: []string{"single"},
want: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"single": SingleNestedAttributeBuilder{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
DeprecationMessage: "deprecated",
Validators: []validator.Object{v},
PlanModifiers: []planmodifier.Object{pm},
},
},
},
},
{
name: "SingleNestedBlock/RecursiveBlocks",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"single": SingleNestedBlockBuilder{
NestedObject: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"nested_block": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
},
},
},
},
},
},
path: []string{"single"},
want: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"single": SingleNestedAttributeBuilder{
Attributes: map[string]AttributeBuilder{
"nested_block": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
},
},
},
},
},
},
{
name: "PanicOnEmptyPath",
path: nil,
expectPanic: true,
},
}
for _, c := range testCases {
t.Run(c.name, func(t *testing.T) {
if c.expectPanic {
assert.Panics(t, func() {
ConstructCustomizableSchema(c.baseSchema).ConvertToAttribute(c.path...)
})
} else {
got := ConstructCustomizableSchema(c.baseSchema).ConvertToAttribute(c.path...)
assert.Equal(t, c.want, got.attr.(SingleNestedBlockBuilder).NestedObject)
}
})
}
}
10 changes: 5 additions & 5 deletions internal/providers/pluginfw/tfschema/list_nested_attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (a ListNestedAttributeBuilder) BuildResourceAttribute() schema.Attribute {
}
}

func (a ListNestedAttributeBuilder) SetOptional() BaseSchemaBuilder {
func (a ListNestedAttributeBuilder) SetOptional() AttributeBuilder {
if a.Optional && !a.Required {
panic("attribute is already optional")
}
Expand All @@ -53,7 +53,7 @@ func (a ListNestedAttributeBuilder) SetOptional() BaseSchemaBuilder {
return a
}

func (a ListNestedAttributeBuilder) SetRequired() BaseSchemaBuilder {
func (a ListNestedAttributeBuilder) SetRequired() AttributeBuilder {
if !a.Optional && a.Required {
panic("attribute is already required")
}
Expand All @@ -62,23 +62,23 @@ func (a ListNestedAttributeBuilder) SetRequired() BaseSchemaBuilder {
return a
}

func (a ListNestedAttributeBuilder) SetSensitive() BaseSchemaBuilder {
func (a ListNestedAttributeBuilder) SetSensitive() AttributeBuilder {
if a.Sensitive {
panic("attribute is already sensitive")
}
a.Sensitive = true
return a
}

func (a ListNestedAttributeBuilder) SetComputed() BaseSchemaBuilder {
func (a ListNestedAttributeBuilder) SetComputed() AttributeBuilder {
if a.Computed {
panic("attribute is already computed")
}
a.Computed = true
return a
}

func (a ListNestedAttributeBuilder) SetReadOnly() BaseSchemaBuilder {
func (a ListNestedAttributeBuilder) SetReadOnly() AttributeBuilder {
if a.Computed && !a.Optional && !a.Required {
panic("attribute is already read only")
}
Expand Down
Loading
Loading