diff --git a/.changelog/477.txt b/.changelog/477.txt new file mode 100644 index 000000000..9abb61e63 --- /dev/null +++ b/.changelog/477.txt @@ -0,0 +1,7 @@ +```release-note:note +tfsdk: Schema definitions may now introduce single nested mode blocks, however this support is only intended for migrating terraform-plugin-sdk timeouts blocks. New implementations should prefer single nested attributes instead. +``` + +```release-note:enhancement +tfsdk: Added single nested mode block support +``` diff --git a/internal/fwschema/block_nested_mode.go b/internal/fwschema/block_nested_mode.go index 5206b3024..d92e622b3 100644 --- a/internal/fwschema/block_nested_mode.go +++ b/internal/fwschema/block_nested_mode.go @@ -3,10 +3,11 @@ package fwschema // BlockNestingMode is an enum type of the ways attributes and blocks can be // nested in a block. They can be a list or a set. // -// While the protocol and theoretically Terraform itself support map, single, -// and group nesting modes, this framework intentionally only supports list -// and set blocks as those other modes were not typically implemented or -// tested since the older Terraform Plugin SDK did not support them. +// While the protocol and theoretically Terraform itself support map and group +// nesting modes, this framework intentionally only supports list, set, and +// single blocks as those other modes were not typically implemented or +// tested with Terraform since the older Terraform Plugin SDK did not support +// them. type BlockNestingMode uint8 const ( @@ -23,4 +24,13 @@ const ( // with multiple, unique instances of those attributes nested inside a // set under another attribute. BlockNestingModeSet BlockNestingMode = 2 + + // BlockNestingModeSingle is for attributes that represent a single object. + // The object cannot be repeated in the practitioner configuration. + // + // While the framework implements support for this block nesting mode, it + // is not thoroughly tested in production Terraform environments beyond the + // resource timeouts block from the older Terraform Plugin SDK. Use single + // nested attributes for new implementations instead. + BlockNestingModeSingle BlockNestingMode = 3 ) diff --git a/internal/fwschemadata/data_get_at_path_test.go b/internal/fwschemadata/data_get_at_path_test.go index 15b405b4a..db524bf5b 100644 --- a/internal/fwschemadata/data_get_at_path_test.go +++ b/internal/fwschemadata/data_get_at_path_test.go @@ -5164,6 +5164,454 @@ func TestDataGetAtPath(t *testing.T) { "test2", }, }, + "SingleBlock-types.Object-null": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + nil, + ), + }, + ), + }, + path: path.Root("object"), + target: new(types.Object), + expected: &types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Null: true, + }, + }, + "SingleBlock-types.Object-unknown": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + }, + path: path.Root("object"), + target: new(types.Object), + expected: &types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Unknown: true, + }, + }, + "SingleBlock-types.Object-value": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_string": tftypes.NewValue(tftypes.String, "test1"), + }, + ), + }, + ), + }, + path: path.Root("object"), + target: new(types.Object), + expected: &types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_string": types.String{Value: "test1"}, + }, + }, + }, + "SingleBlock-*struct-null": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + nil, + ), + }, + ), + }, + path: path.Root("object"), + target: new(*struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expected: new(*struct { + NestedString types.String `tfsdk:"nested_string"` + }), + }, + "SingleBlock-*struct-unknown": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + }, + path: path.Root("object"), + target: new(*struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expected: new(*struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("object"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "unhandled unknown value", + ), + }, + }, + "SingleBlock-*struct-value": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_string": tftypes.NewValue(tftypes.String, "test1"), + }, + ), + }, + ), + }, + path: path.Root("object"), + target: new(*struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expected: pointer(&struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{Value: "test1"}, + }), + }, + "SingleBlock-struct-null": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + nil, + ), + }, + ), + }, + path: path.Root("object"), + target: new(struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expected: &struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("object"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "unhandled null value", + ), + }, + }, + "SingleBlock-struct-unknown": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + }, + path: path.Root("object"), + target: new(struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expected: &struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("object"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "unhandled unknown value", + ), + }, + }, + "SingleBlock-struct-value": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_string": tftypes.NewValue(tftypes.String, "test1"), + }, + ), + }, + ), + }, + path: path.Root("object"), + target: new(struct { + NestedString types.String `tfsdk:"nested_string"` + }), + expected: &struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{Value: "test1"}, + }, + }, "SingleNestedAttributes-types.Object-null": { data: fwschemadata.Data{ Schema: tfsdk.Schema{ diff --git a/internal/fwschemadata/data_get_test.go b/internal/fwschemadata/data_get_test.go index 99c7b8cd8..95db886fe 100644 --- a/internal/fwschemadata/data_get_test.go +++ b/internal/fwschemadata/data_get_test.go @@ -5786,6 +5786,507 @@ func TestDataGet(t *testing.T) { }, }, }, + "SingleBlock-types.Object-null": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + nil, + ), + }, + ), + }, + target: new(struct { + Object types.Object `tfsdk:"object"` + }), + expected: &struct { + Object types.Object `tfsdk:"object"` + }{ + Object: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Null: true, + }, + }, + }, + "SingleBlock-types.Object-unknown": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + }, + target: new(struct { + Object types.Object `tfsdk:"object"` + }), + expected: &struct { + Object types.Object `tfsdk:"object"` + }{ + Object: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Unknown: true, + }, + }, + }, + "SingleBlock-types.Object-value": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_string": tftypes.NewValue(tftypes.String, "test1"), + }, + ), + }, + ), + }, + target: new(struct { + Object types.Object `tfsdk:"object"` + }), + expected: &struct { + Object types.Object `tfsdk:"object"` + }{ + Object: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_string": types.String{Value: "test1"}, + }, + }, + }, + }, + "SingleBlock-*struct-null": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + nil, + ), + }, + ), + }, + target: new(struct { + Object *struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }), + expected: &struct { + Object *struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }{ + Object: nil, + }, + }, + "SingleBlock-*struct-unknown": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + }, + target: new(struct { + Object *struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }), + expected: &struct { + Object *struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }{ + Object: nil, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("object"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "unhandled unknown value", + ), + }, + }, + "SingleBlock-*struct-value": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_string": tftypes.NewValue(tftypes.String, "test1"), + }, + ), + }, + ), + }, + target: new(struct { + Object *struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }), + expected: &struct { + Object *struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }{ + Object: &struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{Value: "test1"}, + }, + }, + }, + "SingleBlock-struct-null": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + nil, + ), + }, + ), + }, + target: new(struct { + Object struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }), + expected: &struct { + Object struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }{ + Object: struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("object"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "unhandled null value", + ), + }, + }, + "SingleBlock-struct-unknown": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + }, + target: new(struct { + Object struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }), + expected: &struct { + Object struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }{ + Object: struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("object"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "unhandled unknown value", + ), + }, + }, + "SingleBlock-struct-value": { + data: fwschemadata.Data{ + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "object": { + Attributes: map[string]tfsdk.Attribute{ + "nested_string": { + Optional: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "object": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_string": tftypes.NewValue(tftypes.String, "test1"), + }, + ), + }, + ), + }, + target: new(struct { + Object struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }), + expected: &struct { + Object struct { + NestedString types.String `tfsdk:"nested_string"` + } `tfsdk:"object"` + }{ + Object: struct { + NestedString types.String `tfsdk:"nested_string"` + }{ + NestedString: types.String{Value: "test1"}, + }, + }, + }, "SingleNestedAttributes-types.Object-null": { data: fwschemadata.Data{ Schema: tfsdk.Schema{ diff --git a/internal/fwschemadata/data_value_test.go b/internal/fwschemadata/data_value_test.go index 57f07d82a..791a9475d 100644 --- a/internal/fwschemadata/data_value_test.go +++ b/internal/fwschemadata/data_value_test.go @@ -1030,6 +1030,327 @@ func TestDataValueAtPath(t *testing.T) { }).AtName("sub_test"), expected: types.String{Value: "value"}, }, + "WithAttributeName-SingleBlock-null-WithAttributeName-Float64": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other_attr": tftypes.Bool, + "other_block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, + }, + }, map[string]tftypes.Value{ + "other_attr": tftypes.NewValue(tftypes.Bool, nil), + "other_block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, nil), + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, nil), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "other_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.BoolType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.Float64Type, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Float64{Null: true}, + }, + "WithAttributeName-SingleBlock-null-WithAttributeName-Int64": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other_attr": tftypes.Bool, + "other_block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, + }, + }, map[string]tftypes.Value{ + "other_attr": tftypes.NewValue(tftypes.Bool, nil), + "other_block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, nil), + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, nil), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "other_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.BoolType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.Int64Type, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Int64{Null: true}, + }, + "WithAttributeName-SingleBlock-null-WithAttributeName-Set": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other_attr": tftypes.Bool, + "other_block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Set{ + ElementType: tftypes.Bool, + }, + }, + }, + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "other_attr": tftypes.NewValue(tftypes.Bool, nil), + "other_block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Set{ + ElementType: tftypes.Bool, + }, + }, + }, nil), + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, nil), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "other_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.SetType{ + ElemType: types.BoolType, + }, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Set{ElemType: types.StringType, Null: true}, + }, + "WithAttributeName-SingleBlock-null-WithAttributeName-String": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other_attr": tftypes.Bool, + "other_block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "other_attr": tftypes.NewValue(tftypes.Bool, nil), + "other_block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, nil), + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, nil), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "other_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.BoolType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.String{Null: true}, + }, + "WithAttributeName-SingleBlock-WithAttributeName": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other_attr": tftypes.Bool, + "other_block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "other_attr": tftypes.NewValue(tftypes.Bool, nil), + "other_block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.Bool, true), + }), + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + }), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "other_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.BoolType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.String{Value: "value"}, + }, "WithAttributeName-SingleNestedAttributes-null-WithAttributeName-Float64": { data: fwschemadata.Data{ TerraformValue: tftypes.NewValue(tftypes.Object{ diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index 04302d889..a8de19322 100644 --- a/internal/fwserver/block_plan_modification.go +++ b/internal/fwserver/block_plan_modification.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" @@ -341,6 +342,136 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr } resp.AttributePlan = planSet + case fwschema.BlockNestingModeSingle: + configObject, diags := coerceObjectValue(req.AttributePath, req.AttributeConfig) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planObject, diags := coerceObjectValue(req.AttributePath, req.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + stateObject, diags := coerceObjectValue(req.AttributePath, req.AttributeState) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + if planObject.Attrs == nil { + planObject.Attrs = make(map[string]attr.Value) + } + + for name, attr := range b.GetAttributes() { + attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + attrReq := tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: attrConfig, + AttributePath: req.AttributePath.AtName(name), + AttributePlan: attrPlan, + AttributeState: attrState, + Config: req.Config, + Plan: req.Plan, + ProviderMeta: req.ProviderMeta, + State: req.State, + Private: resp.Private, + } + attrResp := ModifyAttributePlanResponse{ + AttributePlan: attrReq.AttributePlan, + RequiresReplace: resp.RequiresReplace, + Private: attrReq.Private, + } + + AttributeModifyPlan(ctx, attr, attrReq, &attrResp) + + planObject.Attrs[name] = attrResp.AttributePlan + resp.Diagnostics.Append(attrResp.Diagnostics...) + resp.RequiresReplace = attrResp.RequiresReplace + resp.Private = attrResp.Private + } + + for name, block := range b.GetBlocks() { + attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + blockReq := tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: attrConfig, + AttributePath: req.AttributePath.AtName(name), + AttributePlan: attrPlan, + AttributeState: attrState, + Config: req.Config, + Plan: req.Plan, + ProviderMeta: req.ProviderMeta, + State: req.State, + Private: resp.Private, + } + blockResp := ModifyAttributePlanResponse{ + AttributePlan: blockReq.AttributePlan, + RequiresReplace: resp.RequiresReplace, + Private: blockReq.Private, + } + + BlockModifyPlan(ctx, block, blockReq, &blockResp) + + planObject.Attrs[name] = blockResp.AttributePlan + resp.Diagnostics.Append(blockResp.Diagnostics...) + resp.RequiresReplace = blockResp.RequiresReplace + resp.Private = blockResp.Private + } + + resp.AttributePlan = planObject default: err := fmt.Errorf("unknown block plan modification nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index 0a59d9fba..861d21872 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -980,6 +980,232 @@ func TestBlockModifyPlan(t *testing.T) { Private: testEmptyProviderData, }, }, + "block-single-null-plan": { + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + req: tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + AttributePath: path.Root("test"), + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Null: true, + }, + AttributeState: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Null: true}, + }, + Null: true, + }, + Private: testProviderData, + }, + }, + "block-single-null-state": { + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + req: tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + AttributePath: path.Root("test"), + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + AttributeState: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Null: true, + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + Private: testProviderData, + }, + }, + "block-single-nested-private": { + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + req: tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + AttributePath: path.Root("test"), + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + AttributeState: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_attr": types.String{Value: "testvalue"}, + }, + }, + Private: testProviderData, + }, + }, + "block-single-nested-usestateforunknown": { + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "nested_computed": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + }, + "nested_required": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + req: tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + "nested_required": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_computed": types.String{Null: true}, + "nested_required": types.String{Value: "testvalue"}, + }, + }, + AttributePath: path.Root("test"), + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + "nested_required": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_computed": types.String{Unknown: true}, + "nested_required": types.String{Value: "testvalue"}, + }, + }, + AttributeState: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + "nested_required": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_computed": types.String{Value: "statevalue"}, + "nested_required": types.String{Value: "testvalue"}, + }, + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: types.Object{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + "nested_required": types.StringType, + }, + Attrs: map[string]attr.Value{ + "nested_computed": types.String{Value: "statevalue"}, + "nested_required": types.String{Value: "testvalue"}, + }, + }, + Private: testEmptyProviderData, + }, + }, "block-requires-replacement": { block: tfsdk.Block{ Attributes: map[string]tfsdk.Attribute{ diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index 17e2b85af..95c9c32ec 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -184,6 +184,53 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr if b.GetMinItems() > 0 && int64(len(s.Elems)) < b.GetMinItems() && !s.IsUnknown() { resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), len(s.Elems))) } + case fwschema.BlockNestingModeSingle: + s, ok := req.AttributeConfig.(types.Object) + + if !ok { + err := fmt.Errorf("unknown block value type (%s) for nesting mode (%T) at path: %s", req.AttributeConfig.Type(ctx), nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Block Validation Error", + "Block validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + for name, attr := range b.GetAttributes() { + nestedAttrReq := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.AtName(name), + AttributePathExpression: req.AttributePathExpression.AtName(name), + Config: req.Config, + } + nestedAttrResp := &tfsdk.ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + AttributeValidate(ctx, attr, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + + for name, block := range b.GetBlocks() { + nestedAttrReq := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.AtName(name), + AttributePathExpression: req.AttributePathExpression.AtName(name), + Config: req.Config, + } + nestedAttrResp := &tfsdk.ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + BlockValidate(ctx, block, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + + if b.GetMinItems() == 1 && s.IsNull() { + resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), 0)) + } default: err := fmt.Errorf("unknown block validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( diff --git a/internal/fwserver/block_validation_test.go b/internal/fwserver/block_validation_test.go index e776748eb..72dd5f120 100644 --- a/internal/fwserver/block_validation_test.go +++ b/internal/fwserver/block_validation_test.go @@ -2263,6 +2263,371 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "single-no-validation": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{}, + }, + "single-validation": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []tfsdk.AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + testErrorDiagnostic1, + }, + }, + }, + "single-maxitems-validation-known-valid": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Optional: true, + }, + }, + MaxItems: 1, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{}, + }, + "single-maxitems-validation-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + nil, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Optional: true, + }, + }, + MaxItems: 1, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{}, + }, + "single-maxitems-validation-unknown": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Optional: true, + }, + }, + MaxItems: 1, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{}, + }, + "single-minitems-validation-known-valid": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Optional: true, + }, + }, + MinItems: 1, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{}, + }, + "single-minitems-validation-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + nil, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Optional: true, + }, + }, + MinItems: 1, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Missing Block Configuration", + "The configuration should declare a minimum of 1 block, however 0 blocks were configured.", + ), + }, + }, + }, + "single-minitems-validation-unknown": { + req: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Optional: true, + }, + }, + MinItems: 1, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: tfsdk.ValidateAttributeResponse{}, + }, } for name, tc := range testCases { diff --git a/internal/toproto5/block.go b/internal/toproto5/block.go index 573799f49..660a08b44 100644 --- a/internal/toproto5/block.go +++ b/internal/toproto5/block.go @@ -38,6 +38,8 @@ func Block(ctx context.Context, name string, path *tftypes.AttributePath, b fwsc schemaNestedBlock.Nesting = tfprotov5.SchemaNestedBlockNestingModeList case fwschema.BlockNestingModeSet: schemaNestedBlock.Nesting = tfprotov5.SchemaNestedBlockNestingModeSet + case fwschema.BlockNestingModeSingle: + schemaNestedBlock.Nesting = tfprotov5.SchemaNestedBlockNestingModeSingle default: return nil, path.NewErrorf("unrecognized nesting mode %v", nm) } diff --git a/internal/toproto5/block_test.go b/internal/toproto5/block_test.go index 877576a95..7a53ecf33 100644 --- a/internal/toproto5/block_test.go +++ b/internal/toproto5/block_test.go @@ -272,6 +272,123 @@ func TestBlock(t *testing.T) { TypeName: "test", }, }, + "nestingmode-single-attributes": { + name: "test", + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test", + }, + }, + "nestingmode-single-attributes-and-blocks": { + name: "test", + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "sub_attr": { + Type: types.StringType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "sub_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_block_attr": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "sub_attr", + Optional: true, + Type: tftypes.String, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "sub_block_attr", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "sub_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test", + }, + }, + "nestingmode-single-blocks": { + name: "test", + block: tfsdk.Block{ + Blocks: map[string]tfsdk.Block{ + "sub_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_block_attr": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "sub_block_attr", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "sub_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test", + }, + }, "deprecationmessage": { name: "test", block: tfsdk.Block{ diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 0f1d26da8..cdee1e51e 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -929,6 +929,49 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "data-source-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "data-source-version": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1718,6 +1761,46 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "provider-version": { input: &fwserver.GetProviderSchemaResponse{ Provider: &tfsdk.Schema{ @@ -2504,6 +2587,46 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-meta-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + ProviderMeta: &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + ProviderMeta: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "provider-meta-version": { input: &fwserver.GetProviderSchemaResponse{ ProviderMeta: &tfsdk.Schema{ @@ -3418,6 +3541,49 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + }, + }, "resource-version": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto5/schema_test.go b/internal/toproto5/schema_test.go index 68fdb6a2e..ca2c00ce9 100644 --- a/internal/toproto5/schema_test.go +++ b/internal/toproto5/schema_test.go @@ -304,6 +304,28 @@ func TestSchema(t *testing.T) { }, NestingMode: tfsdk.BlockNestingModeSet, }, + "single": { + Attributes: map[string]tfsdk.Attribute{ + "string": { + Type: types.StringType, + Required: true, + }, + "number": { + Type: types.NumberType, + Optional: true, + }, + "bool": { + Type: types.BoolType, + Computed: true, + }, + "list": { + Type: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, }, }, expected: &tfprotov5.Schema{ @@ -368,6 +390,35 @@ func TestSchema(t *testing.T) { Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, TypeName: "set", }, + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "bool", + Type: tftypes.Bool, + }, + { + Computed: true, + Name: "list", + Optional: true, + Type: tftypes.List{ElementType: tftypes.String}, + }, + { + Name: "number", + Optional: true, + Type: tftypes.Number, + }, + { + Name: "string", + Required: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "single", + }, }, }, }, diff --git a/internal/toproto6/block.go b/internal/toproto6/block.go index fb6f60334..20db4dcd1 100644 --- a/internal/toproto6/block.go +++ b/internal/toproto6/block.go @@ -38,6 +38,8 @@ func Block(ctx context.Context, name string, path *tftypes.AttributePath, b fwsc schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeList case fwschema.BlockNestingModeSet: schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeSet + case fwschema.BlockNestingModeSingle: + schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeSingle default: return nil, path.NewErrorf("unrecognized nesting mode %v", nm) } diff --git a/internal/toproto6/block_test.go b/internal/toproto6/block_test.go index 5b46d22a2..7f106cf4e 100644 --- a/internal/toproto6/block_test.go +++ b/internal/toproto6/block_test.go @@ -272,6 +272,123 @@ func TestBlock(t *testing.T) { TypeName: "test", }, }, + "nestingmode-single-attributes": { + name: "test", + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test", + }, + }, + "nestingmode-single-attributes-and-blocks": { + name: "test", + block: tfsdk.Block{ + Attributes: map[string]tfsdk.Attribute{ + "sub_attr": { + Type: types.StringType, + Optional: true, + }, + }, + Blocks: map[string]tfsdk.Block{ + "sub_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_block_attr": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_attr", + Optional: true, + Type: tftypes.String, + }, + }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_block_attr", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "sub_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test", + }, + }, + "nestingmode-single-blocks": { + name: "test", + block: tfsdk.Block{ + Blocks: map[string]tfsdk.Block{ + "sub_block": { + Attributes: map[string]tfsdk.Attribute{ + "sub_block_attr": { + Type: types.StringType, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_block_attr", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "sub_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test", + }, + }, "deprecationmessage": { name: "test", block: tfsdk.Block{ diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index 7e2bc9a08..8b3cc9fed 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -977,6 +977,49 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "data-source-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "data-source-version": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1818,6 +1861,46 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "provider-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "provider-version": { input: &fwserver.GetProviderSchemaResponse{ Provider: &tfsdk.Schema{ @@ -2656,6 +2739,46 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "provider-meta-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + ProviderMeta: &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + ProviderMeta: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "provider-meta-version": { input: &fwserver.GetProviderSchemaResponse{ ProviderMeta: &tfsdk.Schema{ @@ -3618,6 +3741,49 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": &tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test_block": { + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_resource": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + }, + }, "resource-version": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto6/schema_test.go b/internal/toproto6/schema_test.go index 39d57d98e..703a7c6bc 100644 --- a/internal/toproto6/schema_test.go +++ b/internal/toproto6/schema_test.go @@ -407,6 +407,28 @@ func TestSchema(t *testing.T) { }, NestingMode: tfsdk.BlockNestingModeSet, }, + "single": { + Attributes: map[string]tfsdk.Attribute{ + "string": { + Type: types.StringType, + Required: true, + }, + "number": { + Type: types.NumberType, + Optional: true, + }, + "bool": { + Type: types.BoolType, + Computed: true, + }, + "list": { + Type: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, }, }, expected: &tfprotov6.Schema{ @@ -471,6 +493,35 @@ func TestSchema(t *testing.T) { Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, TypeName: "set", }, + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "bool", + Type: tftypes.Bool, + }, + { + Computed: true, + Name: "list", + Optional: true, + Type: tftypes.List{ElementType: tftypes.String}, + }, + { + Name: "number", + Optional: true, + Type: tftypes.Number, + }, + { + Name: "string", + Required: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "single", + }, }, }, }, diff --git a/tfsdk/block.go b/tfsdk/block.go index 2ae2d0b8e..187a01052 100644 --- a/tfsdk/block.go +++ b/tfsdk/block.go @@ -115,6 +115,14 @@ func (b Block) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) } return fwschema.NestedBlock{Block: b}, nil + case BlockNestingModeSingle: + _, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("can't apply %T to block NestingModeSingle", step) + } + + return fwschema.NestedBlock{Block: b}.ApplyTerraform5AttributePathStep(step) default: return nil, fmt.Errorf("unsupported block nesting mode: %v", b.NestingMode) } @@ -223,6 +231,8 @@ func (b Block) Type() attr.Type { return types.SetType{ ElemType: attrType, } + case BlockNestingModeSingle: + return attrType default: panic(fmt.Sprintf("unsupported block nesting mode: %v", b.NestingMode)) } diff --git a/tfsdk/block_nested_mode.go b/tfsdk/block_nested_mode.go index 24cd5eea8..ddbd681c2 100644 --- a/tfsdk/block_nested_mode.go +++ b/tfsdk/block_nested_mode.go @@ -5,6 +5,7 @@ import ( ) const ( - BlockNestingModeList = fwschema.BlockNestingModeList - BlockNestingModeSet = fwschema.BlockNestingModeSet + BlockNestingModeList = fwschema.BlockNestingModeList + BlockNestingModeSet = fwschema.BlockNestingModeSet + BlockNestingModeSingle = fwschema.BlockNestingModeSingle ) diff --git a/tfsdk/block_test.go b/tfsdk/block_test.go index 9a7840eff..4da4814ce 100644 --- a/tfsdk/block_test.go +++ b/tfsdk/block_test.go @@ -87,6 +87,38 @@ func TestBlockType(t *testing.T) { }, }, }, + "NestingMode-Single": { + block: Block{ + Attributes: map[string]Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + Blocks: map[string]Block{ + "test_block": { + Attributes: map[string]Attribute{ + "test_block_attribute": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attribute": types.StringType, + "test_block": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_block_attribute": types.StringType, + }, + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/tfsdk/schema_test.go b/tfsdk/schema_test.go index 6c0e243b2..5af63077a 100644 --- a/tfsdk/schema_test.go +++ b/tfsdk/schema_test.go @@ -1163,6 +1163,183 @@ func TestSchemaAttributeAtPath(t *testing.T) { ), }, }, + "WithAttributeName-SingleBlock-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-SingleBlock-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: path.Root("test").AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test").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: test[0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: can't apply tftypes.ElementKeyInt to block NestingModeSingle", + ), + }, + }, + "WithAttributeName-SingleBlock-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: path.Root("test").AtMapKey("sub_test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtMapKey("sub_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[\"sub_test\"]\n"+ + "Original Error: ElementKeyString(\"sub_test\") still remains in the path: can't apply tftypes.ElementKeyString to block NestingModeSingle", + ), + }, + }, + "WithAttributeName-SingleBlock-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: path.Root("test").AtSetValue(types.String{Value: "sub_test"}), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtSetValue(types.String{Value: "sub_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[Value(\"sub_test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to block NestingModeSingle", + ), + }, + }, "WithAttributeName-Object-WithAttributeName": { schema: Schema{ Attributes: map[string]Attribute{ @@ -2384,6 +2561,156 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { expected: Attribute{}, expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to Attributes", }, + "WithAttributeName-SingleBlock-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-SingleBlock-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: can't apply tftypes.ElementKeyInt to block NestingModeSingle", + }, + "WithAttributeName-SingleBlock-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"sub_test\") still remains in the path: can't apply tftypes.ElementKeyString to block NestingModeSingle", + }, + "WithAttributeName-SingleBlock-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other_attr": { + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]Block{ + "other_block": { + Attributes: map[string]Attribute{ + "sub_test": { + Type: types.BoolType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + "test": { + Attributes: map[string]Attribute{ + "other_nested_attr": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to block NestingModeSingle", + }, "WithAttributeName-Object-WithAttributeName": { schema: Schema{ Attributes: map[string]Attribute{ @@ -2671,6 +2998,28 @@ func TestSchemaType(t *testing.T) { }, NestingMode: BlockNestingModeSet, }, + "single_nested_block": { + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Required: true, + }, + "number": { + Type: types.NumberType, + Optional: true, + }, + "bool": { + Type: types.BoolType, + Computed: true, + }, + "list": { + Type: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, + NestingMode: BlockNestingModeSingle, + }, }, } @@ -2714,6 +3063,14 @@ func TestSchemaType(t *testing.T) { }, }, }, + "single_nested_block": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string": types.StringType, + "number": types.NumberType, + "bool": types.BoolType, + "list": types.ListType{ElemType: types.StringType}, + }, + }, }, } @@ -2796,6 +3153,15 @@ func TestSchemaTypeAtPath(t *testing.T) { }, NestingMode: BlockNestingModeSet, }, + "single_block": { + Attributes: map[string]Attribute{ + "single_block_nested": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeSingle, + }, }, }, path: path.Root("list_block"), @@ -2978,6 +3344,15 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { }, NestingMode: BlockNestingModeSet, }, + "single_block": { + Attributes: map[string]Attribute{ + "single_block_nested": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeSingle, + }, }, }, path: tftypes.NewAttributePath().WithAttributeName("list_block"), diff --git a/website/docs/plugin/framework/paths.mdx b/website/docs/plugin/framework/paths.mdx index daa07b638..76a292f60 100644 --- a/website/docs/plugin/framework/paths.mdx +++ b/website/docs/plugin/framework/paths.mdx @@ -358,6 +358,7 @@ The following table shows the different [`path.Path` type](https://pkg.go.dev/gi | ------------------------------ | ----------------- | | `tfsdk.BlockNestingModeList` | `AtListIndex()` | | `tfsdk.BlockNestingModeSet` | `AtSetValue()` | +| `tfsdk.BlockNestingModeSingle` | `AtName()` | Blocks can implement nested blocks. Paths can continue to be built using the associated method with each level of the block type. @@ -481,3 +482,60 @@ path.Root("root_set_block").AtSetValue(types.Object{ } }).AtName("block_string_attribute") ``` + +#### Building Single Block Paths + +A block defined with `tfsdk.BlockNestingModeSingle` conceptually is an object with attribute or block names. + +Given following schema example: + +```go +tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "root_single_block": { + Attributes: map[string]tfsdk.Attribute{ + "block_string_attribute": { + Required: true, + Type: types.StringType, + }, + }, + Blocks: map[string]tfsdk.Block{ + "nested_single_block": { + Attributes: map[string]tfsdk.Attribute{ + "nested_block_string_attribute": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, + NestingMode: tfsdk.BlockNestingModeSingle, + }, + }, +} +``` + +The path which matches the object associated with the `root_single_block` block is: + +```go +path.Root("root_single_block") +``` + +The path which matches the `block_string_attribute` string value in the object associated with `root_single_block` block is: + +```go +path.Root("root_single_block").AtName("block_string_attribute") +``` + +The path which matches the `nested_single_block` object in the object associated with `root_single_block` block is: + +```go +path.Root("root_single_block").AtName("nested_single_block") +``` + +The path which matches the `nested_block_string_attribute` string value in the object associated with the `nested_single_block` in the object associated with `root_single_block` block is: + +```go +path.Root("root_single_block").AtName("nested_single_block").AtName("nested_block_string_attribute") +```