Skip to content

Commit

Permalink
internal/fwschema: Add NestedAttributeObject and NestedBlockObject in…
Browse files Browse the repository at this point in the history
…terfaces

Reference: #132
Reference: #508
Reference: #532

Since the initial version of the framework, it has treated nested attributes and blocks as a singular entity with some internal gynastics to handle the one (single; object) or two (list, map, set to object) actual types that make up the nested attribute or block. Having these schema abstractions as single entities caused developer confusion when attempting to extract data or create paths.

Other desirable features for the framework are the ability to customize the types, define validators, and define plan modifiers of nested attributes and blocks. This type customization, validation, or plan modification may need to be on the nested object to simplify provider implementations. Exposing these options with a flat abstraction could be confusing.

This change introduces the concept of a nested attribute object and a nested block object into the `internal/fwschema` package and makes the necessary implementation changes to `tfsdk.Attribute` and `tfsdk.Block`. While the `tfsdk` package schema handling does not expose the new potential enhancements, the upcoming `datasource`, `provider`, and `resource` schema handling will. The existing unit testing shows no regressions and the new unit tests with validation show it should be possible to implement nested object validation (and when implemented in the near future, plan modification).

This changeset additionally migrates more `tfsdk.Schema` logic into the `internal/fwschema` package in further preparation for creating additional schema implementations that will benefit from the existing schema logic.
  • Loading branch information
bflad committed Nov 18, 2022
1 parent baf4cfd commit dd20174
Show file tree
Hide file tree
Showing 34 changed files with 1,795 additions and 476 deletions.
6 changes: 1 addition & 5 deletions .changelog/543.txt
Expand Up @@ -3,9 +3,5 @@ tfsdk: The `Attribute` type `FrameworkType()` method has been removed. Use the `
```

```release-note:breaking-change
tfsdk: The `Attribute` type `GetAttributes()` method now returns underlying attribute information rather than another interface. Use the `GetNestingMode()` method to determine the nesting mode.
```

```release-note:breaking-change
tfsdk: The `Attribute` type `GetType()` method now returns type information whether the attribute implements the `Type` field or `Attributes` field. Use the `GetAttributes()` and `GetNestingMode()` methods to determine if the `Attributes` field is explicitly being used.
tfsdk: The `Attribute` type `GetType()` method now returns type information whether the attribute implements the `Type` field or `Attributes` field.
```
15 changes: 5 additions & 10 deletions internal/fwschema/block.go
Expand Up @@ -21,16 +21,6 @@ type Block interface {
// Equal should return true if the other block is exactly equivalent.
Equal(o Block) bool

// GetAttributes should return the nested attributes of a block, if
// applicable. This is named differently than Attributes to prevent a
// conflict with the tfsdk.Block field name.
GetAttributes() map[string]Attribute

// GetBlocks should return the nested blocks of a block, if
// applicable. This is named differently than Blocks to prevent a
// conflict with the tfsdk.Block field name.
GetBlocks() map[string]Block

// GetDeprecationMessage should return a non-empty string if an attribute
// is deprecated. This is named differently than DeprecationMessage to
// prevent a conflict with the tfsdk.Attribute field name.
Expand All @@ -57,6 +47,11 @@ type Block interface {
// field name.
GetMinItems() int64

// GetNestedObject should return the object underneath the block.
// For single nesting mode, the NestedBlockObject can be generated from
// the Block.
GetNestedObject() NestedBlockObject

// GetNestingMode should return the nesting mode of a block. This is named
// differently than NestingMode to prevent a conflict with the tfsdk.Block
// field name.
Expand Down
15 changes: 15 additions & 0 deletions internal/fwschema/errors.go
@@ -0,0 +1,15 @@
package fwschema

import "errors"

var (
// ErrPathInsideAtomicAttribute is used with AttributeAtPath is called
// on a path that doesn't have a schema associated with it, because
// it's an element, attribute, or block of a complex type, not a nested
// attribute.
ErrPathInsideAtomicAttribute = errors.New("path leads to element, attribute, or block of a schema.Attribute that has no schema associated with it")

// ErrPathIsBlock is used with AttributeAtPath is called on a path is a
// block, not an attribute. Use blockAtPath on the path instead.
ErrPathIsBlock = errors.New("path leads to block, not an attribute")
)
15 changes: 15 additions & 0 deletions internal/fwschema/fwxschema/nested_attribute_object_validation.go
@@ -0,0 +1,15 @@
package fwxschema

import (
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

// NestedAttributeObjectWithValidators is an optional interface on
// NestedAttributeObject which enables Object validation support.
type NestedAttributeObjectWithValidators interface {
fwschema.NestedAttributeObject

// ObjectValidators should return a list of Object validators.
ObjectValidators() []validator.Object
}
15 changes: 15 additions & 0 deletions internal/fwschema/fwxschema/nested_block_object_validators.go
@@ -0,0 +1,15 @@
package fwxschema

import (
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

// NestedBlockObjectWithValidators is an optional interface on
// NestedBlockObject which enables Object validation support.
type NestedBlockObjectWithValidators interface {
fwschema.NestedBlockObject

// ObjectValidators should return a list of Object validators.
ObjectValidators() []validator.Object
}
8 changes: 4 additions & 4 deletions internal/fwschema/nested_attribute.go
Expand Up @@ -4,10 +4,10 @@ package fwschema
type NestedAttribute interface {
Attribute

// GetAttributes should return the nested attributes of an attribute, if
// applicable. This is named differently than Attribute to prevent a
// conflict with the tfsdk.Attribute field name.
GetAttributes() UnderlyingAttributes
// GetNestedObject should return the object underneath the nested
// attribute. For single nesting mode, the NestedAttributeObject can be
// generated from the Attribute.
GetNestedObject() NestedAttributeObject

// GetNestingMode should return the nesting mode (list, map, set, or
// single) of the nested attributes or left unset if this Attribute
Expand Down
89 changes: 89 additions & 0 deletions internal/fwschema/nested_attribute_object.go
@@ -0,0 +1,89 @@
package fwschema

import (
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// NestedAttributeObject represents the Object inside a NestedAttribute.
// Refer to the fwxschema package for validation and plan modification
// extensions to this interface.
type NestedAttributeObject interface {
tftypes.AttributePathStepper

// Equal should return true if given NestedAttributeObject is equivalent.
Equal(NestedAttributeObject) bool

// GetAttributes should return the nested attributes of an attribute.
GetAttributes() UnderlyingAttributes

// Type should return the framework type of the object.
Type() types.ObjectTypable
}

// NestedAttributeObjectApplyTerraform5AttributePathStep is a helper function
// to perform base tftypes.AttributePathStepper handling using the
// GetAttributes method. NestedAttributeObject implementations should still
// include custom type functionality in addition to using this helper.
func NestedAttributeObjectApplyTerraform5AttributePathStep(o NestedAttributeObject, step tftypes.AttributePathStep) (any, error) {
name, ok := step.(tftypes.AttributeName)

if !ok {
return nil, fmt.Errorf("cannot apply AttributePathStep %T to NestedAttributeObject", step)
}

attribute, ok := o.GetAttributes()[string(name)]

if ok {
return attribute, nil
}

return nil, fmt.Errorf("no attribute %q on NestedAttributeObject", name)
}

// NestedAttributeObjectEqual is a helper function to perform base equality testing
// on two NestedAttributeObject. NestedAttributeObject implementations should still
// compare the concrete types and other custom functionality in addition to
// using this helper.
func NestedAttributeObjectEqual(a, b NestedAttributeObject) bool {
if !a.Type().Equal(b.Type()) {
return false
}

if len(a.GetAttributes()) != len(b.GetAttributes()) {
return false
}

for name, aAttribute := range a.GetAttributes() {
bAttribute, ok := b.GetAttributes()[name]

if !ok {
return false
}

if !aAttribute.Equal(bAttribute) {
return false
}
}

return true
}

// NestedAttributeObjectType is a helper function to perform base type handling
// using the GetAttributes and GetBlocks methods. NestedAttributeObject
// implementations should still include custom type functionality in addition
// to using this helper.
func NestedAttributeObjectType(o NestedAttributeObject) types.ObjectTypable {
attrTypes := make(map[string]attr.Type, len(o.GetAttributes()))

for name, attribute := range o.GetAttributes() {
attrTypes[name] = attribute.GetType()
}

return types.ObjectType{
AttrTypes: attrTypes,
}
}
33 changes: 0 additions & 33 deletions internal/fwschema/nested_block.go

This file was deleted.

118 changes: 118 additions & 0 deletions internal/fwschema/nested_block_object.go
@@ -0,0 +1,118 @@
package fwschema

import (
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// NestedBlockObject represents the Object inside a Block.
// Refer to the fwxschema package for validation and plan modification
// extensions to this interface.
type NestedBlockObject interface {
tftypes.AttributePathStepper

// Equal should return true if given NestedBlockObject is equivalent.
Equal(NestedBlockObject) bool

// GetAttributes should return the nested attributes of the object.
GetAttributes() UnderlyingAttributes

// GetBlocks should return the nested attributes of the object.
GetBlocks() map[string]Block

// Type should return the framework type of the object.
Type() types.ObjectTypable
}

// NestedBlockObjectApplyTerraform5AttributePathStep is a helper function to
// perform base tftypes.AttributePathStepper handling using the GetAttributes
// and GetBlocks methods. NestedBlockObject implementations should still
// include custom type functionality in addition to using this helper.
func NestedBlockObjectApplyTerraform5AttributePathStep(o NestedBlockObject, step tftypes.AttributePathStep) (any, error) {
name, ok := step.(tftypes.AttributeName)

if !ok {
return nil, fmt.Errorf("cannot apply AttributePathStep %T to NestedBlockObject", step)
}

attribute, ok := o.GetAttributes()[string(name)]

if ok {
return attribute, nil
}

block, ok := o.GetBlocks()[string(name)]

if ok {
return block, nil
}

return nil, fmt.Errorf("no attribute or block %q on NestedBlockObject", name)
}

// NestedBlockObjectEqual is a helper function to perform base equality testing
// on two NestedBlockObject. NestedBlockObject implementations should still
// compare the concrete types and other custom functionality in addition to
// using this helper.
func NestedBlockObjectEqual(a, b NestedBlockObject) bool {
if !a.Type().Equal(b.Type()) {
return false
}

if len(a.GetAttributes()) != len(b.GetAttributes()) {
return false
}

for name, aAttribute := range a.GetAttributes() {
bAttribute, ok := b.GetAttributes()[name]

if !ok {
return false
}

if !aAttribute.Equal(bAttribute) {
return false
}
}

if len(a.GetBlocks()) != len(b.GetBlocks()) {
return false
}

for name, aBlock := range a.GetBlocks() {
bBlock, ok := b.GetBlocks()[name]

if !ok {
return false
}

if !aBlock.Equal(bBlock) {
return false
}
}

return true
}

// NestedBlockObjectType is a helper function to perform base type handling
// using the GetAttributes and GetBlocks methods. NestedBlockObject
// implementations should still include custom type functionality in addition
// to using this helper.
func NestedBlockObjectType(o NestedBlockObject) types.ObjectTypable {
attrTypes := make(map[string]attr.Type, len(o.GetAttributes())+len(o.GetBlocks()))

for name, attribute := range o.GetAttributes() {
attrTypes[name] = attribute.GetType()
}

for name, block := range o.GetBlocks() {
attrTypes[name] = block.Type()
}

return types.ObjectType{
AttrTypes: attrTypes,
}
}

0 comments on commit dd20174

Please sign in to comment.