From dd20174e5ea1793a0d0f577e42fdd3ff359d85c7 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 17 Nov 2022 23:28:59 -0500 Subject: [PATCH] internal/fwschema: Add NestedAttributeObject and NestedBlockObject interfaces Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/132 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/508 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/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. --- .changelog/543.txt | 6 +- internal/fwschema/block.go | 15 +- internal/fwschema/errors.go | 15 + .../nested_attribute_object_validation.go | 15 + .../nested_block_object_validators.go | 15 + internal/fwschema/nested_attribute.go | 8 +- internal/fwschema/nested_attribute_object.go | 89 +++++ internal/fwschema/nested_block.go | 33 -- internal/fwschema/nested_block_object.go | 118 ++++++ internal/fwschema/schema.go | 156 ++++++++ internal/fwschema/underlying_attributes.go | 2 +- .../fwserver/attribute_plan_modification.go | 10 +- internal/fwserver/attribute_validation.go | 170 ++++++--- .../fwserver/attribute_validation_test.go | 256 +++++++++++++ internal/fwserver/block_plan_modification.go | 14 +- internal/fwserver/block_validation.go | 195 ++++++---- internal/fwserver/block_validation_test.go | 342 ++++++++++++++++++ .../testschema/blockwithlistvalidators.go | 32 +- .../testschema/blockwithobjectvalidators.go | 34 +- .../testschema/blockwithsetvalidators.go | 32 +- .../testschema/nested_attribute_object.go | 79 ++++ ...nested_attribute_object_with_validators.go | 87 +++++ .../testing/testschema/nested_block_object.go | 45 +++ .../nested_block_object_with_validators.go | 53 +++ internal/testing/testschema/schema.go | 82 +++++ internal/toproto5/block.go | 6 +- internal/toproto5/schema_attribute.go | 2 +- internal/toproto6/block.go | 6 +- internal/toproto6/schema_attribute.go | 6 +- tfsdk/attribute.go | 52 +-- tfsdk/block.go | 52 +-- tfsdk/nested_attribute_object.go | 44 +++ tfsdk/nested_block_object.go | 60 +++ tfsdk/schema.go | 140 +------ 34 files changed, 1795 insertions(+), 476 deletions(-) create mode 100644 internal/fwschema/errors.go create mode 100644 internal/fwschema/fwxschema/nested_attribute_object_validation.go create mode 100644 internal/fwschema/fwxschema/nested_block_object_validators.go create mode 100644 internal/fwschema/nested_attribute_object.go delete mode 100644 internal/fwschema/nested_block.go create mode 100644 internal/fwschema/nested_block_object.go create mode 100644 internal/testing/testschema/nested_attribute_object.go create mode 100644 internal/testing/testschema/nested_attribute_object_with_validators.go create mode 100644 internal/testing/testschema/nested_block_object.go create mode 100644 internal/testing/testschema/nested_block_object_with_validators.go create mode 100644 internal/testing/testschema/schema.go create mode 100644 tfsdk/nested_attribute_object.go create mode 100644 tfsdk/nested_block_object.go diff --git a/.changelog/543.txt b/.changelog/543.txt index 4578b4a14..8411e73fe 100644 --- a/.changelog/543.txt +++ b/.changelog/543.txt @@ -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. ``` diff --git a/internal/fwschema/block.go b/internal/fwschema/block.go index 1841a8785..6f465a23b 100644 --- a/internal/fwschema/block.go +++ b/internal/fwschema/block.go @@ -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. @@ -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. diff --git a/internal/fwschema/errors.go b/internal/fwschema/errors.go new file mode 100644 index 000000000..d1be30e44 --- /dev/null +++ b/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") +) diff --git a/internal/fwschema/fwxschema/nested_attribute_object_validation.go b/internal/fwschema/fwxschema/nested_attribute_object_validation.go new file mode 100644 index 000000000..78dd23453 --- /dev/null +++ b/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 +} diff --git a/internal/fwschema/fwxschema/nested_block_object_validators.go b/internal/fwschema/fwxschema/nested_block_object_validators.go new file mode 100644 index 000000000..48e0b0507 --- /dev/null +++ b/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 +} diff --git a/internal/fwschema/nested_attribute.go b/internal/fwschema/nested_attribute.go index 2bd85383a..f2bcb1f5f 100644 --- a/internal/fwschema/nested_attribute.go +++ b/internal/fwschema/nested_attribute.go @@ -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 diff --git a/internal/fwschema/nested_attribute_object.go b/internal/fwschema/nested_attribute_object.go new file mode 100644 index 000000000..573b74c18 --- /dev/null +++ b/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, + } +} diff --git a/internal/fwschema/nested_block.go b/internal/fwschema/nested_block.go deleted file mode 100644 index 5d49e37e8..000000000 --- a/internal/fwschema/nested_block.go +++ /dev/null @@ -1,33 +0,0 @@ -package fwschema - -import ( - "fmt" - - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -type NestedBlock struct { - Block -} - -// ApplyTerraform5AttributePathStep allows Blocks to be walked using -// tftypes.Walk and tftypes.Transform. -func (b NestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { - a, ok := step.(tftypes.AttributeName) - - if !ok { - return nil, fmt.Errorf("can't apply %T to block", step) - } - - attrName := string(a) - - if attr, ok := b.Block.GetAttributes()[attrName]; ok { - return attr, nil - } - - if block, ok := b.Block.GetBlocks()[attrName]; ok { - return block, nil - } - - return nil, fmt.Errorf("no attribute %q on Attributes or Blocks", a) -} diff --git a/internal/fwschema/nested_block_object.go b/internal/fwschema/nested_block_object.go new file mode 100644 index 000000000..b3b8fdb8d --- /dev/null +++ b/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, + } +} diff --git a/internal/fwschema/schema.go b/internal/fwschema/schema.go index f74bdaf9f..3bfffeaa0 100644 --- a/internal/fwschema/schema.go +++ b/internal/fwschema/schema.go @@ -2,10 +2,13 @@ package fwschema import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -66,3 +69,156 @@ type Schema interface { // the given Terraform path or return an error. TypeAtTerraformPath(context.Context, *tftypes.AttributePath) (attr.Type, error) } + +// SchemaApplyTerraform5AttributePathStep is a helper function to perform base +// tftypes.AttributePathStepper handling using the GetAttributes and GetBlocks +// methods. +func SchemaApplyTerraform5AttributePathStep(s Schema, step tftypes.AttributePathStep) (any, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to schema", step) + } + + if attr, ok := s.GetAttributes()[string(name)]; ok { + return attr, nil + } + + if block, ok := s.GetBlocks()[string(name)]; ok { + return block, nil + } + + return nil, fmt.Errorf("could not find attribute or block %q in schema", name) +} + +// SchemaAttributeAtPath is a helper function to perform base type handling using +// the AttributeAtTerraformPath method. +func SchemaAttributeAtPath(ctx context.Context, s Schema, p path.Path) (Attribute, diag.Diagnostics) { + var diags diag.Diagnostics + + tftypesPath, tftypesDiags := totftypes.AttributePath(ctx, p) + + diags.Append(tftypesDiags...) + + if diags.HasError() { + return nil, diags + } + + attribute, err := s.AttributeAtTerraformPath(ctx, tftypesPath) + + if err != nil { + diags.AddAttributeError( + p, + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", p)+ + fmt.Sprintf("Original Error: %s", err), + ) + return nil, diags + } + + return attribute, diags +} + +// SchemaAttributeAtTerraformPath is a helper function to perform base type +// handling using the tftypes.AttributePathStepper interface. +func SchemaAttributeAtTerraformPath(ctx context.Context, s Schema, p *tftypes.AttributePath) (Attribute, error) { + rawType, remaining, err := tftypes.WalkAttributePath(s, p) + + if err != nil { + return nil, fmt.Errorf("%v still remains in the path: %w", remaining, err) + } + + switch typ := rawType.(type) { + case attr.Type: + return nil, ErrPathInsideAtomicAttribute + case Attribute: + return typ, nil + case Block: + return nil, ErrPathIsBlock + case NestedAttributeObject: + return nil, ErrPathInsideAtomicAttribute + case NestedBlockObject: + return nil, ErrPathInsideAtomicAttribute + case UnderlyingAttributes: + return nil, ErrPathInsideAtomicAttribute + default: + return nil, fmt.Errorf("got unexpected type %T", rawType) + } +} + +// SchemaTypeAtPath is a helper function to perform base type handling using +// the TypeAtTerraformPath method. +func SchemaTypeAtPath(ctx context.Context, s Schema, p path.Path) (attr.Type, diag.Diagnostics) { + var diags diag.Diagnostics + + tftypesPath, tftypesDiags := totftypes.AttributePath(ctx, p) + + diags.Append(tftypesDiags...) + + if diags.HasError() { + return nil, diags + } + + attrType, err := s.TypeAtTerraformPath(ctx, tftypesPath) + + if err != nil { + diags.AddAttributeError( + p, + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", p)+ + fmt.Sprintf("Original Error: %s", err), + ) + return nil, diags + } + + return attrType, diags +} + +// SchemaTypeAtTerraformPath is a helper function to perform base type handling +// using the tftypes.AttributePathStepper interface. +func SchemaTypeAtTerraformPath(ctx context.Context, s Schema, p *tftypes.AttributePath) (attr.Type, error) { + rawType, remaining, err := tftypes.WalkAttributePath(s, p) + + if err != nil { + return nil, fmt.Errorf("%v still remains in the path: %w", remaining, err) + } + + switch typ := rawType.(type) { + case attr.Type: + return typ, nil + case Attribute: + return typ.GetType(), nil + case Block: + return typ.Type(), nil + case NestedAttributeObject: + return typ.Type(), nil + case NestedBlockObject: + return typ.Type(), nil + case Schema: + return typ.Type(), nil + case UnderlyingAttributes: + return typ.Type(), nil + default: + return nil, fmt.Errorf("got unexpected type %T", rawType) + } +} + +// SchemaType is a helper function to perform base type handling using the +// GetAttributes and GetBlocks methods. +func SchemaType(s Schema) attr.Type { + attrTypes := map[string]attr.Type{} + + for name, attr := range s.GetAttributes() { + attrTypes[name] = attr.GetType() + } + + for name, block := range s.GetBlocks() { + attrTypes[name] = block.Type() + } + + return types.ObjectType{AttrTypes: attrTypes} +} diff --git a/internal/fwschema/underlying_attributes.go b/internal/fwschema/underlying_attributes.go index dc0988638..c1efdf560 100644 --- a/internal/fwschema/underlying_attributes.go +++ b/internal/fwschema/underlying_attributes.go @@ -54,7 +54,7 @@ func (u UnderlyingAttributes) Equal(o UnderlyingAttributes) bool { } // Type returns the framework type of the underlying attributes. -func (u UnderlyingAttributes) Type() attr.Type { +func (u UnderlyingAttributes) Type() types.ObjectTypable { attrTypes := make(map[string]attr.Type, len(u)) for name, attr := range u { diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index 1770467b4..aa21558b4 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -102,6 +102,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } + nestedAttributeObject := nestedAttribute.GetNestedObject() + nm := nestedAttribute.GetNestingMode() switch nm { case fwschema.NestingModeList: @@ -160,7 +162,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo planAttributes := planObject.Attributes() - for name, attr := range nestedAttribute.GetAttributes() { + for name, attr := range nestedAttributeObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -282,7 +284,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo planAttributes := planObject.Attributes() - for name, attr := range nestedAttribute.GetAttributes() { + for name, attr := range nestedAttributeObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -404,7 +406,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo planAttributes := planObject.Attributes() - for name, attr := range nestedAttribute.GetAttributes() { + for name, attr := range nestedAttributeObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -501,7 +503,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo planAttributes := planObject.Attributes() - for name, attr := range nestedAttribute.GetAttributes() { + for name, attr := range nestedAttributeObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index b4d0ea14f..259f0fd2a 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -34,7 +34,7 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req tfsdk.Vali return } - if ok && len(tfsdkAttribute.GetAttributes()) > 0 && tfsdkAttribute.GetNestingMode() == fwschema.NestingModeUnknown { + if ok && tfsdkAttribute.GetNestingMode() == fwschema.NestingModeUnknown && tfsdkAttribute.Attributes != nil { resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid Attribute Definition", @@ -753,6 +753,8 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } + nestedAttributeObject := nestedAttribute.GetNestedObject() + nm := nestedAttribute.GetNestingMode() switch nm { case fwschema.NestingModeList: @@ -776,21 +778,18 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - for idx := range l.Elements() { - for nestedName, nestedAttr := range nestedAttribute.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(idx).AtName(nestedName), - AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(nestedName), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + for idx, value := range l.Elements() { + nestedAttributeObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: value, + AttributePath: req.AttributePath.AtListIndex(idx), + AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), + Config: req.Config, + } + nestedAttributeObjectResp := &tfsdk.ValidateAttributeResponse{} - AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + NestedAttributeObjectValidate(ctx, nestedAttributeObject, nestedAttributeObjectReq, nestedAttributeObjectResp) - resp.Diagnostics = nestedAttrResp.Diagnostics - } + resp.Diagnostics.Append(nestedAttributeObjectResp.Diagnostics...) } case fwschema.NestingModeSet: setVal, ok := req.AttributeConfig.(types.SetValuable) @@ -814,20 +813,17 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute } for _, value := range s.Elements() { - for nestedName, nestedAttr := range nestedAttribute.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(value).AtName(nestedName), - AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(nestedName), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } - - AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics + nestedAttributeObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: value, + AttributePath: req.AttributePath.AtSetValue(value), + AttributePathExpression: req.AttributePathExpression.AtSetValue(value), + Config: req.Config, } + nestedAttributeObjectResp := &tfsdk.ValidateAttributeResponse{} + + NestedAttributeObjectValidate(ctx, nestedAttributeObject, nestedAttributeObjectReq, nestedAttributeObjectResp) + + resp.Diagnostics.Append(nestedAttributeObjectResp.Diagnostics...) } case fwschema.NestingModeMap: mapVal, ok := req.AttributeConfig.(types.MapValuable) @@ -850,21 +846,18 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - for key := range m.Elements() { - for nestedName, nestedAttr := range nestedAttribute.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtMapKey(key).AtName(nestedName), - AttributePathExpression: req.AttributePathExpression.AtMapKey(key).AtName(nestedName), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + for key, value := range m.Elements() { + nestedAttributeObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: value, + AttributePath: req.AttributePath.AtMapKey(key), + AttributePathExpression: req.AttributePathExpression.AtMapKey(key), + Config: req.Config, + } + nestedAttributeObjectResp := &tfsdk.ValidateAttributeResponse{} - AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + NestedAttributeObjectValidate(ctx, nestedAttributeObject, nestedAttributeObjectReq, nestedAttributeObjectResp) - resp.Diagnostics = nestedAttrResp.Diagnostics - } + resp.Diagnostics.Append(nestedAttributeObjectResp.Diagnostics...) } case fwschema.NestingModeSingle: objectVal, ok := req.AttributeConfig.(types.ObjectValuable) @@ -891,20 +884,17 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - for nestedName, nestedAttr := range nestedAttribute.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtName(nestedName), - AttributePathExpression: req.AttributePathExpression.AtName(nestedName), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + nestedAttributeObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: o, + AttributePath: req.AttributePath, + AttributePathExpression: req.AttributePathExpression, + Config: req.Config, + } + nestedAttributeObjectResp := &tfsdk.ValidateAttributeResponse{} - AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + NestedAttributeObjectValidate(ctx, nestedAttributeObject, nestedAttributeObjectReq, nestedAttributeObjectResp) - resp.Diagnostics = nestedAttrResp.Diagnostics - } + resp.Diagnostics.Append(nestedAttributeObjectResp.Diagnostics...) default: err := fmt.Errorf("unknown attribute validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( @@ -916,3 +906,79 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } } + +func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttributeObject, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + objectWithValidators, ok := o.(fwxschema.NestedAttributeObjectWithValidators) + + if ok { + objectVal, ok := req.AttributeConfig.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Walk Error", + "An unexpected error occurred while walking the schema for attribute validation. "+ + "This is an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Unknown attribute value type (%T) at path: %s", req.AttributeConfig, req.AttributePath), + ) + + return + } + + object, diags := objectVal.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have + // errors from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.ObjectRequest{ + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, objectValidator := range objectWithValidators.ObjectValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.ObjectResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + objectValidator.ValidateObject(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + } + + for nestedName, nestedAttr := range o.GetAttributes() { + nestedAttrReq := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + Config: req.Config, + } + nestedAttrResp := &tfsdk.ValidateAttributeResponse{} + + AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics.Append(nestedAttrResp.Diagnostics...) + } +} diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 8dbfc9431..4dab29c43 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" @@ -3513,6 +3514,261 @@ func TestAttributeValidateString(t *testing.T) { } } +func TestNestedAttributeObjectValidateObject(t *testing.T) { + t.Parallel() + + testAttributeConfig := types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ) + testConfig := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.AttributeWithObjectValidators{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Required: true, + }, + }, + }, + } + + testCases := map[string]struct { + object fwschema.NestedAttributeObject + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Config + expected := testConfig + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.ConfigValue + expected := testAttributeConfig + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "nested-attributes-validation": { + object: testschema.NestedAttributeObjectWithValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("test").AtName("testattr"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtName("testattr"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + NestedAttributeObjectValidate(context.Background(), testCase.object, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + var ( testErrorDiagnostic1 = diag.NewErrorDiagnostic( "Error Diagnostic 1", diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index ae935266c..a781b1927 100644 --- a/internal/fwserver/block_plan_modification.go +++ b/internal/fwserver/block_plan_modification.go @@ -61,6 +61,8 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } + nestedBlockObject := b.GetNestedObject() + nm := b.GetNestingMode() switch nm { case fwschema.BlockNestingModeList: @@ -119,7 +121,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr planAttributes := planObject.Attributes() - for name, attr := range b.GetAttributes() { + for name, attr := range nestedBlockObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -169,7 +171,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr resp.Private = attrResp.Private } - for name, block := range b.GetBlocks() { + for name, block := range nestedBlockObject.GetBlocks() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -291,7 +293,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr planAttributes := planObject.Attributes() - for name, attr := range b.GetAttributes() { + for name, attr := range nestedBlockObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -341,7 +343,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr resp.Private = attrResp.Private } - for name, block := range b.GetBlocks() { + for name, block := range nestedBlockObject.GetBlocks() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -438,7 +440,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr planAttributes = make(map[string]attr.Value) } - for name, attr := range b.GetAttributes() { + for name, attr := range nestedBlockObject.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) @@ -488,7 +490,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr resp.Private = attrResp.Private } - for name, block := range b.GetBlocks() { + for name, block := range nestedBlockObject.GetBlocks() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) resp.Diagnostics.Append(diags...) diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index c62c8bbe9..24998d696 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -66,6 +66,8 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr BlockValidateSet(ctx, blockWithValidators, req, resp) } + nestedBlockObject := b.GetNestedObject() + nm := b.GetNestingMode() switch nm { case fwschema.BlockNestingModeList: @@ -89,36 +91,18 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr return } - for idx := range l.Elements() { - for name, attr := range b.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), - AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(name), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } - - AttributeValidate(ctx, attr, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics + for idx, value := range l.Elements() { + nestedBlockObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: value, + AttributePath: req.AttributePath.AtListIndex(idx), + AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), + Config: req.Config, } + nestedBlockObjectResp := &tfsdk.ValidateAttributeResponse{} - for name, block := range b.GetBlocks() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), - AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(name), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } - - BlockValidate(ctx, block, nestedAttrReq, nestedAttrResp) + NestedBlockObjectValidate(ctx, nestedBlockObject, nestedBlockObjectReq, nestedBlockObjectResp) - resp.Diagnostics = nestedAttrResp.Diagnostics - } + resp.Diagnostics.Append(nestedBlockObjectResp.Diagnostics...) } // Terraform 0.12 through 0.15.1 do not implement block MaxItems @@ -165,35 +149,17 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr } for _, value := range s.Elements() { - for name, attr := range b.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(value).AtName(name), - AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(name), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } - - AttributeValidate(ctx, attr, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics + nestedBlockObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: value, + AttributePath: req.AttributePath.AtSetValue(value), + AttributePathExpression: req.AttributePathExpression.AtSetValue(value), + Config: req.Config, } + nestedBlockObjectResp := &tfsdk.ValidateAttributeResponse{} - for name, block := range b.GetBlocks() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(value).AtName(name), - AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(name), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + NestedBlockObjectValidate(ctx, nestedBlockObject, nestedBlockObjectReq, nestedBlockObjectResp) - BlockValidate(ctx, block, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics - } + resp.Diagnostics.Append(nestedBlockObjectResp.Diagnostics...) } // Terraform 0.12 through 0.15.1 do not implement block MaxItems @@ -239,35 +205,17 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr return } - for name, attr := range b.GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtName(name), - AttributePathExpression: req.AttributePathExpression.AtName(name), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } - - AttributeValidate(ctx, attr, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics + nestedBlockObjectReq := tfsdk.ValidateAttributeRequest{ + AttributeConfig: o, + AttributePath: req.AttributePath, + AttributePathExpression: req.AttributePathExpression, + Config: req.Config, } + nestedBlockObjectResp := &tfsdk.ValidateAttributeResponse{} - for name, block := range b.GetBlocks() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtName(name), - AttributePathExpression: req.AttributePathExpression.AtName(name), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + NestedBlockObjectValidate(ctx, nestedBlockObject, nestedBlockObjectReq, nestedBlockObjectResp) - BlockValidate(ctx, block, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics - } + resp.Diagnostics.Append(nestedBlockObjectResp.Diagnostics...) if b.GetMinItems() == 1 && o.IsNull() { resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), 0)) @@ -488,6 +436,95 @@ func BlockValidateSet(ctx context.Context, block fwxschema.BlockWithSetValidator } } +func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + objectWithValidators, ok := o.(fwxschema.NestedBlockObjectWithValidators) + + if ok { + objectVal, ok := req.AttributeConfig.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Block Validation Walk Error", + "An unexpected error occurred while walking the schema for block validation. "+ + "This is an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Unknown block value type (%T) at path: %s", req.AttributeConfig, req.AttributePath), + ) + + return + } + + object, diags := objectVal.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have + // errors from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.ObjectRequest{ + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, objectValidator := range objectWithValidators.ObjectValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.ObjectResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + objectValidator.ValidateObject(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + } + + for nestedName, nestedAttr := range o.GetAttributes() { + nestedAttrReq := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + Config: req.Config, + } + nestedAttrResp := &tfsdk.ValidateAttributeResponse{} + + AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics.Append(nestedAttrResp.Diagnostics...) + } + + for nestedName, nestedBlock := range o.GetBlocks() { + nestedBlockReq := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + Config: req.Config, + } + nestedBlockResp := &tfsdk.ValidateAttributeResponse{} + + BlockValidate(ctx, nestedBlock, nestedBlockReq, nestedBlockResp) + + resp.Diagnostics.Append(nestedBlockResp.Diagnostics...) + } +} + func blockMaxItemsDiagnostic(attrPath path.Path, maxItems int64, elements int) diag.Diagnostic { var details strings.Builder diff --git a/internal/fwserver/block_validation_test.go b/internal/fwserver/block_validation_test.go index e0f4186f5..31dbb59f8 100644 --- a/internal/fwserver/block_validation_test.go +++ b/internal/fwserver/block_validation_test.go @@ -3583,6 +3583,348 @@ func TestBlockValidateSet(t *testing.T) { } } +func TestNestedBlockObjectValidateObject(t *testing.T) { + t.Parallel() + + testAttributeConfig := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{"testblockattr": types.StringType}, + map[string]attr.Value{"testblockattr": types.StringValue("testvalue")}, + ), + }, + ) + testConfig := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + "testblock": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testblockattr": tftypes.String, + }, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + "testblock": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testblockattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + "testblock": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testblockattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testblockattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{ + Required: true, + }, + }, + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testblockattr": testschema.AttributeWithStringValidators{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + } + + testCases := map[string]struct { + object fwschema.NestedBlockObject + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Config + expected := testConfig + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.ConfigValue + expected := testAttributeConfig + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "nested-attributes-validation": { + object: testschema.NestedBlockObjectWithValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("test").AtName("testattr"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtName("testattr"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "nested-blocks-validation": { + object: testschema.NestedBlockObjectWithValidators{ + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + Config: testConfig, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("test").AtName("testblock"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtName("testblock"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + NestedBlockObjectValidate(context.Background(), testCase.object, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestBlockMaxItemsDiagnostic(t *testing.T) { t.Parallel() diff --git a/internal/testing/testschema/blockwithlistvalidators.go b/internal/testing/testschema/blockwithlistvalidators.go index 67604b654..b236d7ed0 100644 --- a/internal/testing/testschema/blockwithlistvalidators.go +++ b/internal/testing/testschema/blockwithlistvalidators.go @@ -38,16 +38,6 @@ func (b BlockWithListValidators) Equal(o fwschema.Block) bool { return fwschema.BlocksEqual(b, o) } -// GetAttributes satisfies the fwschema.Block interface. -func (b BlockWithListValidators) GetAttributes() map[string]fwschema.Attribute { - return nil -} - -// GetBlocks satisfies the fwschema.Block interface. -func (b BlockWithListValidators) GetBlocks() map[string]fwschema.Block { - return nil -} - // GetDeprecationMessage satisfies the fwschema.Block interface. func (b BlockWithListValidators) GetDeprecationMessage() string { return b.DeprecationMessage @@ -73,6 +63,14 @@ func (b BlockWithListValidators) GetMinItems() int64 { return b.MinItems } +// GetNestedObject satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + } +} + // GetNestingMode satisfies the fwschema.Block interface. func (b BlockWithListValidators) GetNestingMode() fwschema.BlockNestingMode { return fwschema.BlockNestingModeList @@ -85,19 +83,7 @@ func (b BlockWithListValidators) ListValidators() []validator.List { // Type satisfies the fwschema.Block interface. func (b BlockWithListValidators) Type() attr.Type { - attrType := types.ObjectType{ - AttrTypes: make(map[string]attr.Type, len(b.GetAttributes())+len(b.GetBlocks())), - } - - for attrName, attr := range b.GetAttributes() { - attrType.AttrTypes[attrName] = attr.GetType() - } - - for blockName, block := range b.GetBlocks() { - attrType.AttrTypes[blockName] = block.Type() - } - return types.ListType{ - ElemType: attrType, + ElemType: b.GetNestedObject().Type(), } } diff --git a/internal/testing/testschema/blockwithobjectvalidators.go b/internal/testing/testschema/blockwithobjectvalidators.go index d5f5a70ab..308c87172 100644 --- a/internal/testing/testschema/blockwithobjectvalidators.go +++ b/internal/testing/testschema/blockwithobjectvalidators.go @@ -5,7 +5,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -38,16 +37,6 @@ func (b BlockWithObjectValidators) Equal(o fwschema.Block) bool { return fwschema.BlocksEqual(b, o) } -// GetAttributes satisfies the fwschema.Block interface. -func (b BlockWithObjectValidators) GetAttributes() map[string]fwschema.Attribute { - return nil -} - -// GetBlocks satisfies the fwschema.Block interface. -func (b BlockWithObjectValidators) GetBlocks() map[string]fwschema.Block { - return nil -} - // GetDeprecationMessage satisfies the fwschema.Block interface. func (b BlockWithObjectValidators) GetDeprecationMessage() string { return b.DeprecationMessage @@ -73,6 +62,15 @@ func (b BlockWithObjectValidators) GetMinItems() int64 { return b.MinItems } +// GetNestedObject satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObjectWithValidators{ + Attributes: b.Attributes, + Blocks: b.Blocks, + Validators: b.Validators, + } +} + // GetNestingMode satisfies the fwschema.Block interface. func (b BlockWithObjectValidators) GetNestingMode() fwschema.BlockNestingMode { return fwschema.BlockNestingModeSingle @@ -85,17 +83,5 @@ func (b BlockWithObjectValidators) ObjectValidators() []validator.Object { // Type satisfies the fwschema.Block interface. func (b BlockWithObjectValidators) Type() attr.Type { - attrType := types.ObjectType{ - AttrTypes: make(map[string]attr.Type, len(b.GetAttributes())+len(b.GetBlocks())), - } - - for attrName, attr := range b.GetAttributes() { - attrType.AttrTypes[attrName] = attr.GetType() - } - - for blockName, block := range b.GetBlocks() { - attrType.AttrTypes[blockName] = block.Type() - } - - return attrType + return b.GetNestedObject().Type() } diff --git a/internal/testing/testschema/blockwithsetvalidators.go b/internal/testing/testschema/blockwithsetvalidators.go index f6b45721f..b4743b3d4 100644 --- a/internal/testing/testschema/blockwithsetvalidators.go +++ b/internal/testing/testschema/blockwithsetvalidators.go @@ -38,16 +38,6 @@ func (b BlockWithSetValidators) Equal(o fwschema.Block) bool { return fwschema.BlocksEqual(b, o) } -// GetAttributes satisfies the fwschema.Block interface. -func (b BlockWithSetValidators) GetAttributes() map[string]fwschema.Attribute { - return nil -} - -// GetBlocks satisfies the fwschema.Block interface. -func (b BlockWithSetValidators) GetBlocks() map[string]fwschema.Block { - return nil -} - // GetDeprecationMessage satisfies the fwschema.Block interface. func (b BlockWithSetValidators) GetDeprecationMessage() string { return b.DeprecationMessage @@ -73,6 +63,14 @@ func (b BlockWithSetValidators) GetMinItems() int64 { return b.MinItems } +// GetNestedObject satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + } +} + // GetNestingMode satisfies the fwschema.Block interface. func (b BlockWithSetValidators) GetNestingMode() fwschema.BlockNestingMode { return fwschema.BlockNestingModeSet @@ -85,19 +83,7 @@ func (b BlockWithSetValidators) SetValidators() []validator.Set { // Type satisfies the fwschema.Block interface. func (b BlockWithSetValidators) Type() attr.Type { - attrType := types.ObjectType{ - AttrTypes: make(map[string]attr.Type, len(b.GetAttributes())+len(b.GetBlocks())), - } - - for attrName, attr := range b.GetAttributes() { - attrType.AttrTypes[attrName] = attr.GetType() - } - - for blockName, block := range b.GetBlocks() { - attrType.AttrTypes[blockName] = block.Type() - } - return types.SetType{ - ElemType: attrType, + ElemType: b.GetNestedObject().Type(), } } diff --git a/internal/testing/testschema/nested_attribute_object.go b/internal/testing/testschema/nested_attribute_object.go new file mode 100644 index 000000000..d743406fa --- /dev/null +++ b/internal/testing/testschema/nested_attribute_object.go @@ -0,0 +1,79 @@ +package testschema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwschema.NestedAttributeObject = NestedAttributeObject{} + +type NestedAttributeObject struct { + Attributes map[string]fwschema.Attribute +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedAttributeObject) ApplyTerraform5AttributePathStep(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) + +} + +// Equal returns true if the given NestedAttributeObject is equivalent. +func (o NestedAttributeObject) Equal(other fwschema.NestedAttributeObject) bool { + if !o.Type().Equal(other.Type()) { + return false + } + + if len(o.GetAttributes()) != len(other.GetAttributes()) { + return false + } + + for name, oAttribute := range o.GetAttributes() { + otherAttribute, ok := other.GetAttributes()[name] + + if !ok { + return false + } + + if !oAttribute.Equal(otherAttribute) { + return false + } + } + + return true +} + +// GetAttributes returns the Attributes field value. +func (o NestedAttributeObject) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// Type returns the framework type of the NestedAttributeObject. +func (o NestedAttributeObject) Type() types.ObjectTypable { + attrTypes := make(map[string]attr.Type, len(o.Attributes)) + + for name, attribute := range o.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/internal/testing/testschema/nested_attribute_object_with_validators.go b/internal/testing/testschema/nested_attribute_object_with_validators.go new file mode 100644 index 000000000..c724f607b --- /dev/null +++ b/internal/testing/testschema/nested_attribute_object_with_validators.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedAttributeObjectWithValidators = NestedAttributeObjectWithValidators{} + +type NestedAttributeObjectWithValidators struct { + Attributes map[string]fwschema.Attribute + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedAttributeObjectWithValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to NestedAttributeObjectWithValidators", step) + } + + attribute, ok := o.GetAttributes()[string(name)] + + if ok { + return attribute, nil + } + + return nil, fmt.Errorf("no attribute %q on NestedAttributeObjectWithValidators", name) + +} + +// Equal returns true if the given NestedAttributeObjectWithValidators is equivalent. +func (o NestedAttributeObjectWithValidators) Equal(other fwschema.NestedAttributeObject) bool { + if !o.Type().Equal(other.Type()) { + return false + } + + if len(o.GetAttributes()) != len(other.GetAttributes()) { + return false + } + + for name, oAttribute := range o.GetAttributes() { + otherAttribute, ok := other.GetAttributes()[name] + + if !ok { + return false + } + + if !oAttribute.Equal(otherAttribute) { + return false + } + } + + return true +} + +// GetAttributes returns the Attributes field value. +func (o NestedAttributeObjectWithValidators) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// ObjectValidators returns the Validators field value. +func (o NestedAttributeObjectWithValidators) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedAttributeObjectWithValidators. +func (o NestedAttributeObjectWithValidators) Type() types.ObjectTypable { + attrTypes := make(map[string]attr.Type, len(o.Attributes)) + + for name, attribute := range o.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/internal/testing/testschema/nested_block_object.go b/internal/testing/testschema/nested_block_object.go new file mode 100644 index 000000000..e978c07ee --- /dev/null +++ b/internal/testing/testschema/nested_block_object.go @@ -0,0 +1,45 @@ +package testschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwschema.NestedBlockObject = NestedBlockObject{} + +type NestedBlockObject struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObject is equivalent. +func (o NestedBlockObject) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObject); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObject) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObject) GetBlocks() map[string]fwschema.Block { + return o.Blocks +} + +// Type returns the framework type of the NestedBlockObject. +func (o NestedBlockObject) Type() types.ObjectTypable { + return fwschema.NestedBlockObjectType(o) +} diff --git a/internal/testing/testschema/nested_block_object_with_validators.go b/internal/testing/testschema/nested_block_object_with_validators.go new file mode 100644 index 000000000..bc0042b21 --- /dev/null +++ b/internal/testing/testschema/nested_block_object_with_validators.go @@ -0,0 +1,53 @@ +package testschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedBlockObjectWithValidators = NestedBlockObjectWithValidators{} + +type NestedBlockObjectWithValidators struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObjectWithValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObjectWithValidators is equivalent. +func (o NestedBlockObjectWithValidators) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObjectWithValidators); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObjectWithValidators) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObjectWithValidators) GetBlocks() map[string]fwschema.Block { + return o.Blocks +} + +// ObjectValidators returns the Validators field value. +func (o NestedBlockObjectWithValidators) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedBlockObjectWithValidators. +func (o NestedBlockObjectWithValidators) Type() types.ObjectTypable { + return fwschema.NestedBlockObjectType(o) +} diff --git a/internal/testing/testschema/schema.go b/internal/testing/testschema/schema.go new file mode 100644 index 000000000..a3b42f1af --- /dev/null +++ b/internal/testing/testschema/schema.go @@ -0,0 +1,82 @@ +package testschema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwschema.Schema = Schema{} + +type Schema struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + Version int64 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Schema interface. +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// AttributeAtPath satisfies the fwschema.Schema interface. +func (s Schema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) +} + +// AttributeAtTerraformPath satisfies the fwschema.Schema interface. +func (s Schema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes satisfies the fwschema.Schema interface. +func (s Schema) GetAttributes() map[string]fwschema.Attribute { + return s.Attributes +} + +// GetBlocks satisfies the fwschema.Schema interface. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return s.Blocks +} + +// GetDeprecationMessage satisfies the fwschema.Schema interface. +func (s Schema) GetDeprecationMessage() string { + return s.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Schema interface. +func (s Schema) GetDescription() string { + return s.Description +} + +// GetMarkdownDescription satisfies the fwschema.Schema interface. +func (s Schema) GetMarkdownDescription() string { + return s.MarkdownDescription +} + +// GetVersion satisfies the fwschema.Schema interface. +func (s Schema) GetVersion() int64 { + return s.Version +} + +// Type satisfies the fwschema.Schema interface. +func (s Schema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath satisfies the fwschema.Schema interface. +func (s Schema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) +} + +// TypeAtTerraformPath satisfies the fwschema.Schema interface. +func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} diff --git a/internal/toproto5/block.go b/internal/toproto5/block.go index 660a08b44..e35e4816c 100644 --- a/internal/toproto5/block.go +++ b/internal/toproto5/block.go @@ -44,7 +44,9 @@ func Block(ctx context.Context, name string, path *tftypes.AttributePath, b fwsc return nil, path.NewErrorf("unrecognized nesting mode %v", nm) } - for attrName, attr := range b.GetAttributes() { + nestedBlockObject := b.GetNestedObject() + + for attrName, attr := range nestedBlockObject.GetAttributes() { attrPath := path.WithAttributeName(attrName) attrProto5, err := SchemaAttribute(ctx, attrName, attrPath, attr) @@ -55,7 +57,7 @@ func Block(ctx context.Context, name string, path *tftypes.AttributePath, b fwsc schemaNestedBlock.Block.Attributes = append(schemaNestedBlock.Block.Attributes, attrProto5) } - for blockName, block := range b.GetBlocks() { + for blockName, block := range nestedBlockObject.GetBlocks() { blockPath := path.WithAttributeName(blockName) blockProto5, err := Block(ctx, blockName, blockPath, block) diff --git a/internal/toproto5/schema_attribute.go b/internal/toproto5/schema_attribute.go index 6c6d40c32..86c94ea19 100644 --- a/internal/toproto5/schema_attribute.go +++ b/internal/toproto5/schema_attribute.go @@ -21,7 +21,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa return nil, path.NewErrorf("protocol version 5 cannot have Attributes set") } - if tfsdkAttribute.GetNestingMode() != fwschema.NestingModeUnknown || len(tfsdkAttribute.GetAttributes()) > 0 { + if tfsdkAttribute.GetNestingMode() != fwschema.NestingModeUnknown || tfsdkAttribute.Attributes != nil { return nil, path.NewErrorf("protocol version 5 cannot have Attributes set") } } diff --git a/internal/toproto6/block.go b/internal/toproto6/block.go index 20db4dcd1..0909dede0 100644 --- a/internal/toproto6/block.go +++ b/internal/toproto6/block.go @@ -44,7 +44,9 @@ func Block(ctx context.Context, name string, path *tftypes.AttributePath, b fwsc return nil, path.NewErrorf("unrecognized nesting mode %v", nm) } - for attrName, attr := range b.GetAttributes() { + nestedBlockObject := b.GetNestedObject() + + for attrName, attr := range nestedBlockObject.GetAttributes() { attrPath := path.WithAttributeName(attrName) attrProto6, err := SchemaAttribute(ctx, attrName, attrPath, attr) @@ -55,7 +57,7 @@ func Block(ctx context.Context, name string, path *tftypes.AttributePath, b fwsc schemaNestedBlock.Block.Attributes = append(schemaNestedBlock.Block.Attributes, attrProto6) } - for blockName, block := range b.GetBlocks() { + for blockName, block := range nestedBlockObject.GetBlocks() { blockPath := path.WithAttributeName(blockName) blockProto6, err := Block(ctx, blockName, blockPath, block) diff --git a/internal/toproto6/schema_attribute.go b/internal/toproto6/schema_attribute.go index 64334617f..30ef06da3 100644 --- a/internal/toproto6/schema_attribute.go +++ b/internal/toproto6/schema_attribute.go @@ -16,7 +16,7 @@ import ( func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePath, a fwschema.Attribute) (*tfprotov6.SchemaAttribute, error) { tfsdkAttribute, ok := a.(tfsdk.Attribute) - if ok && tfsdkAttribute.GetNestingMode() == fwschema.NestingModeUnknown && len(tfsdkAttribute.GetAttributes()) > 0 { + if ok && tfsdkAttribute.GetNestingMode() == fwschema.NestingModeUnknown && tfsdkAttribute.Attributes != nil { return nil, path.NewErrorf("cannot have both Attributes and Type set") } @@ -24,7 +24,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa return nil, path.NewErrorf("must have Attributes or Type set") } - if ok && tfsdkAttribute.GetNestingMode() != fwschema.NestingModeUnknown && len(tfsdkAttribute.GetAttributes()) == 0 { + if ok && tfsdkAttribute.GetNestingMode() != fwschema.NestingModeUnknown && (tfsdkAttribute.Attributes == nil || len(tfsdkAttribute.Attributes.GetAttributes()) == 0) { return nil, path.NewErrorf("must have Attributes or Type set") } @@ -80,7 +80,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa return nil, path.NewErrorf("unrecognized nesting mode %v", nm) } - for nestedName, nestedA := range nestedAttribute.GetAttributes() { + for nestedName, nestedA := range nestedAttribute.GetNestedObject().GetAttributes() { nestedSchemaAttribute, err := SchemaAttribute(ctx, nestedName, path.WithAttributeName(nestedName), nestedA) if err != nil { diff --git a/tfsdk/attribute.go b/tfsdk/attribute.go index ef656caee..b18c5967e 100644 --- a/tfsdk/attribute.go +++ b/tfsdk/attribute.go @@ -173,49 +173,15 @@ func (a Attribute) Equal(o fwschema.Attribute) bool { return false } - if a.GetAttributes() != nil && !a.GetAttributes().Equal(other.GetAttributes()) { + if a.GetNestedObject() != nil && !a.GetNestedObject().Equal(other.GetNestedObject()) { return false } - if other.GetAttributes() != nil && !other.GetAttributes().Equal(a.GetAttributes()) { + if other.GetNestedObject() != nil && !other.GetNestedObject().Equal(a.GetNestedObject()) { return false } - if !a.GetType().Equal(other.GetType()) { - return false - } - - if a.GetDescription() != o.GetDescription() { - return false - } - if a.GetMarkdownDescription() != o.GetMarkdownDescription() { - return false - } - if a.IsRequired() != o.IsRequired() { - return false - } - if a.IsOptional() != o.IsOptional() { - return false - } - if a.IsComputed() != o.IsComputed() { - return false - } - if a.IsSensitive() != o.IsSensitive() { - return false - } - if a.GetDeprecationMessage() != o.GetDeprecationMessage() { - return false - } - return true -} - -// GetAttributes satisfies the fwschema.Attribute interface. -func (a Attribute) GetAttributes() fwschema.UnderlyingAttributes { - if a.Attributes != nil { - return a.Attributes.GetAttributes() - } - - return nil + return fwschema.AttributesEqual(a, o) } // GetDeprecationMessage satisfies the fwschema.Attribute interface. @@ -233,6 +199,18 @@ func (a Attribute) GetMarkdownDescription() string { return a.MarkdownDescription } +// GetNestedObject returns a generated NestedAttributeObject if the +// Attribute represents nested attributes, otherwise nil. +func (a Attribute) GetNestedObject() fwschema.NestedAttributeObject { + if a.GetNestingMode() == fwschema.NestingModeUnknown { + return nil + } + + return nestedAttributeObject{ + Attributes: a.Attributes.GetAttributes(), + } +} + // GetNestingMode returns the Attributes nesting mode, if set. Otherwise, // returns NestingModeUnset. func (a Attribute) GetNestingMode() fwschema.NestingMode { diff --git a/tfsdk/block.go b/tfsdk/block.go index 4eaed2de0..3f30fa5a9 100644 --- a/tfsdk/block.go +++ b/tfsdk/block.go @@ -3,7 +3,6 @@ package tfsdk import ( "fmt" - "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -106,7 +105,7 @@ func (b Block) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) return nil, fmt.Errorf("can't apply %T to block NestingModeList", step) } - return fwschema.NestedBlock{Block: b}, nil + return b.GetNestedObject(), nil case BlockNestingModeSet: _, ok := step.(tftypes.ElementKeyValue) @@ -114,7 +113,7 @@ func (b Block) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) return nil, fmt.Errorf("can't apply %T to block NestingModeSet", step) } - return fwschema.NestedBlock{Block: b}, nil + return b.GetNestedObject(), nil case BlockNestingModeSingle: _, ok := step.(tftypes.AttributeName) @@ -122,7 +121,7 @@ func (b Block) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) return nil, fmt.Errorf("can't apply %T to block NestingModeSingle", step) } - return fwschema.NestedBlock{Block: b}.ApplyTerraform5AttributePathStep(step) + return b.GetNestedObject().ApplyTerraform5AttributePathStep(step) default: return nil, fmt.Errorf("unsupported block nesting mode: %v", b.NestingMode) } @@ -130,41 +129,7 @@ func (b Block) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) // Equal returns true if `b` and `o` should be considered Equal. func (b Block) Equal(o fwschema.Block) bool { - if !cmp.Equal(b.GetAttributes(), o.GetAttributes()) { - return false - } - if !cmp.Equal(b.GetBlocks(), o.GetBlocks()) { - return false - } - if b.GetDeprecationMessage() != o.GetDeprecationMessage() { - return false - } - if b.GetDescription() != o.GetDescription() { - return false - } - if b.GetMarkdownDescription() != o.GetMarkdownDescription() { - return false - } - if b.GetMaxItems() != o.GetMaxItems() { - return false - } - if b.GetMinItems() != o.GetMinItems() { - return false - } - if b.GetNestingMode() != o.GetNestingMode() { - return false - } - return true -} - -// GetAttributes satisfies the fwschema.Block interface. -func (b Block) GetAttributes() map[string]fwschema.Attribute { - return schemaAttributes(b.Attributes) -} - -// GetBlocks satisfies the fwschema.Block interface. -func (b Block) GetBlocks() map[string]fwschema.Block { - return schemaBlocks(b.Blocks) + return fwschema.BlocksEqual(b, o) } // GetDeprecationMessage satisfies the fwschema.Block interface. @@ -192,6 +157,15 @@ func (b Block) GetMinItems() int64 { return b.MinItems } +// GetNestedObject returns a generated NestedBlockObject from the +// Attributes and Blocks field values. +func (b Block) GetNestedObject() fwschema.NestedBlockObject { + return nestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + } +} + // GetNestingMode satisfies the fwschema.Block interface. func (b Block) GetNestingMode() fwschema.BlockNestingMode { return b.NestingMode diff --git a/tfsdk/nested_attribute_object.go b/tfsdk/nested_attribute_object.go new file mode 100644 index 000000000..ff22671bf --- /dev/null +++ b/tfsdk/nested_attribute_object.go @@ -0,0 +1,44 @@ +package tfsdk + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwschema.NestedAttributeObject = nestedAttributeObject{} + +// nestedAttributeObject is the object containing the underlying attributes +// for an Attribute with nested attributes. This is a temporary type until +// Attribute is removed. +type nestedAttributeObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]fwschema.Attribute +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o nestedAttributeObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedAttributeObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given nestedAttributeObject is equivalent. +func (o nestedAttributeObject) Equal(other fwschema.NestedAttributeObject) bool { + if _, ok := other.(nestedAttributeObject); !ok { + return false + } + + return fwschema.NestedAttributeObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o nestedAttributeObject) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// Type returns the framework type of the nestedAttributeObject. +func (o nestedAttributeObject) Type() types.ObjectTypable { + return fwschema.NestedAttributeObjectType(o) +} diff --git a/tfsdk/nested_block_object.go b/tfsdk/nested_block_object.go new file mode 100644 index 000000000..4af45c46b --- /dev/null +++ b/tfsdk/nested_block_object.go @@ -0,0 +1,60 @@ +package tfsdk + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwschema.NestedBlockObject = nestedBlockObject{} + +// nestedBlockObject is the object containing the underlying attributes and +// blocks for Block. It is a temporary type until Block is removed. +// +// This object enables customizing and simplifying details within its parent +// Block, therefore it cannot have Terraform schema fields such as Description, +// etc. +type nestedBlockObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o nestedBlockObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given nestedBlockObject is equivalent. +func (o nestedBlockObject) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(nestedBlockObject); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o nestedBlockObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// GetAttributes returns the Blocks field value. +func (o nestedBlockObject) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(o.Blocks) +} + +// Type returns the framework type of the nestedBlockObject. +func (o nestedBlockObject) Type() types.ObjectTypable { + return fwschema.NestedBlockObjectType(o) +} diff --git a/tfsdk/schema.go b/tfsdk/schema.go index 3ab5763a2..848292f13 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -2,15 +2,11 @@ package tfsdk import ( "context" - "errors" - "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" - "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -19,11 +15,11 @@ var ( // 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") + ErrPathInsideAtomicAttribute = fwschema.ErrPathInsideAtomicAttribute // 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") + ErrPathIsBlock = fwschema.ErrPathIsBlock ) // Schema must satify the fwschema.Schema interface. @@ -82,77 +78,17 @@ type Schema struct { // ApplyTerraform5AttributePathStep applies the given AttributePathStep to the // schema. func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { - a, ok := step.(tftypes.AttributeName) - - if !ok { - return nil, fmt.Errorf("cannot apply AttributePathStep %T to schema", step) - } - - attrName := string(a) - - if attr, ok := s.Attributes[attrName]; ok { - return attr, nil - } - - if block, ok := s.Blocks[attrName]; ok { - return block, nil - } - - return nil, fmt.Errorf("could not find attribute or block %q in schema", a) + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) } // TypeAtPath returns the framework type at the given schema path. -func (s Schema) TypeAtPath(ctx context.Context, schemaPath path.Path) (attr.Type, diag.Diagnostics) { - var diags diag.Diagnostics - - tftypesPath, tftypesDiags := totftypes.AttributePath(ctx, schemaPath) - - diags.Append(tftypesDiags...) - - if diags.HasError() { - return nil, diags - } - - attrType, err := s.TypeAtTerraformPath(ctx, tftypesPath) - - if err != nil { - diags.AddAttributeError( - schemaPath, - "Invalid Schema Path", - "When attempting to get the framework type associated with a schema path, an unexpected error was returned. "+ - "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ - fmt.Sprintf("Path: %s\n", schemaPath.String())+ - fmt.Sprintf("Original Error: %s", err), - ) - return nil, diags - } - - return attrType, diags +func (s Schema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) } // TypeAtTerraformPath returns the framework type at the given tftypes path. -func (s Schema) TypeAtTerraformPath(_ context.Context, path *tftypes.AttributePath) (attr.Type, error) { - rawType, remaining, err := tftypes.WalkAttributePath(s, path) - if err != nil { - return nil, fmt.Errorf("%v still remains in the path: %w", remaining, err) - } - - switch typ := rawType.(type) { - case attr.Type: - return typ, nil - case fwschema.UnderlyingAttributes: - return typ.Type(), nil - case fwschema.NestedBlock: - return typ.Block.Type(), nil - case Attribute: - return typ.GetType(), nil - case Block: - return typ.Type(), nil - case Schema: - return typ.Type(), nil - default: - return nil, fmt.Errorf("got unexpected type %T", rawType) - } +func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) } // GetAttributes satisfies the fwschema.Schema interface. @@ -187,73 +123,21 @@ func (s Schema) GetVersion() int64 { // Type returns the framework type of the schema. func (s Schema) Type() attr.Type { - attrTypes := map[string]attr.Type{} - - for name, attr := range s.Attributes { - attrTypes[name] = attr.GetType() - } - - for name, block := range s.Blocks { - attrTypes[name] = block.Type() - } - - return types.ObjectType{AttrTypes: attrTypes} + return fwschema.SchemaType(s) } // AttributeAtPath returns the Attribute at the passed path. If the path points // to an element or attribute of a complex type, rather than to an Attribute, // it will return an ErrPathInsideAtomicAttribute error. -func (s Schema) AttributeAtPath(ctx context.Context, schemaPath path.Path) (fwschema.Attribute, diag.Diagnostics) { - var diags diag.Diagnostics - - tftypesPath, tftypesDiags := totftypes.AttributePath(ctx, schemaPath) - - diags.Append(tftypesDiags...) - - if diags.HasError() { - return nil, diags - } - - attribute, err := s.AttributeAtTerraformPath(ctx, tftypesPath) - - if err != nil { - diags.AddAttributeError( - schemaPath, - "Invalid Schema Path", - "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ - "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ - fmt.Sprintf("Path: %s\n", schemaPath.String())+ - fmt.Sprintf("Original Error: %s", err), - ) - return nil, diags - } - - return attribute, diags +func (s Schema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) } // AttributeAtPath returns the Attribute at the passed path. If the path points // to an element or attribute of a complex type, rather than to an Attribute, // it will return an ErrPathInsideAtomicAttribute error. -func (s Schema) AttributeAtTerraformPath(_ context.Context, path *tftypes.AttributePath) (fwschema.Attribute, error) { - res, remaining, err := tftypes.WalkAttributePath(s, path) - if err != nil { - return Attribute{}, fmt.Errorf("%v still remains in the path: %w", remaining, err) - } - - switch r := res.(type) { - case attr.Type: - return Attribute{}, ErrPathInsideAtomicAttribute - case fwschema.UnderlyingAttributes: - return Attribute{}, ErrPathInsideAtomicAttribute - case fwschema.NestedBlock: - return Attribute{}, ErrPathInsideAtomicAttribute - case fwschema.Attribute: - return r, nil - case Block: - return Attribute{}, ErrPathIsBlock - default: - return Attribute{}, fmt.Errorf("got unexpected type %T", res) - } +func (s Schema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) } // schemaAttributes is a tfsdk to fwschema type conversion function.