From 9353b7c00b8ad4d6ac9cc11cdc9d88e1d6d20c26 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 29 Nov 2022 13:55:25 -0500 Subject: [PATCH] provider/metaschema: Initial package (#562) Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/132 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/326 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/437 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/491 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/508 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/532 This change introduces a new `provider/metaschema` package, which contains schema interfaces and types relevant to provider meta schemas, such as omitting `Computed`, `DeprecatedMessage`, `Sensitive`, plan modifiers, validators, blocks, schema descriptions, and schema versioning. This new schema implementation also provides strongly typed attributes and nested attributes with customizable types. Nested attributes are exposed with a separate nested object for customization. The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for provider meta schemas. No changes are required for data handling during requests. Example definition: ```go package test import ( "context" "github.com/bflad/terraform-plugin-framework-type-time/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) type ExampleCloudProvider struct{} func (p ExampleCloudProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { resp.Schema = metaschema.Schema{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ Required: true, }, "custom_string_attribute": schema.StringAttribute{ CustomType: timetypes.RFC3339Type, Optional: true, }, "list_attribute": schema.ListAttribute{ ElementType: types.StringType, Optional: true, }, "list_nested_attribute": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "bool_attribute": schema.BoolAttribute{ Optional: true, }, }, }, Optional: true, }, "single_nested_attribute": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "int64_attribute": schema.Int64Attribute{ Optional: true, }, }, Optional: true, }, }, } } ``` To migrate a provider meta schema: - Add `github.com/hashicorp/terraform-plugin-framework/provider/metaschema` to the `import` statement - Switch the `provider.ProviderWithMetaSchema` implementation `GetMetaSchema` method to `MetaSchema` whose response includes a `metaschema.Schema` from the new package. Prior implementation: ```go func (p ExampleCloudProvider) GetMetaSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{/* ... */}, nil } ``` Migrated implementation: ```go func (p ExampleCloudProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { resp.Schema = metaschema.Schema{/*...*/} } ``` If the provider requires no meta schema, the method can be removed entirely. - Switch `map[string]tfsdk.Attribute` with `map[string]metaschema.Attribute` - Switch individual attribute definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `metaschema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations. Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation: ```go // The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute tfsdk.Attribute{ Required: true, Type: types.StringType, } ``` Migrated implementation: ```go // The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute schema.StringAttribute{ Required: true, } ``` Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation: ```go // The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute tfsdk.Attribute{ Required: true, Type: types.ListType{ ElemType: types.StringType, }, } ``` Migrated implementation: ```go // The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute metaschema.ListAttribute{ ElementType: types.StringType, Required: true, } ``` Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation: ```go // The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute tfsdk.Attribute{ Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}), Required: true, }, ``` Migrated implementation: ```go // The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute metaschema.SingleNestedAttribute{ Attributes: map[string]metaschema.Attribute{/*...*/}, Required: true, } ``` Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation: ```go // The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute tfsdk.Attribute{ Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}), Required: true, }, ``` Migrated implementation: ```go // The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute metaschema.ListNestedAttribute{ NestedObject: metaschema.NestedAttributeObject{ Attributes: map[string]metaschema.Attribute{/*...*/}, }, Required: true, } ``` --- .changelog/562.txt | 3 + internal/fwserver/server.go | 15 +- .../fwserver/server_getproviderschema_test.go | 21 +- .../server_applyresourcechange_test.go | 27 +- .../server_getproviderschema_test.go | 15 +- .../server_planresourcechange_test.go | 23 +- .../server_readdatasource_test.go | 37 +- .../proto5server/server_readresource_test.go | 41 +- .../server_applyresourcechange_test.go | 27 +- .../server_getproviderschema_test.go | 15 +- .../server_planresourcechange_test.go | 23 +- .../server_readdatasource_test.go | 40 +- .../proto6server/server_readresource_test.go | 41 +- .../testing/testprovider/providerwithmeta.go | 29 - .../testprovider/providerwithmetaschema.go | 27 + provider/metaschema.go | 24 + provider/metaschema/attribute.go | 33 + provider/metaschema/bool_attribute.go | 115 +++ provider/metaschema/bool_attribute_test.go | 369 ++++++++ provider/metaschema/doc.go | 5 + provider/metaschema/float64_attribute.go | 118 +++ provider/metaschema/float64_attribute_test.go | 368 ++++++++ provider/metaschema/int64_attribute.go | 118 +++ provider/metaschema/int64_attribute_test.go | 368 ++++++++ provider/metaschema/list_attribute.go | 128 +++ provider/metaschema/list_attribute_test.go | 373 ++++++++ provider/metaschema/list_nested_attribute.go | 155 ++++ .../metaschema/list_nested_attribute_test.go | 523 ++++++++++++ provider/metaschema/map_attribute.go | 131 +++ provider/metaschema/map_attribute_test.go | 373 ++++++++ provider/metaschema/map_nested_attribute.go | 155 ++++ .../metaschema/map_nested_attribute_test.go | 523 ++++++++++++ provider/metaschema/nested_attribute.go | 11 + .../metaschema/nested_attribute_object.go | 60 ++ .../nested_attribute_object_test.go | 237 +++++ provider/metaschema/number_attribute.go | 119 +++ provider/metaschema/number_attribute_test.go | 369 ++++++++ provider/metaschema/object_attribute.go | 130 +++ provider/metaschema/object_attribute_test.go | 379 ++++++++ provider/metaschema/schema.go | 108 +++ provider/metaschema/schema_test.go | 808 ++++++++++++++++++ provider/metaschema/set_attribute.go | 126 +++ provider/metaschema/set_attribute_test.go | 373 ++++++++ provider/metaschema/set_nested_attribute.go | 150 ++++ .../metaschema/set_nested_attribute_test.go | 523 ++++++++++++ .../metaschema/single_nested_attribute.go | 170 ++++ .../single_nested_attribute_test.go | 491 +++++++++++ provider/metaschema/string_attribute.go | 115 +++ provider/metaschema/string_attribute_test.go | 369 ++++++++ provider/provider.go | 26 +- provider/schema.go | 2 +- 51 files changed, 8645 insertions(+), 184 deletions(-) create mode 100644 .changelog/562.txt delete mode 100644 internal/testing/testprovider/providerwithmeta.go create mode 100644 internal/testing/testprovider/providerwithmetaschema.go create mode 100644 provider/metaschema.go create mode 100644 provider/metaschema/attribute.go create mode 100644 provider/metaschema/bool_attribute.go create mode 100644 provider/metaschema/bool_attribute_test.go create mode 100644 provider/metaschema/doc.go create mode 100644 provider/metaschema/float64_attribute.go create mode 100644 provider/metaschema/float64_attribute_test.go create mode 100644 provider/metaschema/int64_attribute.go create mode 100644 provider/metaschema/int64_attribute_test.go create mode 100644 provider/metaschema/list_attribute.go create mode 100644 provider/metaschema/list_attribute_test.go create mode 100644 provider/metaschema/list_nested_attribute.go create mode 100644 provider/metaschema/list_nested_attribute_test.go create mode 100644 provider/metaschema/map_attribute.go create mode 100644 provider/metaschema/map_attribute_test.go create mode 100644 provider/metaschema/map_nested_attribute.go create mode 100644 provider/metaschema/map_nested_attribute_test.go create mode 100644 provider/metaschema/nested_attribute.go create mode 100644 provider/metaschema/nested_attribute_object.go create mode 100644 provider/metaschema/nested_attribute_object_test.go create mode 100644 provider/metaschema/number_attribute.go create mode 100644 provider/metaschema/number_attribute_test.go create mode 100644 provider/metaschema/object_attribute.go create mode 100644 provider/metaschema/object_attribute_test.go create mode 100644 provider/metaschema/schema.go create mode 100644 provider/metaschema/schema_test.go create mode 100644 provider/metaschema/set_attribute.go create mode 100644 provider/metaschema/set_attribute_test.go create mode 100644 provider/metaschema/set_nested_attribute.go create mode 100644 provider/metaschema/set_nested_attribute_test.go create mode 100644 provider/metaschema/single_nested_attribute.go create mode 100644 provider/metaschema/single_nested_attribute_test.go create mode 100644 provider/metaschema/string_attribute.go create mode 100644 provider/metaschema/string_attribute_test.go diff --git a/.changelog/562.txt b/.changelog/562.txt new file mode 100644 index 000000000..5e5eaca38 --- /dev/null +++ b/.changelog/562.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +provider: The `ProviderWithMetaSchema` type `GetMetaSchema` method has been replaced with the `MetaSchema` method +``` diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index be9c8e01e..da661cf30 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -315,7 +315,7 @@ func (s *Server) ProviderSchema(ctx context.Context) (fwschema.Schema, diag.Diag // it implements the ProviderWithMetaSchema interface. The Schema and // Diagnostics are cached on first use. func (s *Server) ProviderMetaSchema(ctx context.Context) (fwschema.Schema, diag.Diagnostics) { - providerWithProviderMeta, ok := s.Provider.(provider.ProviderWithMetaSchema) + providerWithMetaSchema, ok := s.Provider.(provider.ProviderWithMetaSchema) if !ok { return nil, nil @@ -330,12 +330,15 @@ func (s *Server) ProviderMetaSchema(ctx context.Context) (fwschema.Schema, diag. return s.providerMetaSchema, s.providerMetaSchemaDiags } - logging.FrameworkDebug(ctx, "Calling provider defined Provider GetMetaSchema") - providerMetaSchema, diags := providerWithProviderMeta.GetMetaSchema(ctx) - logging.FrameworkDebug(ctx, "Called provider defined Provider GetMetaSchema") + req := provider.MetaSchemaRequest{} + resp := &provider.MetaSchemaResponse{} - s.providerMetaSchema = &providerMetaSchema - s.providerMetaSchemaDiags = diags + logging.FrameworkDebug(ctx, "Calling provider defined Provider MetaSchema") + providerWithMetaSchema.MetaSchema(ctx, req, resp) + logging.FrameworkDebug(ctx, "Called provider defined Provider MetaSchema") + + s.providerMetaSchema = resp.Schema + s.providerMetaSchemaDiags = resp.Diagnostics return s.providerMetaSchema, s.providerMetaSchemaDiags } diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index 969bae978..cf4317927 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -12,11 +12,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" providerschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" ) func TestServerGetProviderSchema(t *testing.T) { @@ -280,15 +279,14 @@ func TestServerGetProviderSchema(t *testing.T) { server: &fwserver.Server{ Provider: &testprovider.ProviderWithMetaSchema{ Provider: &testprovider.Provider{}, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, @@ -296,11 +294,10 @@ func TestServerGetProviderSchema(t *testing.T) { expectedResponse: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, - ProviderMeta: &tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + ProviderMeta: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, }, diff --git a/internal/proto5server/server_applyresourcechange_test.go b/internal/proto5server/server_applyresourcechange_test.go index b50a8b7a3..0a8eda2a3 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -9,13 +9,13 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -57,11 +57,10 @@ func TestServerApplyResourceChange(t *testing.T) { "test_provider_meta_attribute": tftypes.NewValue(tftypes.String, "test-provider-meta-value"), }) - testProviderMetaSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_provider_meta_attribute": { + testProviderMetaSchema := metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_provider_meta_attribute": metaschema.StringAttribute{ Optional: true, - Type: types.StringType, }, }, } @@ -233,8 +232,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -559,8 +558,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -946,8 +945,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -1015,8 +1014,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 096586737..b05f971ae 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -8,16 +8,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" providerschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" @@ -253,15 +251,14 @@ func TestServerGetProviderSchema(t *testing.T) { FrameworkServer: fwserver.Server{ Provider: &testprovider.ProviderWithMetaSchema{ Provider: &testprovider.Provider{}, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index 7bd6b7483..272951e83 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -5,13 +5,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -55,11 +55,10 @@ func TestServerPlanResourceChange(t *testing.T) { "test_provider_meta_attribute": tftypes.NewValue(tftypes.String, "test-provider-meta-value"), }) - testProviderMetaSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_provider_meta_attribute": { + testProviderMetaSchema := metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_provider_meta_attribute": metaschema.StringAttribute{ Optional: true, - Type: types.StringType, }, }, } @@ -207,8 +206,8 @@ func TestServerPlanResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -473,8 +472,8 @@ func TestServerPlanResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -836,8 +835,8 @@ func TestServerPlanResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, diff --git a/internal/proto5server/server_readdatasource_test.go b/internal/proto5server/server_readdatasource_test.go index 4a2404549..8b11ccf2e 100644 --- a/internal/proto5server/server_readdatasource_test.go +++ b/internal/proto5server/server_readdatasource_test.go @@ -7,10 +7,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -33,6 +33,19 @@ func TestServerReadDataSource(t *testing.T) { testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + testProviderMetaDynamicValue := testNewDynamicValue(t, + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional": tftypes.String, + "test_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_optional": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }, + ) + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), @@ -142,7 +155,7 @@ func TestServerReadDataSource(t *testing.T) { }, ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config struct { - TestComputed types.String `tfsdk:"test_computed"` + TestOptional types.String `tfsdk:"test_optional"` TestRequired types.String `tfsdk:"test_required"` } @@ -157,26 +170,24 @@ func TestServerReadDataSource(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { - Computed: true, - Type: types.StringType, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, }, - "test_required": { + "test_required": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, }, request: &tfprotov5.ReadDataSourceRequest{ Config: testEmptyDynamicValue, - ProviderMeta: testConfigDynamicValue, + ProviderMeta: testProviderMetaDynamicValue, TypeName: "test_data_source", }, expectedResponse: &tfprotov5.ReadDataSourceResponse{ diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index ffe661e75..8def2aec1 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -9,13 +9,13 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -43,6 +43,19 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov5.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + testProviderMetaDynamicValue := testNewDynamicValue(t, + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional": tftypes.String, + "test_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_optional": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }, + ) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -143,14 +156,14 @@ func TestServerReadResource(t *testing.T) { }, ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data struct { - TestComputed types.String `tfsdk:"test_computed"` + TestOptional types.String `tfsdk:"test_optional"` TestRequired types.String `tfsdk:"test_required"` } resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestRequired.ValueString() != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestRequired.ValueString()) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta", data.TestRequired.ValueString()) } }, } @@ -158,26 +171,24 @@ func TestServerReadResource(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { - Computed: true, - Type: types.StringType, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, }, - "test_required": { + "test_required": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, }, request: &tfprotov5.ReadResourceRequest{ CurrentState: testEmptyDynamicValue, - ProviderMeta: testCurrentStateValue, + ProviderMeta: testProviderMetaDynamicValue, TypeName: "test_resource", }, expectedResponse: &tfprotov5.ReadResourceResponse{ diff --git a/internal/proto6server/server_applyresourcechange_test.go b/internal/proto6server/server_applyresourcechange_test.go index e2eb03f31..d2748c9c5 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -9,13 +9,13 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -57,11 +57,10 @@ func TestServerApplyResourceChange(t *testing.T) { "test_provider_meta_attribute": tftypes.NewValue(tftypes.String, "test-provider-meta-value"), }) - testProviderMetaSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_provider_meta_attribute": { + testProviderMetaSchema := metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_provider_meta_attribute": metaschema.StringAttribute{ Optional: true, - Type: types.StringType, }, }, } @@ -233,8 +232,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -559,8 +558,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -946,8 +945,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -1015,8 +1014,8 @@ func TestServerApplyResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index b3266b09a..5f2bd7192 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -8,16 +8,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" providerschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" @@ -253,15 +251,14 @@ func TestServerGetProviderSchema(t *testing.T) { FrameworkServer: fwserver.Server{ Provider: &testprovider.ProviderWithMetaSchema{ Provider: &testprovider.Provider{}, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, diff --git a/internal/proto6server/server_planresourcechange_test.go b/internal/proto6server/server_planresourcechange_test.go index 4a81b3c76..764c21f78 100644 --- a/internal/proto6server/server_planresourcechange_test.go +++ b/internal/proto6server/server_planresourcechange_test.go @@ -5,13 +5,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -55,11 +55,10 @@ func TestServerPlanResourceChange(t *testing.T) { "test_provider_meta_attribute": tftypes.NewValue(tftypes.String, "test-provider-meta-value"), }) - testProviderMetaSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_provider_meta_attribute": { + testProviderMetaSchema := metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_provider_meta_attribute": metaschema.StringAttribute{ Optional: true, - Type: types.StringType, }, }, } @@ -207,8 +206,8 @@ func TestServerPlanResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -473,8 +472,8 @@ func TestServerPlanResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, @@ -836,8 +835,8 @@ func TestServerPlanResourceChange(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testProviderMetaSchema, nil + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = testProviderMetaSchema }, }, }, diff --git a/internal/proto6server/server_readdatasource_test.go b/internal/proto6server/server_readdatasource_test.go index a7b818285..277936f5d 100644 --- a/internal/proto6server/server_readdatasource_test.go +++ b/internal/proto6server/server_readdatasource_test.go @@ -7,10 +7,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -33,6 +33,19 @@ func TestServerReadDataSource(t *testing.T) { testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + testProviderMetaDynamicValue := testNewDynamicValue(t, + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional": tftypes.String, + "test_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_optional": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }, + ) + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), @@ -142,7 +155,7 @@ func TestServerReadDataSource(t *testing.T) { }, ReadMethod: func(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config struct { - TestComputed types.String `tfsdk:"test_computed"` + TestOptional types.String `tfsdk:"test_optional"` TestRequired types.String `tfsdk:"test_required"` } @@ -157,26 +170,24 @@ func TestServerReadDataSource(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { - Computed: true, - Type: types.StringType, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, }, - "test_required": { + "test_required": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, }, request: &tfprotov6.ReadDataSourceRequest{ Config: testEmptyDynamicValue, - ProviderMeta: testConfigDynamicValue, + ProviderMeta: testProviderMetaDynamicValue, TypeName: "test_data_source", }, expectedResponse: &tfprotov6.ReadDataSourceResponse{ @@ -284,6 +295,9 @@ func TestServerReadDataSource(t *testing.T) { } if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + for _, d := range got.Diagnostics { + t.Logf("%s: %s\n%s\n", d.Severity, d.Summary, d.Detail) + } t.Errorf("unexpected response difference: %s", diff) } }) diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 0f799d17a..1b70c824f 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -9,13 +9,13 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -43,6 +43,19 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov6.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + testProviderMetaDynamicValue := testNewDynamicValue(t, + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional": tftypes.String, + "test_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_optional": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }, + ) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -143,14 +156,14 @@ func TestServerReadResource(t *testing.T) { }, ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data struct { - TestComputed types.String `tfsdk:"test_computed"` + TestOptional types.String `tfsdk:"test_optional"` TestRequired types.String `tfsdk:"test_required"` } resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestRequired.ValueString() != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestRequired.ValueString()) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta", data.TestRequired.ValueString()) } }, } @@ -158,26 +171,24 @@ func TestServerReadResource(t *testing.T) { } }, }, - GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { - Computed: true, - Type: types.StringType, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, }, - "test_required": { + "test_required": metaschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, }, }, }, request: &tfprotov6.ReadResourceRequest{ CurrentState: testEmptyDynamicValue, - ProviderMeta: testCurrentStateValue, + ProviderMeta: testProviderMetaDynamicValue, TypeName: "test_resource", }, expectedResponse: &tfprotov6.ReadResourceResponse{ diff --git a/internal/testing/testprovider/providerwithmeta.go b/internal/testing/testprovider/providerwithmeta.go deleted file mode 100644 index 93c869116..000000000 --- a/internal/testing/testprovider/providerwithmeta.go +++ /dev/null @@ -1,29 +0,0 @@ -package testprovider - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" -) - -var _ provider.Provider = &ProviderWithMetaSchema{} -var _ provider.ProviderWithMetaSchema = &ProviderWithMetaSchema{} - -// Declarative provider.ProviderWithMetaSchema for unit testing. -type ProviderWithMetaSchema struct { - *Provider - - // ProviderWithMetaSchema interface methods - GetMetaSchemaMethod func(context.Context) (tfsdk.Schema, diag.Diagnostics) -} - -// GetMetaSchema satisfies the provider.ProviderWithMetaSchema interface. -func (p *ProviderWithMetaSchema) GetMetaSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { - if p.GetMetaSchemaMethod == nil { - return tfsdk.Schema{}, nil - } - - return p.GetMetaSchemaMethod(ctx) -} diff --git a/internal/testing/testprovider/providerwithmetaschema.go b/internal/testing/testprovider/providerwithmetaschema.go new file mode 100644 index 000000000..8a76b7771 --- /dev/null +++ b/internal/testing/testprovider/providerwithmetaschema.go @@ -0,0 +1,27 @@ +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +var _ provider.Provider = &ProviderWithMetaSchema{} +var _ provider.ProviderWithMetaSchema = &ProviderWithMetaSchema{} + +// Declarative provider.ProviderWithMetaSchema for unit testing. +type ProviderWithMetaSchema struct { + *Provider + + // ProviderWithMetaSchema interface methods + MetaSchemaMethod func(context.Context, provider.MetaSchemaRequest, *provider.MetaSchemaResponse) +} + +// MetaSchema satisfies the provider.ProviderWithMetaSchema interface. +func (p *ProviderWithMetaSchema) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + if p.MetaSchemaMethod == nil { + return + } + + p.MetaSchemaMethod(ctx, req, resp) +} diff --git a/provider/metaschema.go b/provider/metaschema.go new file mode 100644 index 000000000..9ce9080fb --- /dev/null +++ b/provider/metaschema.go @@ -0,0 +1,24 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" +) + +// MetaSchemaRequest represents a request for the Provider to return its schema. +// An instance of this request struct is supplied as an argument to the +// Provider type Schema method. +type MetaSchemaRequest struct{} + +// MetaSchemaResponse represents a response to a MetaSchemaRequest. An instance of this +// response struct is supplied as an argument to the Provider type Schema +// method. +type MetaSchemaResponse struct { + // Schema is the meta schema of the provider. + Schema metaschema.Schema + + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/provider/metaschema/attribute.go b/provider/metaschema/attribute.go new file mode 100644 index 000000000..ed2fe0399 --- /dev/null +++ b/provider/metaschema/attribute.go @@ -0,0 +1,33 @@ +package metaschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Attribute define a value field inside the Schema. Implementations in this +// package include: +// - BoolAttribute +// - Float64Attribute +// - Int64Attribute +// - ListAttribute +// - MapAttribute +// - NumberAttribute +// - ObjectAttribute +// - SetAttribute +// - StringAttribute +// +// Additionally, the NestedAttribute interface extends Attribute with nested +// attributes. Only supported in protocol version 6. Implementations in this +// package include: +// - ListNestedAttribute +// - MapNestedAttribute +// - SetNestedAttribute +// - SingleNestedAttribute +// +// In practitioner configurations, an equals sign (=) is required to set +// the value. [Configuration Reference] +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Attribute interface { + fwschema.Attribute +} diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go new file mode 100644 index 000000000..fc1b179fc --- /dev/null +++ b/provider/metaschema/bool_attribute.go @@ -0,0 +1,115 @@ +package metaschema + +import ( + "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 ( + _ Attribute = BoolAttribute{} +) + +// BoolAttribute represents a schema attribute that is a boolean. When +// retrieving the value for this attribute, use types.Bool as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a boolean or directly via the true/false keywords. +// +// example_attribute = true +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type BoolAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default types.BoolType. When retrieving data, the types.BoolValuable + // associated with this custom type must be used in place of types.Bool. + CustomType types.BoolTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a BoolAttribute. +func (a BoolAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a BoolAttribute +// and all fields are equal. +func (a BoolAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(BoolAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a BoolAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a BoolAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.BoolType +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a BoolAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a BoolAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a BoolAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a BoolAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/bool_attribute_test.go b/provider/metaschema/bool_attribute_test.go new file mode 100644 index 000000000..c389a6f3a --- /dev/null +++ b/provider/metaschema/bool_attribute_test.go @@ -0,0 +1,369 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.BoolType"), + }, + "ElementKeyInt": { + attribute: metaschema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.BoolType"), + }, + "ElementKeyString": { + attribute: metaschema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.BoolType"), + }, + "ElementKeyValue": { + attribute: metaschema.BoolAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to types.BoolType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.BoolAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: metaschema.BoolAttribute{}, + other: metaschema.BoolAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected string + }{ + "no-description": { + attribute: metaschema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: metaschema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.BoolAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.BoolAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: metaschema.BoolAttribute{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + "optional": { + attribute: metaschema.BoolAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + "required": { + attribute: metaschema.BoolAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/doc.go b/provider/metaschema/doc.go new file mode 100644 index 000000000..72d8f0c1a --- /dev/null +++ b/provider/metaschema/doc.go @@ -0,0 +1,5 @@ +// Package metaschema contains all available meta schema functionality for +// providers. Provider meta schemas define the structure and value types for +// provider_meta configuration data. Meta schemas are implemented via the +// provider.ProviderWithMetaSchema type MetaSchema method. +package metaschema diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go new file mode 100644 index 000000000..99fea520c --- /dev/null +++ b/provider/metaschema/float64_attribute.go @@ -0,0 +1,118 @@ +package metaschema + +import ( + "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 ( + _ Attribute = Float64Attribute{} +) + +// Float64Attribute represents a schema attribute that is a 64-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float64 as the value type unless the CustomType field is set. +// +// Use Int64Attribute for 64-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default types.Float64Type. When retrieving data, the types.Float64Valuable + // associated with this custom type must be used in place of types.Float64. + CustomType types.Float64Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float64Attribute. +func (a Float64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float64Attribute +// and all fields are equal. +func (a Float64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a Float64Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float64Type or the CustomType field value if defined. +func (a Float64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float64Type +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a Float64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Float64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a Float64Attribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/float64_attribute_test.go b/provider/metaschema/float64_attribute_test.go new file mode 100644 index 000000000..a553c49d2 --- /dev/null +++ b/provider/metaschema/float64_attribute_test.go @@ -0,0 +1,368 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.Float64Type"), + }, + "ElementKeyInt": { + attribute: metaschema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.Float64Type"), + }, + "ElementKeyString": { + attribute: metaschema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.Float64Type"), + }, + "ElementKeyValue": { + attribute: metaschema.Float64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to types.Float64Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.Float64Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: metaschema.Float64Attribute{}, + other: metaschema.Float64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected string + }{ + "no-description": { + attribute: metaschema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: metaschema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.Float64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.Float64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: metaschema.Float64Attribute{}, + expected: types.Float64Type, + }, + // "custom-type": { + // attribute: metaschema.Float64Attribute{ + // CustomType: testtypes.Float64Type{}, + // }, + // expected: testtypes.Float64Type{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + "optional": { + attribute: metaschema.Float64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + "required": { + attribute: metaschema.Float64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go new file mode 100644 index 000000000..08eed4fc8 --- /dev/null +++ b/provider/metaschema/int64_attribute.go @@ -0,0 +1,118 @@ +package metaschema + +import ( + "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 ( + _ Attribute = Int64Attribute{} +) + +// Int64Attribute represents a schema attribute that is a 64-bit integer. +// When retrieving the value for this attribute, use types.Int64 as the value +// type unless the CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default types.Int64Type. When retrieving data, the types.Int64Valuable + // associated with this custom type must be used in place of types.Int64. + CustomType types.Int64Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int64Attribute. +func (a Int64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int64Attribute +// and all fields are equal. +func (a Int64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a Int64Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int64Type or the CustomType field value if defined. +func (a Int64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int64Type +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a Int64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Int64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a Int64Attribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/int64_attribute_test.go b/provider/metaschema/int64_attribute_test.go new file mode 100644 index 000000000..26f318529 --- /dev/null +++ b/provider/metaschema/int64_attribute_test.go @@ -0,0 +1,368 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.Int64Type"), + }, + "ElementKeyInt": { + attribute: metaschema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.Int64Type"), + }, + "ElementKeyString": { + attribute: metaschema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.Int64Type"), + }, + "ElementKeyValue": { + attribute: metaschema.Int64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to types.Int64Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.Int64Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: metaschema.Int64Attribute{}, + other: metaschema.Int64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected string + }{ + "no-description": { + attribute: metaschema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: metaschema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.Int64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.Int64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: metaschema.Int64Attribute{}, + expected: types.Int64Type, + }, + // "custom-type": { + // attribute: metaschema.Int64Attribute{ + // CustomType: testtypes.Int64Type{}, + // }, + // expected: testtypes.Int64Type{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + "optional": { + attribute: metaschema.Int64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + "required": { + attribute: metaschema.Int64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go new file mode 100644 index 000000000..99d1877a6 --- /dev/null +++ b/provider/metaschema/list_attribute.go @@ -0,0 +1,128 @@ +package metaschema + +import ( + "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 ( + _ Attribute = ListAttribute{} +) + +// ListAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use ListNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a list or an element directly via square brace 0-based index syntax: +// +// # first known element +// .example_attribute[0] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType. When retrieving data, the types.ListValuable + // associated with this custom type must be used in place of types.List. + CustomType types.ListTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a list +// index or an error. +func (a ListAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ListAttribute +// and all fields are equal. +func (a ListAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a ListAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ListType or the CustomType field value if defined. +func (a ListAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a ListAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ListAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a ListAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/list_attribute_test.go b/provider/metaschema/list_attribute_test.go new file mode 100644 index 000000000..8ffd69d5c --- /dev/null +++ b/provider/metaschema/list_attribute_test.go @@ -0,0 +1,373 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + other: metaschema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + other: metaschema.ListAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected string + }{ + "no-description": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: metaschema.ListAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.ListAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: metaschema.ListAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: metaschema.ListAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: metaschema.ListAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go new file mode 100644 index 000000000..5afc97bd4 --- /dev/null +++ b/provider/metaschema/list_nested_attribute.go @@ -0,0 +1,155 @@ +package metaschema + +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 ( + _ NestedAttribute = ListNestedAttribute{} +) + +// ListNestedAttribute represents an attribute that is a list of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ListAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list of objects or directly via square and curly brace syntax. +// +// # list of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_attribute[0] +// # first known object nested_attribute value +// .example_attribute[0].nested_attribute +type ListNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // types.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType types.ListTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyInt, otherwise returns an error. +func (a ListNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a ListNestedAttribute +// and all fields are equal. +func (a ListNestedAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListNestedAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a ListNestedAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a ListNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a ListNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeList. +func (a ListNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeList +} + +// GetType returns ListType of ObjectType or CustomType. +func (a ListNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a ListNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ListNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a ListNestedAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/list_nested_attribute_test.go b/provider/metaschema/list_nested_attribute_test.go new file mode 100644 index 000000000..cafb56307 --- /dev/null +++ b/provider/metaschema/list_nested_attribute_test.go @@ -0,0 +1,523 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedAttribute"), + }, + "ElementKeyInt": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedAttribute"), + }, + "ElementKeyValue": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedAttribute"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-attributes": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected string + }{ + "no-description": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: metaschema.ListNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: metaschema.ListNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected metaschema.NestedAttributeObject + }{ + "nested-object": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: metaschema.ListNestedAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: metaschema.ListNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: metaschema.ListNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go new file mode 100644 index 000000000..5fa891dfc --- /dev/null +++ b/provider/metaschema/map_attribute.go @@ -0,0 +1,131 @@ +package metaschema + +import ( + "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 ( + _ Attribute = MapAttribute{} +) + +// MapAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use MapNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via curly brace syntax. +// +// # map of strings +// example_attribute = { +// key1 = "first", +// key2 = "second", +// } +// +// Terraform configurations reference this attribute using expressions that +// accept a map or an element directly via square brace string syntax: +// +// # key1 known element +// .example_attribute["key1"] +type MapAttribute struct { + // ElementType is the type for all elements of the map. This field must be + // set. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default types.MapType. When retrieving data, the types.MapValuable + // associated with this custom type must be used in place of types.Map. + CustomType types.MapTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a map +// index or an error. +func (a MapAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a MapAttribute +// and all fields are equal. +func (a MapAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(MapAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a MapAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a MapAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.MapType or the CustomType field value if defined. +func (a MapAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a MapAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a MapAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a MapAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/map_attribute_test.go b/provider/metaschema/map_attribute_test.go new file mode 100644 index 000000000..47524df5c --- /dev/null +++ b/provider/metaschema/map_attribute_test.go @@ -0,0 +1,373 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapType"), + }, + "ElementKeyInt": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapType"), + }, + "ElementKeyString": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-element-type": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + other: metaschema.MapAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + other: metaschema.MapAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected string + }{ + "no-description": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: metaschema.MapAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.MapAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: types.MapType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: metaschema.MapAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: metaschema.MapAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: metaschema.MapAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go new file mode 100644 index 000000000..5a8781db3 --- /dev/null +++ b/provider/metaschema/map_nested_attribute.go @@ -0,0 +1,155 @@ +package metaschema + +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 ( + _ NestedAttribute = MapNestedAttribute{} +) + +// MapNestedAttribute represents an attribute that is a set of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use MapAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set of objects or directly via curly brace syntax. +// +// # map of objects +// example_attribute = { +// key = { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a map of objects or an element directly via square brace string +// syntax: +// +// # known object at key +// .example_attribute["key"] +// # known object nested_attribute value at key +// .example_attribute["key"].nested_attribute +type MapNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.MapType of types.ObjectType. When retrieving data, the + // types.MapValuable associated with this custom type must be used in + // place of types.Map. + CustomType types.MapTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyString, otherwise returns an error. +func (a MapNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyString) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to MapNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a MapNestedAttribute +// and all fields are equal. +func (a MapNestedAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(MapNestedAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a MapNestedAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a MapNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeList. +func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeMap +} + +// GetType returns MapType of ObjectType or CustomType. +func (a MapNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a MapNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a MapNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a MapNestedAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/map_nested_attribute_test.go b/provider/metaschema/map_nested_attribute_test.go new file mode 100644 index 000000000..bd795a63d --- /dev/null +++ b/provider/metaschema/map_nested_attribute_test.go @@ -0,0 +1,523 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapNestedAttribute"), + }, + "ElementKeyInt": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapNestedAttribute"), + }, + "ElementKeyString": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapNestedAttribute"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-attributes": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected string + }{ + "no-description": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: metaschema.MapNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: metaschema.MapNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected metaschema.NestedAttributeObject + }{ + "nested-object": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: metaschema.MapNestedAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: metaschema.MapNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: metaschema.MapNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.MapNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/nested_attribute.go b/provider/metaschema/nested_attribute.go new file mode 100644 index 000000000..cd96146d1 --- /dev/null +++ b/provider/metaschema/nested_attribute.go @@ -0,0 +1,11 @@ +package metaschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Nested attributes are only compatible with protocol version 6. +type NestedAttribute interface { + Attribute + fwschema.NestedAttribute +} diff --git a/provider/metaschema/nested_attribute_object.go b/provider/metaschema/nested_attribute_object.go new file mode 100644 index 000000000..a893f6c3c --- /dev/null +++ b/provider/metaschema/nested_attribute_object.go @@ -0,0 +1,60 @@ +package metaschema + +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 a ListNestedAttribute, MapNestedAttribute, SetNestedAttribute, or +// SingleNestedAttribute (automatically generated). When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. The Attributes field must be set. Nested attributes are only +// compatible with protocol version 6. +// +// This object enables customizing and simplifying details within its parent +// NestedAttribute, therefore it cannot have Terraform schema fields such as +// Required, Description, etc. +type NestedAttributeObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default types.ObjectType. When retrieving data, the types.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType types.ObjectTypable +} + +// 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 schemaAttributes(o.Attributes) +} + +// Type returns the framework type of the NestedAttributeObject. +func (o NestedAttributeObject) Type() types.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedAttributeObjectType(o) +} diff --git a/provider/metaschema/nested_attribute_object_test.go b/provider/metaschema/nested_attribute_object_test.go new file mode 100644 index 000000000..9b3d2978c --- /dev/null +++ b/provider/metaschema/nested_attribute_object_test.go @@ -0,0 +1,237 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object metaschema.NestedAttributeObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: metaschema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on NestedAttributeObject"), + }, + "ElementKeyInt": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedAttributeObject"), + }, + "ElementKeyString": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedAttributeObject"), + }, + "ElementKeyValue": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedAttributeObject"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object metaschema.NestedAttributeObject + other fwschema.NestedAttributeObject + expected bool + }{ + "different-attributes": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + other: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + other: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object metaschema.NestedAttributeObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: metaschema.NestedAttributeObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr1": metaschema.StringAttribute{}, + "testattr2": metaschema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": metaschema.StringAttribute{}, + "testattr2": metaschema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object metaschema.NestedAttributeObject + expected attr.Type + }{ + "base": { + object: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + // "custom-type": { + // block: metaschema.NestedAttributeObject{ + // CustomType: testtypes.SingleType{}, + // }, + // expected: testtypes.SingleType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go new file mode 100644 index 000000000..8c99edc51 --- /dev/null +++ b/provider/metaschema/number_attribute.go @@ -0,0 +1,119 @@ +package metaschema + +import ( + "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 ( + _ Attribute = NumberAttribute{} +) + +// NumberAttribute represents a schema attribute that is a generic number with +// up to 512 bits of floating point or integer precision. When retrieving the +// value for this attribute, use types.Number as the value type unless the +// CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// Int64Attribute for 64-bit integer number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point or integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type NumberAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default types.NumberType. When retrieving data, the types.NumberValuable + // associated with this custom type must be used in place of types.Number. + CustomType types.NumberTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a NumberAttribute. +func (a NumberAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a NumberAttribute +// and all fields are equal. +func (a NumberAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(NumberAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a NumberAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.NumberType or the CustomType field value if defined. +func (a NumberAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.NumberType +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a NumberAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a NumberAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a NumberAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a NumberAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/number_attribute_test.go b/provider/metaschema/number_attribute_test.go new file mode 100644 index 000000000..fba9c3546 --- /dev/null +++ b/provider/metaschema/number_attribute_test.go @@ -0,0 +1,369 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.NumberType"), + }, + "ElementKeyInt": { + attribute: metaschema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.NumberType"), + }, + "ElementKeyString": { + attribute: metaschema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.NumberType"), + }, + "ElementKeyValue": { + attribute: metaschema.NumberAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to types.NumberType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.NumberAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: metaschema.NumberAttribute{}, + other: metaschema.NumberAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected string + }{ + "no-description": { + attribute: metaschema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: metaschema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.NumberAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.NumberAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: metaschema.NumberAttribute{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + "optional": { + attribute: metaschema.NumberAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + "required": { + attribute: metaschema.NumberAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go new file mode 100644 index 000000000..954fd72e7 --- /dev/null +++ b/provider/metaschema/object_attribute.go @@ -0,0 +1,130 @@ +package metaschema + +import ( + "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 ( + _ Attribute = ObjectAttribute{} +) + +// ObjectAttribute represents a schema attribute that is an object with only +// type information for underlying attributes. When retrieving the value for +// this attribute, use types.Object as the value type unless the CustomType +// field is set. The AttributeTypes field must be set. +// +// Prefer SingleNestedAttribute over ObjectAttribute if the provider is +// using protocol version 6 and full attribute functionality is needed. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # object with one attribute +// example_attribute = { +// underlying_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute directly via period syntax: +// +// # underlying attribute +// .example_attribute.underlying_attribute +type ObjectAttribute struct { + // AttributeTypes is the mapping of underlying attribute names to attribute + // types. This field must be set. + AttributeTypes map[string]attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default types.ObjectType. When retrieving data, the types.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType types.ObjectTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into an +// attribute name or an error. +func (a ObjectAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ObjectAttribute +// and all fields are equal. +func (a ObjectAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ObjectAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a ObjectAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a ObjectAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ObjectAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ObjectType or the CustomType field value if defined. +func (a ObjectAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ObjectType{ + AttrTypes: a.AttributeTypes, + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a ObjectAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ObjectAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ObjectAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a ObjectAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/object_attribute_test.go b/provider/metaschema/object_attribute_test.go new file mode 100644 index 000000000..ba44ef9a2 --- /dev/null +++ b/provider/metaschema/object_attribute_test.go @@ -0,0 +1,379 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("testattr"), + expected: types.StringType, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: nil, // types.ObjectType implementation returns no error + }, + "ElementKeyInt": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to ObjectType"), + }, + "ElementKeyString": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ObjectType"), + }, + "ElementKeyValue": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ObjectType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attribute-type": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.BoolType}}, + expected: false, + }, + "equal": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected string + }{ + "no-description": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "description": { + attribute: metaschema.ObjectAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.ObjectAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, + }, + // "custom-type": { + // attribute: metaschema.ObjectAttribute{ + // CustomType: testtypes.ObjectType{}, + // }, + // expected: testtypes.ObjectType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "optional": { + attribute: metaschema.ObjectAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "required": { + attribute: metaschema.ObjectAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/schema.go b/provider/metaschema/schema.go new file mode 100644 index 000000000..dde25bf3c --- /dev/null +++ b/provider/metaschema/schema.go @@ -0,0 +1,108 @@ +package metaschema + +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" +) + +// Schema must satify the fwschema.Schema interface. +var _ fwschema.Schema = Schema{} + +// Schema defines the structure and value types of provider_meta configuration +// data. This type is used as the provider.MetaSchemaResponse type Schema +// field, which is implemented by the provider.ProviderWithMetaSchema type +// MetaSchema method. +type Schema 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 +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// schema. +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// 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, 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(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes returns the Attributes field value. +func (s Schema) GetAttributes() map[string]fwschema.Attribute { + return schemaAttributes(s.Attributes) +} + +// GetBlocks always returns nil as meta schemas cannot contain blocks. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return nil +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for meta schemas. +func (s Schema) GetDeprecationMessage() string { + return "" +} + +// GetDescription always returns an empty string as there is no purpose for +// a meta schema description. The provider schema description should describe +// the provider itself. +func (s Schema) GetDescription() string { + return "" +} + +// GetMarkdownDescription always returns an empty string as there is no purpose +// for a meta schema description. The provider schema description should +// describe the provider itself. +func (s Schema) GetMarkdownDescription() string { + return "" +} + +// GetVersion always returns 0 as provider meta schemas cannot be versioned. +func (s Schema) GetVersion() int64 { + return 0 +} + +// Type returns the framework type of the schema. +func (s Schema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath returns the framework type at the given schema path. +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(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} + +// schemaAttributes is a provider to fwschema type conversion function. +func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { + result := make(map[string]fwschema.Attribute, len(attributes)) + + for name, attribute := range attributes { + result[name] = attribute + } + + return result +} diff --git a/provider/metaschema/schema_test.go b/provider/metaschema/schema_test.go new file mode 100644 index 000000000..e5722f9bc --- /dev/null +++ b/provider/metaschema/schema_test.go @@ -0,0 +1,808 @@ +package metaschema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "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-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: metaschema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: metaschema.Schema{}, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "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"+ + "Path: \n"+ + "Original Error: got unexpected type metaschema.Schema", + ), + }, + }, + "root": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "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"+ + "Path: \n"+ + "Original Error: got unexpected type metaschema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "other": metaschema.BoolAttribute{}, + "test": metaschema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: metaschema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: path.Empty().AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "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"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "WithElementKeyString": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: path.Empty().AtMapKey("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("test"), + "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"+ + "Path: [\"test\"]\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "WithElementKeyValue": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: path.Empty().AtSetValue(types.StringValue("test")), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringValue("test")), + "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"+ + "Path: [Value(\"test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.schema.AttributeAtPath(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: metaschema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type metaschema.Schema", + }, + "empty-nil": { + schema: metaschema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type metaschema.Schema", + }, + "root": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type metaschema.Schema", + }, + "nil": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type metaschema.Schema", + }, + "WithAttributeName-attribute": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "other": metaschema.BoolAttribute{}, + "test": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: metaschema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: nil, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: nil, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtTerraformPath(context.Background(), tc.path) + + 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 err == nil && 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 result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: metaschema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr1": metaschema.StringAttribute{}, + "testattr2": metaschema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": metaschema.StringAttribute{}, + "testattr2": metaschema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: metaschema.Schema{}, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected string + }{ + "no-deprecation-message": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected string + }{ + "no-description": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected string + }{ + "no-markdown-description": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected int64 + }{ + "0": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetVersion() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expected attr.Type + }{ + "base": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: metaschema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "bool": metaschema.BoolAttribute{}, + "string": metaschema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "bool": metaschema.BoolAttribute{}, + "string": metaschema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: metaschema.Schema{}, + path: path.Root("non-existent"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("non-existent"), + "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"+ + "Path: non-existent\n"+ + "Original Error: AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema", + ), + }, + }, + "ElementKeyInt": { + schema: metaschema.Schema{}, + path: path.Empty().AtListIndex(0), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "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"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + schema: metaschema.Schema{}, + path: path.Empty().AtMapKey("invalid"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("invalid"), + "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"+ + "Path: [\"invalid\"]\n"+ + "Original Error: ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + schema: metaschema.Schema{}, + path: path.Empty().AtSetValue(types.StringNull()), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringNull()), + "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"+ + "Path: [Value()]\n"+ + "Original Error: ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.schema.TypeAtPath(context.Background(), testCase.path) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: metaschema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: metaschema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "bool": metaschema.BoolAttribute{}, + "string": metaschema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "bool": metaschema.BoolAttribute{}, + "string": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "bool": metaschema.BoolAttribute{}, + "string": metaschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: metaschema.Schema{}, + path: tftypes.NewAttributePath().WithAttributeName("non-existent"), + expectedError: fmt.Errorf("AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema"), + }, + "ElementKeyInt": { + schema: metaschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expectedError: fmt.Errorf("ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: metaschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyString("invalid"), + expectedError: fmt.Errorf("ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: metaschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + expectedError: fmt.Errorf("ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.TypeAtTerraformPath(context.Background(), testCase.path) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go new file mode 100644 index 000000000..7261145e2 --- /dev/null +++ b/provider/metaschema/set_attribute.go @@ -0,0 +1,126 @@ +package metaschema + +import ( + "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 ( + _ Attribute = SetAttribute{} +) + +// SetAttribute represents a schema attribute that is a set with a single +// element type. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use SetNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set or directly via square brace syntax. +// +// # set of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a set. Sets cannot be indexed in Terraform, therefore an expression +// is required to access an explicit element. +type SetAttribute struct { + // ElementType is the type for all elements of the set. This field must be + // set. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType. When retrieving data, the types.SetValuable + // associated with this custom type must be used in place of types.Set. + CustomType types.SetTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a set +// index or an error. +func (a SetAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a SetAttribute +// and all fields are equal. +func (a SetAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(SetAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a SetAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a SetAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.SetType or the CustomType field value if defined. +func (a SetAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.ElementType, + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a SetAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a SetAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a SetAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/set_attribute_test.go b/provider/metaschema/set_attribute_test.go new file mode 100644 index 000000000..473166179 --- /dev/null +++ b/provider/metaschema/set_attribute_test.go @@ -0,0 +1,373 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetType"), + }, + "ElementKeyInt": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetType"), + }, + "ElementKeyString": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetType"), + }, + "ElementKeyValue": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: types.StringType, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-element-type": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + other: metaschema.SetAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + other: metaschema.SetAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected string + }{ + "no-description": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: metaschema.SetAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.SetAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: types.SetType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: metaschema.SetAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: metaschema.SetAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: metaschema.SetAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go new file mode 100644 index 000000000..d3cc9f69f --- /dev/null +++ b/provider/metaschema/set_nested_attribute.go @@ -0,0 +1,150 @@ +package metaschema + +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 ( + _ NestedAttribute = SetNestedAttribute{} +) + +// SetNestedAttribute represents an attribute that is a set of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use SetAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set of objects or directly via square and curly brace syntax. +// +// # set of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a set of objects. Sets cannot be indexed in Terraform, therefore +// an expression is required to access an explicit element. +type SetNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // types.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType types.SetTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyValue, otherwise returns an error. +func (a SetNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a SetNestedAttribute +// and all fields are equal. +func (a SetNestedAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(SetNestedAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a SetNestedAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a SetNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeList. +func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSet +} + +// GetType returns SetType of ObjectType or CustomType. +func (a SetNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a SetNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a SetNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a SetNestedAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/set_nested_attribute_test.go b/provider/metaschema/set_nested_attribute_test.go new file mode 100644 index 000000000..d434203d7 --- /dev/null +++ b/provider/metaschema/set_nested_attribute_test.go @@ -0,0 +1,523 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedAttribute"), + }, + "ElementKeyInt": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedAttribute"), + }, + "ElementKeyString": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedAttribute"), + }, + "ElementKeyValue": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-attributes": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + other: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected string + }{ + "no-description": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: metaschema.SetNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: metaschema.SetNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected metaschema.NestedAttributeObject + }{ + "nested-object": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: metaschema.SetNestedAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: metaschema.SetNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: metaschema.SetNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.SetNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go new file mode 100644 index 000000000..58c0105e6 --- /dev/null +++ b/provider/metaschema/single_nested_attribute.go @@ -0,0 +1,170 @@ +package metaschema + +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 ( + _ NestedAttribute = SingleNestedAttribute{} +) + +// SingleNestedAttribute represents an attribute that is a single object where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Object +// as the value type unless the CustomType field is set. The Attributes field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ObjectAttribute if the underlying attributes do not require definition +// beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # single object +// example_attribute = { +// nested_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_attribute.nested_attribute +type SingleNestedAttribute struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default types.ObjectType. When retrieving data, the types.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType types.ObjectTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (a SingleNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedAttribute", step) + } + + attribute, ok := a.Attributes[string(name)] + + if !ok { + return nil, fmt.Errorf("no attribute %q on SingleNestedAttribute", name) + } + + return attribute, nil +} + +// Equal returns true if the given Attribute is a SingleNestedAttribute +// and all fields are equal. +func (a SingleNestedAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(SingleNestedAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetAttributes returns the Attributes field value. +func (a SingleNestedAttribute) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(a.Attributes) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a SingleNestedAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a SingleNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SingleNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns a generated NestedAttributeObject from the +// Attributes and CustomType field values. +func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return NestedAttributeObject{ + Attributes: a.Attributes, + CustomType: a.CustomType, + } +} + +// GetNestingMode always returns NestingModeList. +func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSingle +} + +// GetType returns ListType of ObjectType or CustomType. +func (a SingleNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + attrTypes := make(map[string]attr.Type, len(a.Attributes)) + + for name, attribute := range a.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a SingleNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a SingleNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SingleNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a SingleNestedAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/single_nested_attribute_test.go b/provider/metaschema/single_nested_attribute_test.go new file mode 100644 index 000000000..cbcf9be89 --- /dev/null +++ b/provider/metaschema/single_nested_attribute_test.go @@ -0,0 +1,491 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: metaschema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on SingleNestedAttribute"), + }, + "ElementKeyInt": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedAttribute"), + }, + "ElementKeyString": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedAttribute"), + }, + "ElementKeyValue": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedAttribute"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attributes": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + other: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + other: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected string + }{ + "no-description": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + attribute: metaschema.SingleNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: metaschema.SingleNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected metaschema.NestedAttributeObject + }{ + "nested-object": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + // "custom-type": { + // attribute: metaschema.SingleNestedAttribute{ + // CustomType: testtypes.SingleType{}, + // }, + // expected: testtypes.SingleType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: false, + }, + "optional": { + attribute: metaschema.SingleNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: false, + }, + "required": { + attribute: metaschema.SingleNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "testattr": metaschema.StringAttribute{}, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go new file mode 100644 index 000000000..bf5da78b0 --- /dev/null +++ b/provider/metaschema/string_attribute.go @@ -0,0 +1,115 @@ +package metaschema + +import ( + "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 ( + _ Attribute = StringAttribute{} +) + +// StringAttribute represents a schema attribute that is a string. When +// retrieving the value for this attribute, use types.String as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a string or directly via double quote syntax. +// +// example_attribute = "value" +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type StringAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default types.StringType. When retrieving data, the types.StringValuable + // associated with this custom type must be used in place of types.String. + CustomType types.StringTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a StringAttribute. +func (a StringAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a StringAttribute +// and all fields are equal. +func (a StringAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(StringAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage always returns an empty string as there is no +// deprecation validation support for provider meta schemas. +func (a StringAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a StringAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a StringAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.StringType +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a StringAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a StringAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a StringAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as there is no plan for provider meta +// schema data. +func (a StringAttribute) IsSensitive() bool { + return false +} diff --git a/provider/metaschema/string_attribute_test.go b/provider/metaschema/string_attribute_test.go new file mode 100644 index 000000000..ef150c1e9 --- /dev/null +++ b/provider/metaschema/string_attribute_test.go @@ -0,0 +1,369 @@ +package metaschema_test + +import ( + "fmt" + "strings" + "testing" + + "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/internal/testing/testschema" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: metaschema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.StringType"), + }, + "ElementKeyInt": { + attribute: metaschema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.StringType"), + }, + "ElementKeyString": { + attribute: metaschema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.StringType"), + }, + "ElementKeyValue": { + attribute: metaschema.StringAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to types.StringType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: metaschema.StringAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: metaschema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: metaschema.StringAttribute{}, + other: metaschema.StringAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected string + }{ + "no-description": { + attribute: metaschema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: metaschema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: metaschema.StringAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: metaschema.StringAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: metaschema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: metaschema.StringAttribute{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + "optional": { + attribute: metaschema.StringAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-required": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + "required": { + attribute: metaschema.StringAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/provider.go b/provider/provider.go index a7fbea07c..8586fadfc 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -15,11 +15,7 @@ import ( // // Providers can optionally implement these additional concepts: // -// - Resources: ProviderWithResources or (deprecated) -// ProviderWithGetResources. -// - Data Sources: ProviderWithDataSources or (deprecated) -// ProviderWithGetDataSources. -// - Validation: Schema-based via tfsdk.Attribute or entire configuration +// - Validation: Schema-based or entire configuration // via ProviderWithConfigValidators or ProviderWithValidateConfig. // - Meta Schema: ProviderWithMetaSchema type Provider interface { @@ -89,15 +85,25 @@ type ProviderWithMetadata interface { Metadata(context.Context, MetadataRequest, *MetadataResponse) } -// ProviderWithMetaSchema is a provider with a provider meta schema. +// ProviderWithMetaSchema is a provider with a provider meta schema, which +// is configured by practitioners via the provider_meta configuration block +// and the configuration data is included with certain data source and resource +// operations. The intended use case is to enable Terraform module authors +// within the same organization of the provider to track module usage in +// requests. Other use cases are explicitly not supported. All provider +// instances (aliases) receive the same data. +// // This functionality is currently experimental and subject to change or break -// without warning; it should only be used by providers that are collaborating -// on its use with the Terraform team. +// without warning. It is not protected by version compatibility guarantees. type ProviderWithMetaSchema interface { Provider - // GetMetaSchema returns the provider meta schema. - GetMetaSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) + // MetaSchema should return the meta schema for this provider. + // + // This functionality is currently experimental and subject to change or + // break without warning. It is not protected by version compatibility + // guarantees. + MetaSchema(context.Context, MetaSchemaRequest, *MetaSchemaResponse) } // ProviderWithSchema is a temporary interface type that extends diff --git a/provider/schema.go b/provider/schema.go index cb53bad0a..ce8337f85 100644 --- a/provider/schema.go +++ b/provider/schema.go @@ -14,7 +14,7 @@ type SchemaRequest struct{} // response struct is supplied as an argument to the Provider type Schema // method. type SchemaResponse struct { - // Schema is the schema of the data source. + // Schema is the schema of the provider. Schema schema.Schema // Diagnostics report errors or warnings related to validating the data