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.