diff --git a/action/doc.go b/action/doc.go index 351e8b352..804b5f0d5 100644 --- a/action/doc.go +++ b/action/doc.go @@ -1,5 +1,15 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// TODO:Actions: Eventual package docs for actions +// Package action contains all interfaces, request types, and response +// types for an action implementation. +// +// In Terraform, an action is a concept which enables provider developers +// to offer practitioners ad-hoc side-effects to be used in their configuration. +// +// The main starting point for implementations in this package is the +// [Action] type which represents an instance of an action that has its +// own configuration, plan, and invoke logic. The [Action] implementations +// are referenced by the [provider.ProviderWithActions] type Actions method, +// which enables the action practitioner usage. package action diff --git a/action/invoke.go b/action/invoke.go index 65be360fb..2e86a509e 100644 --- a/action/invoke.go +++ b/action/invoke.go @@ -29,9 +29,7 @@ type InvokeResponse struct { Diagnostics diag.Diagnostics // SendProgress will immediately send a progress update to Terraform core during action invocation. - // This function is provided by the framework and can be called multiple times while action logic is running. - // - // TODO:Actions: More documentation about when you should use this / when you shouldn't + // This function is pre-populated by the framework and can be called multiple times while action logic is running. SendProgress func(event InvokeProgressEvent) // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented diff --git a/action/schema/unlinked_schema_test.go b/action/schema/unlinked_schema_test.go index 1af3164f7..881e1d6a3 100644 --- a/action/schema/unlinked_schema_test.go +++ b/action/schema/unlinked_schema_test.go @@ -406,7 +406,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/datasource/schema/schema_test.go b/datasource/schema/schema_test.go index a6eb8ee5c..36bbd47c4 100644 --- a/datasource/schema/schema_test.go +++ b/datasource/schema/schema_test.go @@ -406,7 +406,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/ephemeral/schema/schema_test.go b/ephemeral/schema/schema_test.go index e08d76a5d..d6212aff3 100644 --- a/ephemeral/schema/schema_test.go +++ b/ephemeral/schema/schema_test.go @@ -406,7 +406,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/fromproto5/identity_schema.go b/internal/fromproto5/identity_schema.go new file mode 100644 index 000000000..be90546fb --- /dev/null +++ b/internal/fromproto5/identity_schema.go @@ -0,0 +1,79 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// IdentitySchema converts a *tfprotov5.ResourceIdentitySchema into a resource/identityschema Schema, used for +// converting protocol identity schemas (from another provider server, such as SDKv2 or terraform-plugin-go) +// into Framework identity schemas. +func IdentitySchema(ctx context.Context, s *tfprotov5.ResourceIdentitySchema) (*identityschema.Schema, error) { + if s == nil { + return nil, nil + } + + attrs, err := IdentitySchemaAttributes(ctx, s.IdentityAttributes) + if err != nil { + return nil, err + } + + return &identityschema.Schema{ + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol identity schema + // to the resource identity schema, just enough data to allow provider developers to read and set data. + Attributes: attrs, + }, nil +} + +func IdentitySchemaAttributes(ctx context.Context, protoAttrs []*tfprotov5.ResourceIdentitySchemaAttribute) (map[string]identityschema.Attribute, error) { + attrs := make(map[string]identityschema.Attribute, len(protoAttrs)) + for _, protoAttr := range protoAttrs { + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol identity schema + // to the resource identity schema, just enough data to allow provider developers to read and set data. + switch { + case protoAttr.Type.Is(tftypes.Bool): + attrs[protoAttr.Name] = identityschema.BoolAttribute{ + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + case protoAttr.Type.Is(tftypes.Number): + attrs[protoAttr.Name] = identityschema.NumberAttribute{ + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + case protoAttr.Type.Is(tftypes.String): + attrs[protoAttr.Name] = identityschema.StringAttribute{ + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + case protoAttr.Type.Is(tftypes.List{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + l := protoAttr.Type.(tftypes.List) + + elementType, err := basetypes.TerraformTypeToFrameworkType(l.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = identityschema.ListAttribute{ + ElementType: elementType, + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + default: + // MAINTAINER NOTE: Not all terraform types are valid identity attribute types. Framework fully supports + // all of the possible identity attribute types, so any errors here would be invalid protocol identities. + return nil, fmt.Errorf("no supported identity attribute for %q, type: %T", protoAttr.Name, protoAttr.Type) + } + } + + return attrs, nil +} diff --git a/internal/fromproto5/identity_schema_test.go b/internal/fromproto5/identity_schema_test.go new file mode 100644 index 000000000..a9c4bcbf0 --- /dev/null +++ b/internal/fromproto5/identity_schema_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchema(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.ResourceIdentitySchema + expected *identityschema.Schema + expectedErr string + }{ + "nil": { + input: nil, + expected: nil, + }, + "no-attrs": { + input: &tfprotov5.ResourceIdentitySchema{}, + expected: &identityschema.Schema{ + Attributes: make(map[string]identityschema.Attribute, 0), + }, + }, + "primitives-attrs": { + input: &tfprotov5.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + RequiredForImport: true, + }, + { + Name: "number", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "string", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + }, + expected: &identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + "number": identityschema.NumberAttribute{ + OptionalForImport: true, + }, + "string": identityschema.StringAttribute{ + OptionalForImport: true, + }, + }, + }, + }, + "list-attr": { + input: &tfprotov5.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "list_of_bools", + Type: tftypes.List{ElementType: tftypes.Bool}, + RequiredForImport: true, + }, + }, + }, + expected: &identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "list_of_bools": identityschema.ListAttribute{ + ElementType: basetypes.BoolType{}, + RequiredForImport: true, + }, + }, + }, + }, + "map-error": { + input: &tfprotov5.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "map_of_strings", + Type: tftypes.Map{ElementType: tftypes.String}, + OptionalForImport: true, + }, + }, + }, + expectedErr: `no supported identity attribute for "map_of_strings", type: tftypes.Map`, + }, + "set-error": { + input: &tfprotov5.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + OptionalForImport: true, + }, + }, + }, + expectedErr: `no supported identity attribute for "set_of_strings", type: tftypes.Set`, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := fromproto5.IdentitySchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/fromproto5/resource_schema.go b/internal/fromproto5/resource_schema.go new file mode 100644 index 000000000..26ab8b064 --- /dev/null +++ b/internal/fromproto5/resource_schema.go @@ -0,0 +1,208 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ResourceSchema converts a *tfprotov5.Schema into a resource/schema Schema, used for +// converting protocol schemas (from another provider server, such as SDKv2 or terraform-plugin-go) +// into Framework schemas. +func ResourceSchema(ctx context.Context, s *tfprotov5.Schema) (*resourceschema.Schema, error) { + if s == nil || s.Block == nil { + return nil, nil + } + + attrs, err := ResourceSchemaAttributes(ctx, s.Block.Attributes) + if err != nil { + return nil, err + } + + blocks, err := ResourceSchemaNestedBlocks(ctx, s.Block.BlockTypes) + if err != nil { + return nil, err + } + + return &resourceschema.Schema{ + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol schema + // to the resource schema, just enough data to allow provider developers to read and set data. + Attributes: attrs, + Blocks: blocks, + }, nil +} + +func ResourceSchemaAttributes(ctx context.Context, protoAttrs []*tfprotov5.SchemaAttribute) (map[string]resourceschema.Attribute, error) { + attrs := make(map[string]resourceschema.Attribute, len(protoAttrs)) + for _, protoAttr := range protoAttrs { + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol schema + // to the resource schema, just enough data to allow provider developers to read and set data. + switch { + case protoAttr.Type.Is(tftypes.Bool): + attrs[protoAttr.Name] = resourceschema.BoolAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Number): + attrs[protoAttr.Name] = resourceschema.NumberAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.String): + attrs[protoAttr.Name] = resourceschema.StringAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.DynamicPseudoType): + attrs[protoAttr.Name] = resourceschema.DynamicAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.List{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + l := protoAttr.Type.(tftypes.List) + + elementType, err := basetypes.TerraformTypeToFrameworkType(l.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = resourceschema.ListAttribute{ + ElementType: elementType, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Map{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + m := protoAttr.Type.(tftypes.Map) + + elementType, err := basetypes.TerraformTypeToFrameworkType(m.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = resourceschema.MapAttribute{ + ElementType: elementType, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Set{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + s := protoAttr.Type.(tftypes.Set) + + elementType, err := basetypes.TerraformTypeToFrameworkType(s.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = resourceschema.SetAttribute{ + ElementType: elementType, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Object{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + o := protoAttr.Type.(tftypes.Object) + + attrTypes := make(map[string]attr.Type, len(o.AttributeTypes)) + for name, tfType := range o.AttributeTypes { + t, err := basetypes.TerraformTypeToFrameworkType(tfType) + if err != nil { + return nil, err + } + attrTypes[name] = t + } + + attrs[protoAttr.Name] = resourceschema.ObjectAttribute{ + AttributeTypes: attrTypes, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + default: + // MAINTAINER NOTE: Currently the only type not supported by Framework is a tuple, since there + // is no corresponding attribute to represent it. + // + // https://github.com/hashicorp/terraform-plugin-framework/issues/54 + return nil, fmt.Errorf("no supported attribute for %q, type: %T", protoAttr.Name, protoAttr.Type) + } + } + + return attrs, nil +} + +func ResourceSchemaNestedBlocks(ctx context.Context, protoBlocks []*tfprotov5.SchemaNestedBlock) (map[string]resourceschema.Block, error) { + nestedBlocks := make(map[string]resourceschema.Block, len(protoBlocks)) + for _, protoBlock := range protoBlocks { + if protoBlock.Block == nil { + continue + } + + attrs, err := ResourceSchemaAttributes(ctx, protoBlock.Block.Attributes) + if err != nil { + return nil, err + } + blocks, err := ResourceSchemaNestedBlocks(ctx, protoBlock.Block.BlockTypes) + if err != nil { + return nil, err + } + + switch protoBlock.Nesting { + case tfprotov5.SchemaNestedBlockNestingModeList: + nestedBlocks[protoBlock.TypeName] = resourceschema.ListNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: attrs, + Blocks: blocks, + }, + } + case tfprotov5.SchemaNestedBlockNestingModeSet: + nestedBlocks[protoBlock.TypeName] = resourceschema.SetNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: attrs, + Blocks: blocks, + }, + } + case tfprotov5.SchemaNestedBlockNestingModeSingle: + nestedBlocks[protoBlock.TypeName] = resourceschema.SingleNestedBlock{ + Attributes: attrs, + Blocks: blocks, + } + default: + // MAINTAINER NOTE: Currently the only block type not supported by Framework is a map nested block, since there + // is no corresponding framework block implementation to represent it. + return nil, fmt.Errorf("no supported block for nesting mode %v in nested block %q", protoBlock.Nesting, protoBlock.TypeName) + } + } + + return nestedBlocks, nil +} diff --git a/internal/fromproto5/resource_schema_test.go b/internal/fromproto5/resource_schema_test.go new file mode 100644 index 000000000..685f87e1c --- /dev/null +++ b/internal/fromproto5/resource_schema_test.go @@ -0,0 +1,455 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceSchema(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.Schema + expected *resourceschema.Schema + expectedErr string + }{ + "nil": { + input: nil, + expected: nil, + }, + "no-block": { + input: &tfprotov5.Schema{}, + expected: nil, + }, + "no-attrs-no-nested-blocks": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + expected: &resourceschema.Schema{ + Attributes: make(map[string]resourceschema.Attribute, 0), + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "primitives-attrs": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + { + Name: "number", + Type: tftypes.Number, + Optional: true, + Computed: true, + }, + { + Name: "string", + Type: tftypes.String, + Optional: true, + Computed: true, + Sensitive: true, + }, + { + Name: "dynamic", + Type: tftypes.DynamicPseudoType, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + "number": resourceschema.NumberAttribute{ + Optional: true, + Computed: true, + }, + "string": resourceschema.StringAttribute{ + Optional: true, + Computed: true, + Sensitive: true, + }, + "dynamic": resourceschema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "collection-attrs": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "list_of_bools", + Type: tftypes.List{ElementType: tftypes.Bool}, + Required: true, + WriteOnly: true, + }, + { + Name: "map_of_numbers", + Type: tftypes.Map{ElementType: tftypes.Number}, + Optional: true, + Computed: true, + }, + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + Optional: true, + Computed: true, + Sensitive: true, + }, + { + Name: "list_of_objects", + Type: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "dynamic": tftypes.DynamicPseudoType, + "string": tftypes.String, + }, + }, + }, + Required: true, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "list_of_bools": resourceschema.ListAttribute{ + ElementType: basetypes.BoolType{}, + Required: true, + WriteOnly: true, + }, + "map_of_numbers": resourceschema.MapAttribute{ + ElementType: basetypes.NumberType{}, + Optional: true, + Computed: true, + }, + "set_of_strings": resourceschema.SetAttribute{ + ElementType: basetypes.StringType{}, + Optional: true, + Computed: true, + Sensitive: true, + }, + "list_of_objects": resourceschema.ListAttribute{ + ElementType: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "dynamic": basetypes.DynamicType{}, + "string": basetypes.StringType{}, + }, + }, + Required: true, + }, + }, + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "object-attr": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "object", + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + "dynamic": tftypes.DynamicPseudoType, + "string": tftypes.String, + }, + }, + Optional: true, + Computed: true, + Sensitive: true, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "object": resourceschema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "bool": basetypes.BoolType{}, + "dynamic": basetypes.DynamicType{}, + "string": basetypes.StringType{}, + }, + Optional: true, + Computed: true, + Sensitive: true, + }, + }, + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "tuple-error": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "tuple", + Type: tftypes.Tuple{ + ElementTypes: []tftypes.Type{ + tftypes.Bool, + tftypes.Number, + tftypes.String, + }, + }, + Required: true, + }, + }, + }, + }, + expectedErr: `no supported attribute for "tuple", type: tftypes.Tuple`, + }, + "list-block": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "list_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "list_of_strings", + Type: tftypes.List{ElementType: tftypes.String}, + Computed: true, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "nested_list_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "list_block": resourceschema.ListNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "list_of_strings": resourceschema.ListAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + }, + Blocks: map[string]resourceschema.Block{ + "nested_list_block": resourceschema.ListNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + "set-block": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "set_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + Computed: true, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "nested_set_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "set_block": resourceschema.SetNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "set_of_strings": resourceschema.SetAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + }, + Blocks: map[string]resourceschema.Block{ + "nested_set_block": resourceschema.SetNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + "single-block": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "single_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "map_of_strings", + Type: tftypes.Map{ElementType: tftypes.String}, + Computed: true, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "nested_single_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "single_block": resourceschema.SingleNestedBlock{ + Attributes: map[string]resourceschema.Attribute{ + "map_of_strings": resourceschema.MapAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + }, + Blocks: map[string]resourceschema.Block{ + "nested_single_block": resourceschema.SingleNestedBlock{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + "map-block": { + input: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "map_block", + Nesting: tfprotov5.SchemaNestedBlockNestingModeMap, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedErr: `no supported block for nesting mode MAP in nested block "map_block"`, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := fromproto5.ResourceSchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/fromproto6/identity_schema.go b/internal/fromproto6/identity_schema.go new file mode 100644 index 000000000..9051e2930 --- /dev/null +++ b/internal/fromproto6/identity_schema.go @@ -0,0 +1,79 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// IdentitySchema converts a *tfprotov6.ResourceIdentitySchema into a resource/identityschema Schema, used for +// converting protocol identity schemas (from another provider server, such as terraform-plugin-go) +// into Framework identity schemas. +func IdentitySchema(ctx context.Context, s *tfprotov6.ResourceIdentitySchema) (*identityschema.Schema, error) { + if s == nil { + return nil, nil + } + + attrs, err := IdentitySchemaAttributes(ctx, s.IdentityAttributes) + if err != nil { + return nil, err + } + + return &identityschema.Schema{ + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol identity schema + // to the resource identity schema, just enough data to allow provider developers to read and set data. + Attributes: attrs, + }, nil +} + +func IdentitySchemaAttributes(ctx context.Context, protoAttrs []*tfprotov6.ResourceIdentitySchemaAttribute) (map[string]identityschema.Attribute, error) { + attrs := make(map[string]identityschema.Attribute, len(protoAttrs)) + for _, protoAttr := range protoAttrs { + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol identity schema + // to the resource identity schema, just enough data to allow provider developers to read and set data. + switch { + case protoAttr.Type.Is(tftypes.Bool): + attrs[protoAttr.Name] = identityschema.BoolAttribute{ + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + case protoAttr.Type.Is(tftypes.Number): + attrs[protoAttr.Name] = identityschema.NumberAttribute{ + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + case protoAttr.Type.Is(tftypes.String): + attrs[protoAttr.Name] = identityschema.StringAttribute{ + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + case protoAttr.Type.Is(tftypes.List{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + l := protoAttr.Type.(tftypes.List) + + elementType, err := basetypes.TerraformTypeToFrameworkType(l.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = identityschema.ListAttribute{ + ElementType: elementType, + RequiredForImport: protoAttr.RequiredForImport, + OptionalForImport: protoAttr.OptionalForImport, + } + default: + // MAINTAINER NOTE: Not all terraform types are valid identity attribute types. Framework fully supports + // all of the possible identity attribute types, so any errors here would be invalid protocol identities. + return nil, fmt.Errorf("no supported identity attribute for %q, type: %T", protoAttr.Name, protoAttr.Type) + } + } + + return attrs, nil +} diff --git a/internal/fromproto6/identity_schema_test.go b/internal/fromproto6/identity_schema_test.go new file mode 100644 index 000000000..18b1b718e --- /dev/null +++ b/internal/fromproto6/identity_schema_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchema(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.ResourceIdentitySchema + expected *identityschema.Schema + expectedErr string + }{ + "nil": { + input: nil, + expected: nil, + }, + "no-attrs": { + input: &tfprotov6.ResourceIdentitySchema{}, + expected: &identityschema.Schema{ + Attributes: make(map[string]identityschema.Attribute, 0), + }, + }, + "primitives-attrs": { + input: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + RequiredForImport: true, + }, + { + Name: "number", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "string", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + }, + expected: &identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + "number": identityschema.NumberAttribute{ + OptionalForImport: true, + }, + "string": identityschema.StringAttribute{ + OptionalForImport: true, + }, + }, + }, + }, + "list-attr": { + input: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "list_of_bools", + Type: tftypes.List{ElementType: tftypes.Bool}, + RequiredForImport: true, + }, + }, + }, + expected: &identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "list_of_bools": identityschema.ListAttribute{ + ElementType: basetypes.BoolType{}, + RequiredForImport: true, + }, + }, + }, + }, + "map-error": { + input: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "map_of_strings", + Type: tftypes.Map{ElementType: tftypes.String}, + OptionalForImport: true, + }, + }, + }, + expectedErr: `no supported identity attribute for "map_of_strings", type: tftypes.Map`, + }, + "set-error": { + input: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + OptionalForImport: true, + }, + }, + }, + expectedErr: `no supported identity attribute for "set_of_strings", type: tftypes.Set`, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := fromproto6.IdentitySchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/fromproto6/resource_schema.go b/internal/fromproto6/resource_schema.go new file mode 100644 index 000000000..a86bf3b9e --- /dev/null +++ b/internal/fromproto6/resource_schema.go @@ -0,0 +1,263 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ResourceSchema converts a *tfprotov6.Schema into a resource/schema Schema, used for +// converting protocol schemas (from another provider server, such as terraform-plugin-go) +// into Framework schemas. +func ResourceSchema(ctx context.Context, s *tfprotov6.Schema) (*resourceschema.Schema, error) { + if s == nil || s.Block == nil { + return nil, nil + } + + attrs, err := ResourceSchemaAttributes(ctx, s.Block.Attributes) + if err != nil { + return nil, err + } + + blocks, err := ResourceSchemaNestedBlocks(ctx, s.Block.BlockTypes) + if err != nil { + return nil, err + } + + return &resourceschema.Schema{ + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol schema + // to the resource schema, just enough data to allow provider developers to read and set data. + Attributes: attrs, + Blocks: blocks, + }, nil +} + +func ResourceSchemaAttributes(ctx context.Context, protoAttrs []*tfprotov6.SchemaAttribute) (map[string]resourceschema.Attribute, error) { + attrs := make(map[string]resourceschema.Attribute, len(protoAttrs)) + for _, protoAttr := range protoAttrs { + if protoAttr.NestedType != nil { + nestedAttrs, err := ResourceSchemaAttributes(ctx, protoAttr.NestedType.Attributes) + if err != nil { + return nil, err + } + + switch protoAttr.NestedType.Nesting { + case tfprotov6.SchemaObjectNestingModeSingle: + attrs[protoAttr.Name] = resourceschema.SingleNestedAttribute{ + Attributes: nestedAttrs, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case tfprotov6.SchemaObjectNestingModeList: + attrs[protoAttr.Name] = resourceschema.ListNestedAttribute{ + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: nestedAttrs, + }, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case tfprotov6.SchemaObjectNestingModeSet: + attrs[protoAttr.Name] = resourceschema.SetNestedAttribute{ + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: nestedAttrs, + }, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + Sensitive: protoAttr.Sensitive, + } + case tfprotov6.SchemaObjectNestingModeMap: + attrs[protoAttr.Name] = resourceschema.MapNestedAttribute{ + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: nestedAttrs, + }, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + default: + return nil, fmt.Errorf("no supported nested attribute for %q, nesting mode: %s", protoAttr.Name, protoAttr.NestedType.Nesting) + } + + continue + } + + // MAINTAINER NOTE: At the moment, there isn't a need to copy all of the data from the protocol schema + // to the resource schema, just enough data to allow provider developers to read and set data. + switch { + case protoAttr.Type.Is(tftypes.Bool): + attrs[protoAttr.Name] = resourceschema.BoolAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Number): + attrs[protoAttr.Name] = resourceschema.NumberAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.String): + attrs[protoAttr.Name] = resourceschema.StringAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.DynamicPseudoType): + attrs[protoAttr.Name] = resourceschema.DynamicAttribute{ + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.List{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + l := protoAttr.Type.(tftypes.List) + + elementType, err := basetypes.TerraformTypeToFrameworkType(l.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = resourceschema.ListAttribute{ + ElementType: elementType, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Map{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + m := protoAttr.Type.(tftypes.Map) + + elementType, err := basetypes.TerraformTypeToFrameworkType(m.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = resourceschema.MapAttribute{ + ElementType: elementType, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Set{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + s := protoAttr.Type.(tftypes.Set) + + elementType, err := basetypes.TerraformTypeToFrameworkType(s.ElementType) + if err != nil { + return nil, err + } + + attrs[protoAttr.Name] = resourceschema.SetAttribute{ + ElementType: elementType, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + Sensitive: protoAttr.Sensitive, + } + case protoAttr.Type.Is(tftypes.Object{}): + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + o := protoAttr.Type.(tftypes.Object) + + attrTypes := make(map[string]attr.Type, len(o.AttributeTypes)) + for name, tfType := range o.AttributeTypes { + t, err := basetypes.TerraformTypeToFrameworkType(tfType) + if err != nil { + return nil, err + } + attrTypes[name] = t + } + + attrs[protoAttr.Name] = resourceschema.ObjectAttribute{ + AttributeTypes: attrTypes, + Required: protoAttr.Required, + Optional: protoAttr.Optional, + Computed: protoAttr.Computed, + WriteOnly: protoAttr.WriteOnly, + Sensitive: protoAttr.Sensitive, + } + default: + // MAINTAINER NOTE: Currently the only type not supported by Framework is a tuple, since there + // is no corresponding attribute to represent it. + // + // https://github.com/hashicorp/terraform-plugin-framework/issues/54 + return nil, fmt.Errorf("no supported attribute for %q, type: %T", protoAttr.Name, protoAttr.Type) + } + } + + return attrs, nil +} + +func ResourceSchemaNestedBlocks(ctx context.Context, protoBlocks []*tfprotov6.SchemaNestedBlock) (map[string]resourceschema.Block, error) { + nestedBlocks := make(map[string]resourceschema.Block, len(protoBlocks)) + for _, protoBlock := range protoBlocks { + if protoBlock.Block == nil { + continue + } + + attrs, err := ResourceSchemaAttributes(ctx, protoBlock.Block.Attributes) + if err != nil { + return nil, err + } + blocks, err := ResourceSchemaNestedBlocks(ctx, protoBlock.Block.BlockTypes) + if err != nil { + return nil, err + } + + switch protoBlock.Nesting { + case tfprotov6.SchemaNestedBlockNestingModeList: + nestedBlocks[protoBlock.TypeName] = resourceschema.ListNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: attrs, + Blocks: blocks, + }, + } + case tfprotov6.SchemaNestedBlockNestingModeSet: + nestedBlocks[protoBlock.TypeName] = resourceschema.SetNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: attrs, + Blocks: blocks, + }, + } + case tfprotov6.SchemaNestedBlockNestingModeSingle: + nestedBlocks[protoBlock.TypeName] = resourceschema.SingleNestedBlock{ + Attributes: attrs, + Blocks: blocks, + } + default: + // MAINTAINER NOTE: Currently the only block type not supported by Framework is a map nested block, since there + // is no corresponding framework block implementation to represent it. + return nil, fmt.Errorf("no supported block for nesting mode %v in nested block %q", protoBlock.Nesting, protoBlock.TypeName) + } + } + + return nestedBlocks, nil +} diff --git a/internal/fromproto6/resource_schema_test.go b/internal/fromproto6/resource_schema_test.go new file mode 100644 index 000000000..10d701d01 --- /dev/null +++ b/internal/fromproto6/resource_schema_test.go @@ -0,0 +1,781 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceSchema(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.Schema + expected *resourceschema.Schema + expectedErr string + }{ + "nil": { + input: nil, + expected: nil, + }, + "no-block": { + input: &tfprotov6.Schema{}, + expected: nil, + }, + "no-attrs-no-nested-blocks": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + expected: &resourceschema.Schema{ + Attributes: make(map[string]resourceschema.Attribute, 0), + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "primitives-attrs": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + { + Name: "number", + Type: tftypes.Number, + Optional: true, + Computed: true, + }, + { + Name: "string", + Type: tftypes.String, + Optional: true, + Computed: true, + Sensitive: true, + }, + { + Name: "dynamic", + Type: tftypes.DynamicPseudoType, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + "number": resourceschema.NumberAttribute{ + Optional: true, + Computed: true, + }, + "string": resourceschema.StringAttribute{ + Optional: true, + Computed: true, + Sensitive: true, + }, + "dynamic": resourceschema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "collection-attrs": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "list_of_bools", + Type: tftypes.List{ElementType: tftypes.Bool}, + Required: true, + WriteOnly: true, + }, + { + Name: "map_of_numbers", + Type: tftypes.Map{ElementType: tftypes.Number}, + Optional: true, + Computed: true, + }, + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + Optional: true, + Computed: true, + Sensitive: true, + }, + { + Name: "list_of_objects", + Type: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "dynamic": tftypes.DynamicPseudoType, + "string": tftypes.String, + }, + }, + }, + Required: true, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "list_of_bools": resourceschema.ListAttribute{ + ElementType: basetypes.BoolType{}, + Required: true, + WriteOnly: true, + }, + "map_of_numbers": resourceschema.MapAttribute{ + ElementType: basetypes.NumberType{}, + Optional: true, + Computed: true, + }, + "set_of_strings": resourceschema.SetAttribute{ + ElementType: basetypes.StringType{}, + Optional: true, + Computed: true, + Sensitive: true, + }, + "list_of_objects": resourceschema.ListAttribute{ + ElementType: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "dynamic": basetypes.DynamicType{}, + "string": basetypes.StringType{}, + }, + }, + Required: true, + }, + }, + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "object-attr": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "object", + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + "dynamic": tftypes.DynamicPseudoType, + "string": tftypes.String, + }, + }, + Optional: true, + Computed: true, + Sensitive: true, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "object": resourceschema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "bool": basetypes.BoolType{}, + "dynamic": basetypes.DynamicType{}, + "string": basetypes.StringType{}, + }, + Optional: true, + Computed: true, + Sensitive: true, + }, + }, + Blocks: make(map[string]resourceschema.Block, 0), + }, + }, + "tuple-error": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "tuple", + Type: tftypes.Tuple{ + ElementTypes: []tftypes.Type{ + tftypes.Bool, + tftypes.Number, + tftypes.String, + }, + }, + Required: true, + }, + }, + }, + }, + expectedErr: `no supported attribute for "tuple", type: tftypes.Tuple`, + }, + "list-nested": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "list_nested", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeList, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "list_of_strings", + Type: tftypes.List{ElementType: tftypes.String}, + Computed: true, + }, + { + Name: "nested_list_attr", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeList, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: make(map[string]resourceschema.Block, 0), + Attributes: map[string]resourceschema.Attribute{ + "list_nested": resourceschema.ListNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "list_of_strings": resourceschema.ListAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + "nested_list_attr": resourceschema.ListNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "list-block": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "list_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "list_of_strings", + Type: tftypes.List{ElementType: tftypes.String}, + Computed: true, + }, + }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "nested_list_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "list_block": resourceschema.ListNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "list_of_strings": resourceschema.ListAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + }, + Blocks: map[string]resourceschema.Block{ + "nested_list_block": resourceschema.ListNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + "set-nested": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "set_nested", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSet, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + Computed: true, + }, + { + Name: "nested_set_attr", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSet, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: make(map[string]resourceschema.Block, 0), + Attributes: map[string]resourceschema.Attribute{ + "set_nested": resourceschema.SetNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "set_of_strings": resourceschema.SetAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + "nested_set_attr": resourceschema.SetNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "set-block": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "set_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "set_of_strings", + Type: tftypes.Set{ElementType: tftypes.String}, + Computed: true, + }, + }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "nested_set_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "set_block": resourceschema.SetNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "set_of_strings": resourceschema.SetAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + }, + Blocks: map[string]resourceschema.Block{ + "nested_set_block": resourceschema.SetNestedBlock{ + NestedObject: resourceschema.NestedBlockObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + "single-nested": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "single_nested", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "dynamic", + Type: tftypes.DynamicPseudoType, + Computed: true, + }, + { + Name: "nested_single_attr", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: make(map[string]resourceschema.Block, 0), + Attributes: map[string]resourceschema.Attribute{ + "single_nested": resourceschema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]resourceschema.Attribute{ + "dynamic": resourceschema.DynamicAttribute{ + Computed: true, + }, + "nested_single_attr": resourceschema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + "single-block": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "single_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "map_of_strings", + Type: tftypes.Map{ElementType: tftypes.String}, + Computed: true, + }, + }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "nested_single_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "single_block": resourceschema.SingleNestedBlock{ + Attributes: map[string]resourceschema.Attribute{ + "map_of_strings": resourceschema.MapAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + }, + Blocks: map[string]resourceschema.Block{ + "nested_single_block": resourceschema.SingleNestedBlock{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + "map-nested": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "map_nested", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeMap, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "map_of_strings", + Type: tftypes.Map{ElementType: tftypes.String}, + Computed: true, + }, + { + Name: "nested_map_attr", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeMap, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: make(map[string]resourceschema.Block, 0), + Attributes: map[string]resourceschema.Attribute{ + "map_nested": resourceschema.MapNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "map_of_strings": resourceschema.MapAttribute{ + ElementType: basetypes.StringType{}, + Computed: true, + }, + "nested_map_attr": resourceschema.MapNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "map-block": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "map_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeMap, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedErr: `no supported block for nesting mode MAP in nested block "map_block"`, + }, + "block-with-nested-attr": { + input: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "single_block", + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "list_nested", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeList, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "number", + Type: tftypes.Number, + Computed: true, + }, + { + Name: "nested_map_attr", + Required: true, + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeMap, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &resourceschema.Schema{ + Blocks: map[string]resourceschema.Block{ + "single_block": resourceschema.SingleNestedBlock{ + Attributes: map[string]resourceschema.Attribute{ + "list_nested": resourceschema.ListNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "number": resourceschema.NumberAttribute{ + Computed: true, + }, + "nested_map_attr": resourceschema.MapNestedAttribute{ + Required: true, + NestedObject: resourceschema.NestedAttributeObject{ + Attributes: map[string]resourceschema.Attribute{ + "bool": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Attributes: make(map[string]resourceschema.Attribute, 0), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := fromproto6.ResourceSchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/fwserver/server_upgraderesourceidentity_test.go b/internal/fwserver/server_upgraderesourceidentity_test.go index 48c15c85d..79cfc5676 100644 --- a/internal/fwserver/server_upgraderesourceidentity_test.go +++ b/internal/fwserver/server_upgraderesourceidentity_test.go @@ -7,9 +7,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -213,14 +214,6 @@ func TestServerUpgradeResourceIdentity(t *testing.T) { Schema: testIdentitySchema, } - if err != nil { - resp.Diagnostics.AddError( - "Unable to Convert Upgraded Identity", - err.Error(), - ) - return - } - resp.Identity = ResourceIdentity }, }, diff --git a/internal/toproto5/block_test.go b/internal/toproto5/block_test.go index 303474e56..04c879ec4 100644 --- a/internal/toproto5/block_test.go +++ b/internal/toproto5/block_test.go @@ -567,7 +567,7 @@ func TestBlock(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto5/identity_schema_attribute_test.go b/internal/toproto5/identity_schema_attribute_test.go index 28704ba5d..29e651820 100644 --- a/internal/toproto5/identity_schema_attribute_test.go +++ b/internal/toproto5/identity_schema_attribute_test.go @@ -244,7 +244,7 @@ func TestIdentitySchemaAttribute(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto5/identity_schema_test.go b/internal/toproto5/identity_schema_test.go index 8388c4c16..5d867f685 100644 --- a/internal/toproto5/identity_schema_test.go +++ b/internal/toproto5/identity_schema_test.go @@ -125,7 +125,7 @@ func TestIdentitySchema(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto5/schema_attribute_test.go b/internal/toproto5/schema_attribute_test.go index cc52ebd92..1d754ef4f 100644 --- a/internal/toproto5/schema_attribute_test.go +++ b/internal/toproto5/schema_attribute_test.go @@ -385,7 +385,7 @@ func TestSchemaAttribute(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto5/schema_test.go b/internal/toproto5/schema_test.go index 587f0424c..6f6fe3b76 100644 --- a/internal/toproto5/schema_test.go +++ b/internal/toproto5/schema_test.go @@ -539,7 +539,7 @@ func TestSchema(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto6/block_test.go b/internal/toproto6/block_test.go index e1c5784d3..7ccdcc315 100644 --- a/internal/toproto6/block_test.go +++ b/internal/toproto6/block_test.go @@ -567,7 +567,7 @@ func TestBlock(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto6/identity_schema_attribute_test.go b/internal/toproto6/identity_schema_attribute_test.go index 45fa0d1a1..af59bda08 100644 --- a/internal/toproto6/identity_schema_attribute_test.go +++ b/internal/toproto6/identity_schema_attribute_test.go @@ -244,7 +244,7 @@ func TestIdentitySchemaAttribute(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto6/identity_schema_test.go b/internal/toproto6/identity_schema_test.go index f8ebd1728..7759c7034 100644 --- a/internal/toproto6/identity_schema_test.go +++ b/internal/toproto6/identity_schema_test.go @@ -125,7 +125,7 @@ func TestIdentitySchema(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto6/schema_attribute_test.go b/internal/toproto6/schema_attribute_test.go index 1eb83c0c0..eb8ffd907 100644 --- a/internal/toproto6/schema_attribute_test.go +++ b/internal/toproto6/schema_attribute_test.go @@ -453,7 +453,7 @@ func TestSchemaAttribute(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/internal/toproto6/schema_test.go b/internal/toproto6/schema_test.go index 50d203c61..c2ed9e3be 100644 --- a/internal/toproto6/schema_test.go +++ b/internal/toproto6/schema_test.go @@ -643,7 +643,7 @@ func TestSchema(t *testing.T) { // got expected error return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/list/schema/schema_test.go b/list/schema/schema_test.go index b556ac7df..4ae7f04c2 100644 --- a/list/schema/schema_test.go +++ b/list/schema/schema_test.go @@ -339,7 +339,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/provider/metaschema/schema_test.go b/provider/metaschema/schema_test.go index 7460f8016..426c6f819 100644 --- a/provider/metaschema/schema_test.go +++ b/provider/metaschema/schema_test.go @@ -341,7 +341,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/provider/provider.go b/provider/provider.go index f32e2773d..8073e549e 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -138,7 +138,7 @@ type ProviderWithListResources interface { // ProviderWithActions is an interface type that extends Provider to // include actions for usage in practitioner configurations. // -// TODO:Actions: State which Terraform version will support actions +// Actions are supported in Terraform version 1.14 and later. type ProviderWithActions interface { Provider diff --git a/provider/schema/schema_test.go b/provider/schema/schema_test.go index 9007eabdc..ddcba42c2 100644 --- a/provider/schema/schema_test.go +++ b/provider/schema/schema_test.go @@ -406,7 +406,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/resource/identityschema/schema_test.go b/resource/identityschema/schema_test.go index 394cdd426..248612a6c 100644 --- a/resource/identityschema/schema_test.go +++ b/resource/identityschema/schema_test.go @@ -341,7 +341,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/resource/schema/schema_test.go b/resource/schema/schema_test.go index a48fed812..c8de116bb 100644 --- a/resource/schema/schema_test.go +++ b/resource/schema/schema_test.go @@ -406,7 +406,7 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { return } - if err == nil && tc.expectedErr != "" { + if tc.expectedErr != "" { t.Errorf("Expected error to be %q, got nil", tc.expectedErr) return } diff --git a/types/basetypes/bool_type_test.go b/types/basetypes/bool_type_test.go index 2280dcfa3..1a89cb92c 100644 --- a/types/basetypes/bool_type_test.go +++ b/types/basetypes/bool_type_test.go @@ -60,7 +60,7 @@ func TestBoolTypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/dynamic_type.go b/types/basetypes/dynamic_type.go index 87138984a..8e949be9a 100644 --- a/types/basetypes/dynamic_type.go +++ b/types/basetypes/dynamic_type.go @@ -91,7 +91,7 @@ func (t DynamicType) ValueFromTerraform(ctx context.Context, in tftypes.Value) ( return nil, errors.New("ambiguous known value for `tftypes.DynamicPseudoType` detected") } - attrType, err := tftypeToFrameworkType(in.Type()) + attrType, err := TerraformTypeToFrameworkType(in.Type()) if err != nil { return nil, err } @@ -108,91 +108,3 @@ func (t DynamicType) ValueFromTerraform(ctx context.Context, in tftypes.Value) ( func (t DynamicType) ValueType(_ context.Context) attr.Value { return DynamicValue{} } - -// tftypeToFrameworkType is a helper function that returns the framework type equivalent for a given Terraform type. -// -// Custom dynamic type implementations shouldn't need to override this method, but if needed, they can implement similar logic -// in their `ValueFromTerraform` implementation. -func tftypeToFrameworkType(in tftypes.Type) (attr.Type, error) { - // Primitive types - if in.Is(tftypes.Bool) { - return BoolType{}, nil - } - if in.Is(tftypes.Number) { - return NumberType{}, nil - } - if in.Is(tftypes.String) { - return StringType{}, nil - } - - if in.Is(tftypes.DynamicPseudoType) { - // Null and Unknown values that do not have a type determined will have a type of DynamicPseudoType - return DynamicType{}, nil - } - - // Collection types - if in.Is(tftypes.List{}) { - //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function - l := in.(tftypes.List) - - elemType, err := tftypeToFrameworkType(l.ElementType) - if err != nil { - return nil, err - } - return ListType{ElemType: elemType}, nil - } - if in.Is(tftypes.Map{}) { - //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function - m := in.(tftypes.Map) - - elemType, err := tftypeToFrameworkType(m.ElementType) - if err != nil { - return nil, err - } - - return MapType{ElemType: elemType}, nil - } - if in.Is(tftypes.Set{}) { - //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function - s := in.(tftypes.Set) - - elemType, err := tftypeToFrameworkType(s.ElementType) - if err != nil { - return nil, err - } - - return SetType{ElemType: elemType}, nil - } - - // Structural types - if in.Is(tftypes.Object{}) { - //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function - o := in.(tftypes.Object) - - attrTypes := make(map[string]attr.Type, len(o.AttributeTypes)) - for name, tfType := range o.AttributeTypes { - t, err := tftypeToFrameworkType(tfType) - if err != nil { - return nil, err - } - attrTypes[name] = t - } - return ObjectType{AttrTypes: attrTypes}, nil - } - if in.Is(tftypes.Tuple{}) { - //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function - tup := in.(tftypes.Tuple) - - elemTypes := make([]attr.Type, len(tup.ElementTypes)) - for i, tfType := range tup.ElementTypes { - t, err := tftypeToFrameworkType(tfType) - if err != nil { - return nil, err - } - elemTypes[i] = t - } - return TupleType{ElemTypes: elemTypes}, nil - } - - return nil, fmt.Errorf("unsupported tftypes.Type detected: %T", in) -} diff --git a/types/basetypes/float32_type_test.go b/types/basetypes/float32_type_test.go index 49ff4960d..c2d3c8354 100644 --- a/types/basetypes/float32_type_test.go +++ b/types/basetypes/float32_type_test.go @@ -119,7 +119,7 @@ func TestFloat32TypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/float64_type_test.go b/types/basetypes/float64_type_test.go index 2e86204d9..271899492 100644 --- a/types/basetypes/float64_type_test.go +++ b/types/basetypes/float64_type_test.go @@ -205,7 +205,7 @@ func TestFloat64TypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/int32_type_test.go b/types/basetypes/int32_type_test.go index f47ddd803..9d7e128f6 100644 --- a/types/basetypes/int32_type_test.go +++ b/types/basetypes/int32_type_test.go @@ -78,7 +78,7 @@ func TestInt32TypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/int64_type_test.go b/types/basetypes/int64_type_test.go index 6eb115bc9..06ed80cf4 100644 --- a/types/basetypes/int64_type_test.go +++ b/types/basetypes/int64_type_test.go @@ -56,7 +56,7 @@ func TestInt64TypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/number_type_test.go b/types/basetypes/number_type_test.go index 153e8f9c2..7a5654b75 100644 --- a/types/basetypes/number_type_test.go +++ b/types/basetypes/number_type_test.go @@ -57,7 +57,7 @@ func TestNumberTypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/string_type_test.go b/types/basetypes/string_type_test.go index bcaf9e6d1..f1c590dc3 100644 --- a/types/basetypes/string_type_test.go +++ b/types/basetypes/string_type_test.go @@ -56,7 +56,7 @@ func TestStringTypeValueFromTerraform(t *testing.T) { // expectations, we're good return } - if err == nil && test.expectedErr != "" { + if test.expectedErr != "" { t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) return } diff --git a/types/basetypes/terraform_type_to_framework_type.go b/types/basetypes/terraform_type_to_framework_type.go new file mode 100644 index 000000000..e3680b086 --- /dev/null +++ b/types/basetypes/terraform_type_to_framework_type.go @@ -0,0 +1,95 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// TerraformTypeToFrameworkType is a helper function that returns the framework type equivalent for a given Terraform type. +func TerraformTypeToFrameworkType(in tftypes.Type) (attr.Type, error) { + // Primitive types + if in.Is(tftypes.Bool) { + return BoolType{}, nil + } + if in.Is(tftypes.Number) { + return NumberType{}, nil + } + if in.Is(tftypes.String) { + return StringType{}, nil + } + + if in.Is(tftypes.DynamicPseudoType) { + return DynamicType{}, nil + } + + // Collection types + if in.Is(tftypes.List{}) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + l := in.(tftypes.List) + + elemType, err := TerraformTypeToFrameworkType(l.ElementType) + if err != nil { + return nil, err + } + return ListType{ElemType: elemType}, nil + } + if in.Is(tftypes.Map{}) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + m := in.(tftypes.Map) + + elemType, err := TerraformTypeToFrameworkType(m.ElementType) + if err != nil { + return nil, err + } + + return MapType{ElemType: elemType}, nil + } + if in.Is(tftypes.Set{}) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + s := in.(tftypes.Set) + + elemType, err := TerraformTypeToFrameworkType(s.ElementType) + if err != nil { + return nil, err + } + + return SetType{ElemType: elemType}, nil + } + + // Structural types + if in.Is(tftypes.Object{}) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + o := in.(tftypes.Object) + + attrTypes := make(map[string]attr.Type, len(o.AttributeTypes)) + for name, tfType := range o.AttributeTypes { + t, err := TerraformTypeToFrameworkType(tfType) + if err != nil { + return nil, err + } + attrTypes[name] = t + } + return ObjectType{AttrTypes: attrTypes}, nil + } + if in.Is(tftypes.Tuple{}) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `(tftypes.Type).Is` function + tup := in.(tftypes.Tuple) + + elemTypes := make([]attr.Type, len(tup.ElementTypes)) + for i, tfType := range tup.ElementTypes { + t, err := TerraformTypeToFrameworkType(tfType) + if err != nil { + return nil, err + } + elemTypes[i] = t + } + return TupleType{ElemTypes: elemTypes}, nil + } + + return nil, fmt.Errorf("unsupported tftypes.Type detected: %T", in) +} diff --git a/types/basetypes/terraform_type_to_framework_type_test.go b/types/basetypes/terraform_type_to_framework_type_test.go new file mode 100644 index 000000000..955442e67 --- /dev/null +++ b/types/basetypes/terraform_type_to_framework_type_test.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestTerraformTypeToFrameworkType(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + input tftypes.Type + expected attr.Type + }{ + "bool": { + input: tftypes.Bool, + expected: BoolType{}, + }, + "number": { + input: tftypes.Number, + expected: NumberType{}, + }, + "string": { + input: tftypes.String, + expected: StringType{}, + }, + "dynamic": { + input: tftypes.DynamicPseudoType, + expected: DynamicType{}, + }, + "list": { + input: tftypes.List{ElementType: tftypes.Bool}, + expected: ListType{ElemType: BoolType{}}, + }, + "set": { + input: tftypes.Set{ElementType: tftypes.Number}, + expected: SetType{ElemType: NumberType{}}, + }, + "map": { + input: tftypes.Map{ElementType: tftypes.String}, + expected: MapType{ElemType: StringType{}}, + }, + "object": { + input: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + "list": tftypes.List{ElementType: tftypes.Number}, + "nested_obj": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map": tftypes.Map{ElementType: tftypes.DynamicPseudoType}, + "string": tftypes.String, + }, + }, + }, + }, + expected: ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": BoolType{}, + "list": ListType{ElemType: NumberType{}}, + "nested_obj": ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": MapType{ElemType: DynamicType{}}, + "string": StringType{}, + }, + }, + }, + }, + }, + "tuple": { + input: tftypes.Tuple{ + ElementTypes: []tftypes.Type{ + tftypes.Bool, + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list": tftypes.List{ElementType: tftypes.DynamicPseudoType}, + "number": tftypes.Number, + }, + }, + tftypes.Map{ElementType: tftypes.String}, + }, + }, + expected: TupleType{ + ElemTypes: []attr.Type{ + BoolType{}, + ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": ListType{ElemType: DynamicType{}}, + "number": NumberType{}, + }, + }, + MapType{ElemType: StringType{}}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, _ := TerraformTypeToFrameworkType(test.input) + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("Unexpected diff (-expected, +got): %s", diff) + } + }) + } +}