From 00e22519324f6b1f98ce9c4545092b9a788590e3 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 28 Nov 2022 22:21:31 -0500 Subject: [PATCH 1/4] resource/schema/planmodifier: New type-specific plan modifiers package Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/132 As part of upcoming effort to split schema functionality into the `datasource`, `provider`, and `resource` packages, there are some improvements that will land in the new implementations rather than breaking the existing `tfsdk` package schema functionality. One area which has caused developer burden is that "attribute" plan modifiers, currently implementations of the `tfsdk.AttributePlanModifier` interface, receive generic `attr.Value` as the configuration, plan, and state values to perform modification logic. This means that implementors must currently handle validating and converting the value into the concrete type they expect. The upcoming split schemas handling will introduce separate attribute/block types that will enable to framework to strongly type validators and other future schema enhancements. This change prepares the exported interfaces and internal validation logic for those enhancements. Plan modifiers are only available for resources, so this package is explicitly placed under that structure to further reduce usage confusion. --- .changelog/pending.txt | 3 + .../fwxschema/attribute_plan_modification.go | 82 + .../fwxschema/block_plan_modification.go | 28 + ...sted_attribute_object_plan_modification.go | 15 + .../nested_block_object_plan_modification.go | 15 + .../fwserver/attribute_plan_modification.go | 1629 +++- .../attribute_plan_modification_test.go | 7165 +++++++++++++++++ internal/fwserver/block_plan_modification.go | 896 ++- .../fwserver/block_plan_modification_test.go | 3611 +++++++++ internal/privatestate/data.go | 20 + internal/privatestate/data_test.go | 89 + internal/testing/testplanmodifier/bool.go | 44 + internal/testing/testplanmodifier/doc.go | 3 + internal/testing/testplanmodifier/float64.go | 44 + internal/testing/testplanmodifier/int64.go | 44 + internal/testing/testplanmodifier/list.go | 44 + internal/testing/testplanmodifier/map.go | 44 + internal/testing/testplanmodifier/number.go | 44 + internal/testing/testplanmodifier/object.go | 44 + internal/testing/testplanmodifier/set.go | 44 + internal/testing/testplanmodifier/string.go | 44 + .../attributewithboolplanmodifiers.go | 84 + .../attributewithfloat64planmodifiers.go | 84 + .../attributewithint64planmodifiers.go | 84 + .../attributewithlistplanmodifiers.go | 87 + .../attributewithmapplanmodifiers.go | 87 + .../attributewithnumberplanmodifiers.go | 84 + .../attributewithobjectplanmodifiers.go | 87 + .../attributewithsetplanmodifiers.go | 87 + .../attributewithstringplanmodifiers.go | 84 + .../testschema/blockwithlistplanmodifiers.go | 89 + .../blockwithobjectplanmodifiers.go | 87 + .../testschema/blockwithsetplanmodifiers.go | 89 + ...ted_attribute_object_with_planmodifiers.go | 87 + ...nested_block_object_with_plan_modifiers.go | 53 + resource/schema/planmodifier/bool.go | 85 + resource/schema/planmodifier/describer.go | 29 + resource/schema/planmodifier/doc.go | 29 + resource/schema/planmodifier/float64.go | 85 + resource/schema/planmodifier/int64.go | 85 + resource/schema/planmodifier/list.go | 85 + resource/schema/planmodifier/map.go | 85 + resource/schema/planmodifier/number.go | 85 + resource/schema/planmodifier/object.go | 85 + resource/schema/planmodifier/set.go | 85 + resource/schema/planmodifier/string.go | 85 + 46 files changed, 15432 insertions(+), 516 deletions(-) create mode 100644 .changelog/pending.txt create mode 100644 internal/fwschema/fwxschema/nested_attribute_object_plan_modification.go create mode 100644 internal/fwschema/fwxschema/nested_block_object_plan_modification.go create mode 100644 internal/testing/testplanmodifier/bool.go create mode 100644 internal/testing/testplanmodifier/doc.go create mode 100644 internal/testing/testplanmodifier/float64.go create mode 100644 internal/testing/testplanmodifier/int64.go create mode 100644 internal/testing/testplanmodifier/list.go create mode 100644 internal/testing/testplanmodifier/map.go create mode 100644 internal/testing/testplanmodifier/number.go create mode 100644 internal/testing/testplanmodifier/object.go create mode 100644 internal/testing/testplanmodifier/set.go create mode 100644 internal/testing/testplanmodifier/string.go create mode 100644 internal/testing/testschema/attributewithboolplanmodifiers.go create mode 100644 internal/testing/testschema/attributewithfloat64planmodifiers.go create mode 100644 internal/testing/testschema/attributewithint64planmodifiers.go create mode 100644 internal/testing/testschema/attributewithlistplanmodifiers.go create mode 100644 internal/testing/testschema/attributewithmapplanmodifiers.go create mode 100644 internal/testing/testschema/attributewithnumberplanmodifiers.go create mode 100644 internal/testing/testschema/attributewithobjectplanmodifiers.go create mode 100644 internal/testing/testschema/attributewithsetplanmodifiers.go create mode 100644 internal/testing/testschema/attributewithstringplanmodifiers.go create mode 100644 internal/testing/testschema/blockwithlistplanmodifiers.go create mode 100644 internal/testing/testschema/blockwithobjectplanmodifiers.go create mode 100644 internal/testing/testschema/blockwithsetplanmodifiers.go create mode 100644 internal/testing/testschema/nested_attribute_object_with_planmodifiers.go create mode 100644 internal/testing/testschema/nested_block_object_with_plan_modifiers.go create mode 100644 resource/schema/planmodifier/bool.go create mode 100644 resource/schema/planmodifier/describer.go create mode 100644 resource/schema/planmodifier/doc.go create mode 100644 resource/schema/planmodifier/float64.go create mode 100644 resource/schema/planmodifier/int64.go create mode 100644 resource/schema/planmodifier/list.go create mode 100644 resource/schema/planmodifier/map.go create mode 100644 resource/schema/planmodifier/number.go create mode 100644 resource/schema/planmodifier/object.go create mode 100644 resource/schema/planmodifier/set.go create mode 100644 resource/schema/planmodifier/string.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..2a47657bb --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:feature +resource/schema/planmodifier: New package which contains type-specific schema plan modifier interfaces +``` diff --git a/internal/fwschema/fwxschema/attribute_plan_modification.go b/internal/fwschema/fwxschema/attribute_plan_modification.go index 50a6ccbdb..d0c62958c 100644 --- a/internal/fwschema/fwxschema/attribute_plan_modification.go +++ b/internal/fwschema/fwxschema/attribute_plan_modification.go @@ -2,6 +2,7 @@ package fwxschema import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -17,3 +18,84 @@ type AttributeWithPlanModifiers interface { // the tfsdk.Attribute field name. GetPlanModifiers() tfsdk.AttributePlanModifiers } + +// AttributeWithBoolPlanModifiers is an optional interface on Attribute which +// enables Bool plan modifier support. +type AttributeWithBoolPlanModifiers interface { + fwschema.Attribute + + // BoolPlanModifiers should return a list of Bool plan modifiers. + BoolPlanModifiers() []planmodifier.Bool +} + +// AttributeWithFloat64PlanModifiers is an optional interface on Attribute which +// enables Float64 plan modifier support. +type AttributeWithFloat64PlanModifiers interface { + fwschema.Attribute + + // Float64PlanModifiers should return a list of Float64 plan modifiers. + Float64PlanModifiers() []planmodifier.Float64 +} + +// AttributeWithInt64PlanModifiers is an optional interface on Attribute which +// enables Int64 plan modifier support. +type AttributeWithInt64PlanModifiers interface { + fwschema.Attribute + + // Int64PlanModifiers should return a list of Int64 plan modifiers. + Int64PlanModifiers() []planmodifier.Int64 +} + +// AttributeWithListPlanModifiers is an optional interface on Attribute which +// enables List plan modifier support. +type AttributeWithListPlanModifiers interface { + fwschema.Attribute + + // ListPlanModifiers should return a list of List plan modifiers. + ListPlanModifiers() []planmodifier.List +} + +// AttributeWithMapPlanModifiers is an optional interface on Attribute which +// enables Map plan modifier support. +type AttributeWithMapPlanModifiers interface { + fwschema.Attribute + + // MapPlanModifiers should return a list of Map plan modifiers. + MapPlanModifiers() []planmodifier.Map +} + +// AttributeWithNumberPlanModifiers is an optional interface on Attribute which +// enables Number plan modifier support. +type AttributeWithNumberPlanModifiers interface { + fwschema.Attribute + + // NumberPlanModifiers should return a list of Number plan modifiers. + NumberPlanModifiers() []planmodifier.Number +} + +// AttributeWithObjectPlanModifiers is an optional interface on Attribute which +// enables Object plan modifier support. +type AttributeWithObjectPlanModifiers interface { + fwschema.Attribute + + // ObjectPlanModifiers should return a list of Object plan modifiers. + ObjectPlanModifiers() []planmodifier.Object +} + +// AttributeWithSetPlanModifiers is an optional interface on Attribute which +// enables Set plan modifier support. +type AttributeWithSetPlanModifiers interface { + fwschema.Attribute + + // SetPlanModifiers should return a list of Set plan modifiers. + SetPlanModifiers() []planmodifier.Set +} + +// AttributeWithStringPlanModifiers is an optional interface on Attribute which +// enables String plan modifier support. +type AttributeWithStringPlanModifiers interface { + fwschema.Attribute + + // StringPlanModifiers should return a list of String plan modifiers. + StringPlanModifiers() []planmodifier.String +} diff --git a/internal/fwschema/fwxschema/block_plan_modification.go b/internal/fwschema/fwxschema/block_plan_modification.go index 330f026e0..d14f7829b 100644 --- a/internal/fwschema/fwxschema/block_plan_modification.go +++ b/internal/fwschema/fwxschema/block_plan_modification.go @@ -2,6 +2,7 @@ package fwxschema import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -17,3 +18,30 @@ type BlockWithPlanModifiers interface { // the tfsdk.Block field name. GetPlanModifiers() tfsdk.AttributePlanModifiers } + +// BlockWithListPlanModifiers is an optional interface on Block which +// enables List plan modifier support. +type BlockWithListPlanModifiers interface { + fwschema.Block + + // ListPlanModifiers should return a list of List plan modifiers. + ListPlanModifiers() []planmodifier.List +} + +// BlockWithObjectPlanModifiers is an optional interface on Block which +// enables Object plan modifier support. +type BlockWithObjectPlanModifiers interface { + fwschema.Block + + // ObjectPlanModifiers should return a list of Object plan modifiers. + ObjectPlanModifiers() []planmodifier.Object +} + +// BlockWithSetPlanModifiers is an optional interface on Block which +// enables Set plan modifier support. +type BlockWithSetPlanModifiers interface { + fwschema.Block + + // SetPlanModifiers should return a list of Set plan modifiers. + SetPlanModifiers() []planmodifier.Set +} diff --git a/internal/fwschema/fwxschema/nested_attribute_object_plan_modification.go b/internal/fwschema/fwxschema/nested_attribute_object_plan_modification.go new file mode 100644 index 000000000..e40597469 --- /dev/null +++ b/internal/fwschema/fwxschema/nested_attribute_object_plan_modification.go @@ -0,0 +1,15 @@ +package fwxschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// NestedAttributeObjectWithPlanModifiers is an optional interface on +// NestedAttributeObject which enables Object plan modification support. +type NestedAttributeObjectWithPlanModifiers interface { + fwschema.NestedAttributeObject + + // ObjectPlanModifiers should return a list of Object plan modifiers. + ObjectPlanModifiers() []planmodifier.Object +} diff --git a/internal/fwschema/fwxschema/nested_block_object_plan_modification.go b/internal/fwschema/fwxschema/nested_block_object_plan_modification.go new file mode 100644 index 000000000..2cf9782c6 --- /dev/null +++ b/internal/fwschema/fwxschema/nested_block_object_plan_modification.go @@ -0,0 +1,15 @@ +package fwxschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// NestedBlockObjectWithPlanModifiers is an optional interface on +// NestedBlockObject which enables Object plan modification support. +type NestedBlockObjectWithPlanModifiers interface { + fwschema.NestedBlockObject + + // ObjectPlanModifiers should return a list of Object plan modifiers. + ObjectPlanModifiers() []planmodifier.Object +} diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index aa21558b4..21a70a76d 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -32,8 +33,6 @@ type ModifyAttributePlanResponse struct { func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { ctx = logging.FrameworkWithAttributePath(ctx, req.AttributePath.String()) - var requiresReplace bool - privateProviderData := privatestate.EmptyProviderData(ctx) if req.Private != nil { @@ -41,7 +40,11 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo privateProviderData = req.Private } - if attributeWithPlanModifiers, ok := a.(fwxschema.AttributeWithPlanModifiers); ok { + switch attributeWithPlanModifiers := a.(type) { + // Legacy tfsdk.AttributePlanModifier handling + case fwxschema.AttributeWithPlanModifiers: + var requiresReplace bool + for _, planModifier := range attributeWithPlanModifiers.GetPlanModifiers() { modifyResp := &tfsdk.ModifyAttributePlanResponse{ AttributePlan: req.AttributePlan, @@ -76,10 +79,28 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } } - } - if requiresReplace { - resp.RequiresReplace = append(resp.RequiresReplace, req.AttributePath) + if requiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, req.AttributePath) + } + case fwxschema.AttributeWithBoolPlanModifiers: + AttributePlanModifyBool(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithFloat64PlanModifiers: + AttributePlanModifyFloat64(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithInt64PlanModifiers: + AttributePlanModifyInt64(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithListPlanModifiers: + AttributePlanModifyList(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithMapPlanModifiers: + AttributePlanModifyMap(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithNumberPlanModifiers: + AttributePlanModifyNumber(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithObjectPlanModifiers: + AttributePlanModifyObject(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithSetPlanModifiers: + AttributePlanModifySet(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithStringPlanModifiers: + AttributePlanModifyString(ctx, attributeWithPlanModifiers, req, resp) } if resp.Diagnostics.HasError() { @@ -87,7 +108,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo } // Null and unknown values should not have nested schema to modify. - if req.AttributePlan.IsNull() || req.AttributePlan.IsUnknown() { + if resp.AttributePlan.IsNull() || resp.AttributePlan.IsUnknown() { return } @@ -160,65 +181,28 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - planAttributes := planObject.Attributes() - - for name, attr := range nestedAttributeObject.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: attrPath.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) - - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: attrPath, + PathExpression: attrPath.Expression(), + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, + } + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, } - planElements[idx], diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) - - resp.Diagnostics.Append(diags...) + NestedAttributeObjectPlanModify(ctx, nestedAttributeObject, objectReq, objectResp) - if resp.Diagnostics.HasError() { - return - } + planElements[idx] = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) } resp.AttributePlan, diags = types.ListValue(planList.ElementType(ctx), planElements) @@ -282,65 +266,28 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - planAttributes := planObject.Attributes() - - for name, attr := range nestedAttributeObject.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: attrPath.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) - - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: attrPath, + PathExpression: attrPath.Expression(), + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, + } + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, } - planElements[idx], diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) - - resp.Diagnostics.Append(diags...) + NestedAttributeObjectPlanModify(ctx, nestedAttributeObject, objectReq, objectResp) - if resp.Diagnostics.HasError() { - return - } + planElements[idx] = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) } resp.AttributePlan, diags = types.SetValue(planSet.ElementType(ctx), planElements) @@ -404,65 +351,28 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - planAttributes := planObject.Attributes() - - for name, attr := range nestedAttributeObject.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: attrPath.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) - - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: attrPath, + PathExpression: attrPath.Expression(), + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, + } + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, } - planElements[key], diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) - - resp.Diagnostics.Append(diags...) + NestedAttributeObjectPlanModify(ctx, nestedAttributeObject, objectReq, objectResp) - if resp.Diagnostics.HasError() { - return - } + planElements[key] = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) } resp.AttributePlan, diags = types.MapValue(planMap.ElementType(ctx), planElements) @@ -497,79 +407,1356 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - if len(planObject.Attributes()) == 0 { + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, + } + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, + } + + NestedAttributeObjectPlanModify(ctx, nestedAttributeObject, objectReq, objectResp) + + resp.AttributePlan = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) + default: + err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Plan Modification Error", + "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } +} + +// AttributePlanModifyBool performs all types.Bool plan modification. +func AttributePlanModifyBool(ctx context.Context, attribute fwxschema.AttributeWithBoolPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.BoolValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.BoolValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Bool Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Bool attribute plan modification. "+ + "The value type must implement the types.BoolValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToBoolValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.BoolValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Bool Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Bool attribute plan modification. "+ + "The value type must implement the types.BoolValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToBoolValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.BoolValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Bool Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Bool attribute plan modification. "+ + "The value type must implement the types.BoolValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToBoolValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.BoolRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.BoolPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.BoolResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Bool", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyBool(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Bool", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +// AttributePlanModifyFloat64 performs all types.Float64 plan modification. +func AttributePlanModifyFloat64(ctx context.Context, attribute fwxschema.AttributeWithFloat64PlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.Float64Valuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.Float64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float64 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Float64 attribute plan modification. "+ + "The value type must implement the types.Float64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToFloat64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.Float64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float64 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Float64 attribute plan modification. "+ + "The value type must implement the types.Float64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToFloat64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.Float64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float64 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Float64 attribute plan modification. "+ + "The value type must implement the types.Float64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToFloat64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.Float64Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.Float64PlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.Float64Response{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Float64", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyFloat64(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Float64", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { return } + } +} + +// AttributePlanModifyInt64 performs all types.Int64 plan modification. +func AttributePlanModifyInt64(ctx context.Context, attribute fwxschema.AttributeWithInt64PlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.Int64Valuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.Int64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int64 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Int64 attribute plan modification. "+ + "The value type must implement the types.Int64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) - planAttributes := planObject.Attributes() + return + } - for name, attr := range nestedAttributeObject.GetAttributes() { - attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + configValue, diags := configValuable.ToInt64Value(ctx) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } - attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + planValuable, ok := req.AttributePlan.(types.Int64Valuable) - resp.Diagnostics.Append(diags...) + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int64 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Int64 attribute plan modification. "+ + "The value type must implement the types.Int64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) - if resp.Diagnostics.HasError() { - return - } + return + } - attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + planValue, diags := planValuable.ToInt64Value(ctx) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.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, - } + stateValuable, ok := req.AttributeState.(types.Int64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int64 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Int64 attribute plan modification. "+ + "The value type must implement the types.Int64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToInt64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } - AttributeModifyPlan(ctx, attr, attrReq, &attrResp) + planModifyReq := planmodifier.Int64Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private + for _, planModifier := range attribute.Int64PlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.Int64Response{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, } - resp.AttributePlan, diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Int64", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyInt64(ctx, planModifyReq, planModifyResp) - resp.Diagnostics.Append(diags...) + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Int64", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) - if resp.Diagnostics.HasError() { + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { return } - default: - err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + } +} + +// AttributePlanModifyList performs all types.List plan modification. +func AttributePlanModifyList(ctx context.Context, attribute fwxschema.AttributeWithListPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.ListValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ListValuable) + + if !ok { resp.Diagnostics.AddAttributeError( req.AttributePath, - "Attribute Plan Modification Error", - "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + "Invalid List Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform List attribute plan modification. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), ) return } + + configValue, diags := configValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.ListValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform List attribute plan modification. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.ListValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform List attribute plan modification. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.ListRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.ListPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.ListResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.List", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyList(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.List", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +// AttributePlanModifyMap performs all types.Map plan modification. +func AttributePlanModifyMap(ctx context.Context, attribute fwxschema.AttributeWithMapPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.MapValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.MapValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Map Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Map attribute plan modification. "+ + "The value type must implement the types.MapValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToMapValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.MapValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Map Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Map attribute plan modification. "+ + "The value type must implement the types.MapValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToMapValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.MapValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Map Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Map attribute plan modification. "+ + "The value type must implement the types.MapValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToMapValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.MapRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.MapPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.MapResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Map", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyMap(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Map", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +// AttributePlanModifyNumber performs all types.Number plan modification. +func AttributePlanModifyNumber(ctx context.Context, attribute fwxschema.AttributeWithNumberPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.NumberValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.NumberValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Number Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Number attribute plan modification. "+ + "The value type must implement the types.NumberValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToNumberValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.NumberValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Number Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Number attribute plan modification. "+ + "The value type must implement the types.NumberValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToNumberValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.NumberValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Number Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Number attribute plan modification. "+ + "The value type must implement the types.NumberValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToNumberValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.NumberRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.NumberPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.NumberResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Number", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyNumber(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Number", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +// AttributePlanModifyObject performs all types.Object plan modification. +func AttributePlanModifyObject(ctx context.Context, attribute fwxschema.AttributeWithObjectPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.ObjectValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Object attribute plan modification. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Object attribute plan modification. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Object attribute plan modification. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.ObjectPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.ObjectResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyObject(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +// AttributePlanModifySet performs all types.Set plan modification. +func AttributePlanModifySet(ctx context.Context, attribute fwxschema.AttributeWithSetPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.SetValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.SetValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Set attribute plan modification. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.SetValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Set attribute plan modification. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.SetValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Set attribute plan modification. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.SetRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.SetPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.SetResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Set", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifySet(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Set", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +// AttributePlanModifyString performs all types.String plan modification. +func AttributePlanModifyString(ctx context.Context, attribute fwxschema.AttributeWithStringPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.StringValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.StringValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid String Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform String attribute plan modification. "+ + "The value type must implement the types.StringValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToStringValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.StringValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid String Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform String attribute plan modification. "+ + "The value type must implement the types.StringValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToStringValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.StringValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid String Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform String attribute plan modification. "+ + "The value type must implement the types.StringValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToStringValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.StringRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.StringPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.StringResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.String", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyString(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.String", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +func NestedAttributeObjectPlanModify(ctx context.Context, o fwschema.NestedAttributeObject, req planmodifier.ObjectRequest, resp *ModifyAttributePlanResponse) { + if objectWithPlanModifiers, ok := o.(fwxschema.NestedAttributeObjectWithPlanModifiers); ok { + for _, objectValidator := range objectWithPlanModifiers.ObjectPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.ObjectResponse{ + PlanValue: req.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + objectValidator.PlanModifyObject(ctx, req, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + req.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.Path) + } + + // only on new errors + if planModifyResp.Diagnostics.HasError() { + return + } + } + } + + newPlanValueAttributes := req.PlanValue.Attributes() + + for nestedName, nestedAttr := range o.GetAttributes() { + nestedAttrConfig, diags := objectAttributeValue(ctx, req.ConfigValue, nestedName, fwschemadata.DataDescriptionConfiguration) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return + } + + nestedAttrPlan, diags := objectAttributeValue(ctx, req.PlanValue, nestedName, fwschemadata.DataDescriptionPlan) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return + } + + nestedAttrState, diags := objectAttributeValue(ctx, req.StateValue, nestedName, fwschemadata.DataDescriptionState) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return + } + + nestedAttrReq := tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: nestedAttrConfig, + AttributePath: req.Path.AtName(nestedName), + AttributePathExpression: req.PathExpression.AtName(nestedName), + AttributePlan: nestedAttrPlan, + AttributeState: nestedAttrState, + Config: req.Config, + Plan: req.Plan, + Private: resp.Private, + State: req.State, + } + nestedAttrResp := &ModifyAttributePlanResponse{ + AttributePlan: nestedAttrReq.AttributePlan, + RequiresReplace: resp.RequiresReplace, + Private: nestedAttrReq.Private, + } + + AttributeModifyPlan(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + + newPlanValueAttributes[nestedName] = nestedAttrResp.AttributePlan + resp.Diagnostics.Append(nestedAttrResp.Diagnostics...) + resp.Private = nestedAttrResp.Private + resp.RequiresReplace.Append(nestedAttrResp.RequiresReplace...) + } + + newPlanValue, diags := types.ObjectValue(req.PlanValue.AttributeTypes(ctx), newPlanValueAttributes) + + resp.Diagnostics.Append(diags...) + + resp.AttributePlan = newPlanValue } func attributePlanModificationValueError(ctx context.Context, value attr.Value, description fwschemadata.DataDescription, err error) diag.Diagnostic { diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index 8ca0a0fae..1c62b8a05 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -2,6 +2,8 @@ package fwserver import ( "context" + "fmt" + "math/big" "testing" "github.com/google/go-cmp/cmp" @@ -11,11 +13,15 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/planmodifiers" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testplanmodifier" + "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/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -1161,3 +1167,7162 @@ func TestAttributeModifyPlan(t *testing.T) { }) } } + +func TestAttributePlanModifyBool(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithBoolPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(true), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(true), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "request-config": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(true), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.ConfigValue + expected := types.BoolValue(true) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolNull(), + AttributeState: types.BoolNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolNull(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolNull(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(true), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.PlanValue + expected := types.BoolValue(true) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolNull(), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "request-private": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolNull(), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolNull(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(true), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + got := req.StateValue + expected := types.BoolValue(true) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolNull(), + AttributePlan: types.BoolNull(), + AttributeState: types.BoolValue(true), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolNull(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolNull(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(true), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.PlanValue = types.BoolValue(true) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolNull(), + AttributePlan: types.BoolUnknown(), + AttributeState: types.BoolNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolUnknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + }, + "response-private": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolNull(), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolNull(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(false), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(false), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + AttributePlan: types.BoolValue(true), + AttributeState: types.BoolValue(false), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.BoolValue(true), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyBool(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyFloat64(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithFloat64PlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "request-config": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(1.2), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.ConfigValue + expected := types.Float64Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Null(), + AttributeState: types.Float64Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Null(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(1.2), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.PlanValue + expected := types.Float64Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Null(), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "request-private": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected Float64Request.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Null(), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(1.2), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + got := req.StateValue + expected := types.Float64Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Null(), + AttributePlan: types.Float64Null(), + AttributeState: types.Float64Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Null(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.PlanValue = types.Float64Value(1.2) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Null(), + AttributePlan: types.Float64Unknown(), + AttributeState: types.Float64Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Unknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + }, + "response-private": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Null(), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(2.4), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(2.4), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + AttributePlan: types.Float64Value(1.2), + AttributeState: types.Float64Value(2.4), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float64Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyFloat64(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyInt64(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithInt64PlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "request-config": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(1), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.ConfigValue + expected := types.Int64Value(1) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Null(), + AttributeState: types.Int64Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Null(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(1), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.PlanValue + expected := types.Int64Value(1) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Null(), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "request-private": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected Int64Request.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Null(), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(1), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + got := req.StateValue + expected := types.Int64Value(1) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Null(), + AttributePlan: types.Int64Null(), + AttributeState: types.Int64Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Null(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.PlanValue = types.Int64Value(1) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Null(), + AttributePlan: types.Int64Unknown(), + AttributeState: types.Int64Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Unknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + }, + "response-private": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Null(), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(1), + AttributePlan: types.Int64Value(1), + AttributeState: types.Int64Value(2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int64Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyInt64(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyList(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithListPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-config": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.ConfigValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListNull(types.StringType), + AttributeState: types.ListNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.PlanValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-private": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.StateValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListNull(types.StringType), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.PlanValue = types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListUnknown(types.StringType), + AttributeState: types.ListNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListUnknown(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "response-private": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyList(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyMap(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithMapPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-config": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.ConfigValue + expected := types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapNull(types.StringType), + AttributeState: types.MapNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapNull(types.StringType), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.PlanValue + expected := types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapNull(types.StringType), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-private": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected MapRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapNull(types.StringType), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + got := req.StateValue + expected := types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapNull(types.StringType), + AttributePlan: types.MapNull(types.StringType), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapNull(types.StringType), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.PlanValue = types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapNull(types.StringType), + AttributePlan: types.MapUnknown(types.StringType), + AttributeState: types.MapNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapUnknown(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + "response-private": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapNull(types.StringType), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + AttributeState: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyMap(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyNumber(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithNumberPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(1)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(1)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "request-config": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(1)), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.ConfigValue + expected := types.NumberValue(big.NewFloat(1)) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberNull(), + AttributeState: types.NumberNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberNull(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberNull(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(1)), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.PlanValue + expected := types.NumberValue(big.NewFloat(1)) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberNull(), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "request-private": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberNull(), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberNull(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(1)), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + got := req.StateValue + expected := types.NumberValue(big.NewFloat(1)) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberNull(), + AttributePlan: types.NumberNull(), + AttributeState: types.NumberValue(big.NewFloat(1)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberNull(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberNull(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(1)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.PlanValue = types.NumberValue(big.NewFloat(1)) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberNull(), + AttributePlan: types.NumberUnknown(), + AttributeState: types.NumberNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberUnknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + }, + "response-private": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberNull(), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberNull(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(2)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(2)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1)), + AttributePlan: types.NumberValue(big.NewFloat(1)), + AttributeState: types.NumberValue(big.NewFloat(2)), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.NumberValue(big.NewFloat(1)), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyNumber(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithObjectPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-config": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.ConfigValue + expected := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PlanValue + expected := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-private": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.StateValue + expected := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "response-private": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyObject(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifySet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithSetPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-config": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.ConfigValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetNull(types.StringType), + AttributeState: types.SetNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.PlanValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-private": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.StateValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetNull(types.StringType), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.PlanValue = types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetUnknown(types.StringType), + AttributeState: types.SetNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetUnknown(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "response-private": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifySet(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributePlanModifyString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithStringPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("testvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("testvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "request-config": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("testvalue"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.ConfigValue + expected := types.StringValue("testvalue") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringNull(), + AttributeState: types.StringNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringNull(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringNull(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("testvalue"), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.PlanValue + expected := types.StringValue("testvalue") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringNull(), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "request-private": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected StringRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringNull(), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringNull(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("testvalue"), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + got := req.StateValue + expected := types.StringValue("testvalue") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringNull(), + AttributePlan: types.StringNull(), + AttributeState: types.StringValue("testvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringNull(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringNull(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("testvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.PlanValue = types.StringValue("testvalue") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringNull(), + AttributePlan: types.StringUnknown(), + AttributeState: types.StringNull(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringUnknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + }, + "response-private": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringNull(), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringNull(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("oldtestvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("oldtestvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testvalue"), + AttributePlan: types.StringValue("testvalue"), + AttributeState: types.StringValue("oldtestvalue"), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.StringValue("testvalue"), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyString(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectPlanModify(t *testing.T) { + t.Parallel() + + fwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.AttributeWithObjectPlanModifiers{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Required: true, + }, + }, + } + fwValue := types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ) + tfValue := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ) + testConfig := tfsdk.Config{ + Raw: tfValue, + Schema: fwSchema, + } + testPlan := tfsdk.Plan{ + Raw: tfValue, + Schema: fwSchema, + } + testState := tfsdk.State{ + Raw: tfValue, + Schema: fwSchema, + } + + testCases := map[string]struct { + object fwschema.NestedAttributeObject + request planmodifier.ObjectRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-pathexpression": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-config": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Config + expected := testConfig + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-configvalue": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.ConfigValue + expected := fwValue + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-plan": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Plan + expected := testPlan + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-planvalue": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PlanValue + expected := fwValue + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-private": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.State + expected := testState + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-statevalue": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.StateValue + expected := fwValue + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "response-diagnostics": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + AttributePlan: fwValue, + }, + }, + "response-diagnostics-nested": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("test").AtName("testattr"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtName("testattr"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("newtestvalue"), + }, + ) + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("newtestvalue"), + }, + ), + }, + }, + "response-planvalue-nested": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.PlanValue = types.StringValue("newtestvalue") + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("newtestvalue"), + }, + ), + }, + }, + "response-private": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-private-nested": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // should not be removed + }, + }, + }, + "response-requiresreplace-nested": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test").AtName("testattr"), + }, + }, + }, + "response-requiresreplace-update": { + object: testschema.NestedAttributeObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + NestedAttributeObjectPlanModify(context.Background(), testCase.object, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index a781b1927..d6219f24d 100644 --- a/internal/fwserver/block_plan_modification.go +++ b/internal/fwserver/block_plan_modification.go @@ -4,11 +4,12 @@ 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" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -20,8 +21,6 @@ import ( // package from the tfsdk package and not wanting to export the method. // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/365 func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { - var requiresReplace bool - privateProviderData := privatestate.EmptyProviderData(ctx) if req.Private != nil { @@ -29,7 +28,11 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr privateProviderData = req.Private } - if blockWithPlanModifiers, ok := b.(fwxschema.BlockWithPlanModifiers); ok { + switch blockWithPlanModifiers := b.(type) { + // Legacy tfsdk.BlockPlanModifier handling + case fwxschema.BlockWithPlanModifiers: + var requiresReplace bool + for _, planModifier := range blockWithPlanModifiers.GetPlanModifiers() { modifyResp := &tfsdk.ModifyAttributePlanResponse{ AttributePlan: req.AttributePlan, @@ -50,14 +53,24 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } } + + if requiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, req.AttributePath) + } + case fwxschema.BlockWithListPlanModifiers: + BlockPlanModifyList(ctx, blockWithPlanModifiers, req, resp) + case fwxschema.BlockWithObjectPlanModifiers: + BlockPlanModifyObject(ctx, blockWithPlanModifiers, req, resp) + case fwxschema.BlockWithSetPlanModifiers: + BlockPlanModifySet(ctx, blockWithPlanModifiers, req, resp) } - if requiresReplace { - resp.RequiresReplace = append(resp.RequiresReplace, req.AttributePath) + if resp.Diagnostics.HasError() { + return } // Null and unknown values should not have nested schema to modify. - if req.AttributePlan.IsNull() || req.AttributePlan.IsUnknown() { + if resp.AttributePlan.IsNull() || resp.AttributePlan.IsUnknown() { return } @@ -119,115 +132,28 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } - planAttributes := planObject.Attributes() - - for name, attr := range nestedBlockObject.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: attrPath.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) - - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: attrPath, + PathExpression: attrPath.Expression(), + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, } - - for name, block := range nestedBlockObject.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.AtListIndex(idx).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) - - planAttributes[name] = blockResp.AttributePlan - resp.Diagnostics.Append(blockResp.Diagnostics...) - resp.RequiresReplace = blockResp.RequiresReplace - resp.Private = blockResp.Private + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, } - planElements[idx], diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) - - resp.Diagnostics.Append(diags...) + NestedBlockObjectPlanModify(ctx, nestedBlockObject, objectReq, objectResp) - if resp.Diagnostics.HasError() { - return - } + planElements[idx] = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) } resp.AttributePlan, diags = types.ListValue(planList.ElementType(ctx), planElements) @@ -291,270 +217,650 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } - planAttributes := planObject.Attributes() + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: attrPath, + PathExpression: attrPath.Expression(), + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, + } + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, + } - for name, attr := range nestedBlockObject.GetAttributes() { - attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + NestedBlockObjectPlanModify(ctx, nestedBlockObject, objectReq, objectResp) - resp.Diagnostics.Append(diags...) + planElements[idx] = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) + } - if resp.Diagnostics.HasError() { - return - } + resp.AttributePlan, diags = types.SetValue(planSet.ElementType(ctx), planElements) - attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + resp.Diagnostics.Append(diags...) - resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + case fwschema.BlockNestingModeSingle: + configObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributeConfig) - if resp.Diagnostics.HasError() { - return - } + resp.Diagnostics.Append(diags...) - attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + if resp.Diagnostics.HasError() { + return + } - resp.Diagnostics.Append(diags...) + planObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributePlan) - if resp.Diagnostics.HasError() { - return - } + resp.Diagnostics.Append(diags...) - attrReq := tfsdk.ModifyAttributePlanRequest{ - AttributeConfig: attrConfig, - AttributePath: attrPath.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, - } + if resp.Diagnostics.HasError() { + return + } - AttributeModifyPlan(ctx, attr, attrReq, &attrResp) + stateObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributeState) - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private - } + resp.Diagnostics.Append(diags...) - for name, block := range nestedBlockObject.GetBlocks() { - attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + if resp.Diagnostics.HasError() { + return + } - resp.Diagnostics.Append(diags...) + objectReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configObject, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planObject, + Private: resp.Private, + State: req.State, + StateValue: stateObject, + } + objectResp := &ModifyAttributePlanResponse{ + AttributePlan: objectReq.PlanValue, + Private: objectReq.Private, + } - if resp.Diagnostics.HasError() { - return - } + NestedBlockObjectPlanModify(ctx, nestedBlockObject, objectReq, objectResp) - attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + resp.AttributePlan = objectResp.AttributePlan + resp.Diagnostics.Append(objectResp.Diagnostics...) + resp.Private = objectResp.Private + resp.RequiresReplace.Append(objectResp.RequiresReplace...) + default: + err := fmt.Errorf("unknown block plan modification nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Block Plan Modification Error", + "Block plan modification cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) - resp.Diagnostics.Append(diags...) + return + } +} - if resp.Diagnostics.HasError() { - return - } +// BlockPlanModifyList performs all types.List plan modification. +func BlockPlanModifyList(ctx context.Context, block fwxschema.BlockWithListPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.ListValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ListValuable) - attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform List block plan modification. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) - resp.Diagnostics.Append(diags...) + return + } - if resp.Diagnostics.HasError() { - return - } + configValue, diags := configValuable.ToListValue(ctx) - blockReq := tfsdk.ModifyAttributePlanRequest{ - AttributeConfig: attrConfig, - AttributePath: attrPath.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, - } + resp.Diagnostics.Append(diags...) - BlockModifyPlan(ctx, block, blockReq, &blockResp) + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } - planAttributes[name] = blockResp.AttributePlan - resp.Diagnostics.Append(blockResp.Diagnostics...) - resp.RequiresReplace = blockResp.RequiresReplace - resp.Private = blockResp.Private - } + planValuable, ok := req.AttributePlan.(types.ListValuable) - planElements[idx], diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform List block plan modification. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) - resp.Diagnostics.Append(diags...) + return + } - if resp.Diagnostics.HasError() { - return - } - } + planValue, diags := planValuable.ToListValue(ctx) - resp.AttributePlan, diags = types.SetValue(planSet.ElementType(ctx), planElements) + resp.Diagnostics.Append(diags...) - resp.Diagnostics.Append(diags...) + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } - if resp.Diagnostics.HasError() { - return + stateValuable, ok := req.AttributeState.(types.ListValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform List block plan modification. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.ListRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range block.ListPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.ListResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, } - case fwschema.BlockNestingModeSingle: - configObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributeConfig) - resp.Diagnostics.Append(diags...) + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.List", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) - if resp.Diagnostics.HasError() { + planModifier.PlanModifyList(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.List", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { return } + } +} - planObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributePlan) +// BlockPlanModifyObject performs all types.Object plan modification. +func BlockPlanModifyObject(ctx context.Context, block fwxschema.BlockWithObjectPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.ObjectValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ObjectValuable) - resp.Diagnostics.Append(diags...) + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Object block plan modification. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) - if resp.Diagnostics.HasError() { - return + return + } + + configValue, diags := configValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Object block plan modification. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Object block plan modification. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.ObjectRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range block.ObjectPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.ObjectResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, } - stateObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributeState) + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) - resp.Diagnostics.Append(diags...) + planModifier.PlanModifyObject(ctx, planModifyReq, planModifyResp) - if resp.Diagnostics.HasError() { + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { return } + } +} - planAttributes := planObject.Attributes() +// BlockPlanModifySet performs all types.Set plan modification. +func BlockPlanModifySet(ctx context.Context, block fwxschema.BlockWithSetPlanModifiers, req tfsdk.ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use types.SetValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.SetValuable) - if planAttributes == nil { - planAttributes = make(map[string]attr.Value) - } + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Set block plan modification. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) - for name, attr := range nestedBlockObject.GetAttributes() { - attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + return + } - resp.Diagnostics.Append(diags...) + configValue, diags := configValuable.ToSetValue(ctx) - if resp.Diagnostics.HasError() { - return - } + resp.Diagnostics.Append(diags...) - attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } - resp.Diagnostics.Append(diags...) + planValuable, ok := req.AttributePlan.(types.SetValuable) - if resp.Diagnostics.HasError() { - return - } + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Set block plan modification. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) - attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + return + } - resp.Diagnostics.Append(diags...) + planValue, diags := planValuable.ToSetValue(ctx) - if resp.Diagnostics.HasError() { - return + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(types.SetValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Block Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Set block plan modification. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.SetRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range block.SetPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.SetResponse{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Set", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifySet(ctx, planModifyReq, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Set", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifyReq.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + } +} + +func NestedBlockObjectPlanModify(ctx context.Context, o fwschema.NestedBlockObject, req planmodifier.ObjectRequest, resp *ModifyAttributePlanResponse) { + if objectWithPlanModifiers, ok := o.(fwxschema.NestedBlockObjectWithPlanModifiers); ok { + for _, objectValidator := range objectWithPlanModifiers.ObjectPlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.ObjectResponse{ + PlanValue: req.PlanValue, + Private: resp.Private, } - 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, + logging.FrameworkDebug( + ctx, + "Calling provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + objectValidator.PlanModifyObject(ctx, req, planModifyResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined planmodifier.Object", + map[string]interface{}{ + logging.KeyDescription: objectValidator.Description(ctx), + }, + ) + + req.PlanValue = planModifyResp.PlanValue + resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.Path) } - attrResp := ModifyAttributePlanResponse{ - AttributePlan: attrReq.AttributePlan, - RequiresReplace: resp.RequiresReplace, - Private: attrReq.Private, + + // only on new errors + if planModifyResp.Diagnostics.HasError() { + return } + } + } - AttributeModifyPlan(ctx, attr, attrReq, &attrResp) + newPlanValueAttributes := req.PlanValue.Attributes() - planAttributes[name] = attrResp.AttributePlan - resp.Diagnostics.Append(attrResp.Diagnostics...) - resp.RequiresReplace = attrResp.RequiresReplace - resp.Private = attrResp.Private + for nestedName, nestedAttr := range o.GetAttributes() { + nestedAttrConfig, diags := objectAttributeValue(ctx, req.ConfigValue, nestedName, fwschemadata.DataDescriptionConfiguration) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return } - for name, block := range nestedBlockObject.GetBlocks() { - attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) + nestedAttrPlan, diags := objectAttributeValue(ctx, req.PlanValue, nestedName, fwschemadata.DataDescriptionPlan) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + if diags.HasError() { + return + } - attrPlan, diags := objectAttributeValue(ctx, planObject, name, fwschemadata.DataDescriptionPlan) + nestedAttrState, diags := objectAttributeValue(ctx, req.StateValue, nestedName, fwschemadata.DataDescriptionState) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + if diags.HasError() { + return + } - attrState, diags := objectAttributeValue(ctx, stateObject, name, fwschemadata.DataDescriptionState) + nestedAttrReq := tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: nestedAttrConfig, + AttributePath: req.Path.AtName(nestedName), + AttributePathExpression: req.PathExpression.AtName(nestedName), + AttributePlan: nestedAttrPlan, + AttributeState: nestedAttrState, + Config: req.Config, + Plan: req.Plan, + Private: resp.Private, + State: req.State, + } + nestedAttrResp := &ModifyAttributePlanResponse{ + AttributePlan: nestedAttrReq.AttributePlan, + RequiresReplace: resp.RequiresReplace, + Private: nestedAttrReq.Private, + } - resp.Diagnostics.Append(diags...) + AttributeModifyPlan(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) - if resp.Diagnostics.HasError() { - return - } + newPlanValueAttributes[nestedName] = nestedAttrResp.AttributePlan + resp.Diagnostics.Append(nestedAttrResp.Diagnostics...) + resp.Private = nestedAttrResp.Private + resp.RequiresReplace.Append(nestedAttrResp.RequiresReplace...) + } - 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, - } + for nestedName, nestedBlock := range o.GetBlocks() { + nestedBlockConfig, diags := objectAttributeValue(ctx, req.ConfigValue, nestedName, fwschemadata.DataDescriptionConfiguration) - BlockModifyPlan(ctx, block, blockReq, &blockResp) + resp.Diagnostics.Append(diags...) - planAttributes[name] = blockResp.AttributePlan - resp.Diagnostics.Append(blockResp.Diagnostics...) - resp.RequiresReplace = blockResp.RequiresReplace - resp.Private = blockResp.Private + if diags.HasError() { + return } - resp.AttributePlan, diags = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) + nestedBlockPlan, diags := objectAttributeValue(ctx, req.PlanValue, nestedName, fwschemadata.DataDescriptionPlan) resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { + if diags.HasError() { return } - default: - err := fmt.Errorf("unknown block plan modification nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) - resp.Diagnostics.AddAttributeError( - req.AttributePath, - "Block Plan Modification Error", - "Block plan modification cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), - ) - return + nestedBlockState, diags := objectAttributeValue(ctx, req.StateValue, nestedName, fwschemadata.DataDescriptionState) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return + } + + nestedBlockReq := tfsdk.ModifyAttributePlanRequest{ + AttributeConfig: nestedBlockConfig, + AttributePath: req.Path.AtName(nestedName), + AttributePathExpression: req.PathExpression.AtName(nestedName), + AttributePlan: nestedBlockPlan, + AttributeState: nestedBlockState, + Config: req.Config, + Plan: req.Plan, + Private: resp.Private, + State: req.State, + } + nestedBlockResp := &ModifyAttributePlanResponse{ + AttributePlan: nestedBlockReq.AttributePlan, + RequiresReplace: resp.RequiresReplace, + Private: nestedBlockReq.Private, + } + + BlockModifyPlan(ctx, nestedBlock, nestedBlockReq, nestedBlockResp) + + newPlanValueAttributes[nestedName] = nestedBlockResp.AttributePlan + resp.Diagnostics.Append(nestedBlockResp.Diagnostics...) + resp.Private = nestedBlockResp.Private + resp.RequiresReplace.Append(nestedBlockResp.RequiresReplace...) } + + newPlanValue, diags := types.ObjectValue(req.PlanValue.AttributeTypes(ctx), newPlanValueAttributes) + + resp.Diagnostics.Append(diags...) + + resp.AttributePlan = newPlanValue } diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index 20a67d6e4..388d6bcff 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -2,6 +2,7 @@ package fwserver import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -10,10 +11,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/planmodifiers" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -2004,12 +2009,3618 @@ func TestBlockModifyPlan(t *testing.T) { BlockModifyPlan(context.Background(), tc.block, tc.req, &got) if diff := cmp.Diff(tc.expectedResp, got, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { + for _, d := range got.Diagnostics { + t.Logf("%s: %s\n%s\n", d.Severity(), d.Summary(), d.Detail()) + } t.Errorf("Unexpected response (+wanted, -got): %s", diff) } }) } } +func TestBlockPlanModifyList(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block fwxschema.BlockWithListPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-pathexpression": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-config": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-configvalue": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.ConfigValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListNull(types.StringType), + AttributeState: types.ListNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + }, + "request-plan": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-planvalue": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.PlanValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-private": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-statevalue": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + got := req.StateValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListNull(types.StringType), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListNull(types.StringType), + }, + }, + "response-diagnostics": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.PlanValue = types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListUnknown(types.StringType), + AttributeState: types.ListNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListUnknown(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "response-private": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListNull(types.StringType), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + BlockPlanModifyList(context.Background(), testCase.block, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBlockPlanModifyObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block fwxschema.BlockWithObjectPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-pathexpression": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-config": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-configvalue": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.ConfigValue + expected := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + "request-plan": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-planvalue": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PlanValue + expected := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-private": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "request-statevalue": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.StateValue + expected := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + "response-diagnostics": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + "response-private": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + AttributeState: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("oldtestvalue"), + }, + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + BlockPlanModifyObject(context.Background(), testCase.block, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBlockPlanModifySet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block fwxschema.BlockWithSetPlanModifiers + request tfsdk.ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-pathexpression": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-config": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-configvalue": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.ConfigValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetNull(types.StringType), + AttributeState: types.SetNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + }, + "request-plan": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-planvalue": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.PlanValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-private": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{tftypes.NewValue(tftypes.String, "testvalue")}, + ), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "request-statevalue": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + got := req.StateValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetNull(types.StringType), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetNull(types.StringType), + }, + }, + "response-diagnostics": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.PlanValue = types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetUnknown(types.StringType), + AttributeState: types.SetNull(types.StringType), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetUnknown(types.StringType), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + "response-private": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetNull(types.StringType), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetNull(types.StringType), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + AttributeState: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("oldtestvalue")}), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + BlockPlanModifySet(context.Background(), testCase.block, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectPlanModify(t *testing.T) { + t.Parallel() + + fwSchema := testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.BlockWithObjectPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{}, + }, + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testblockattr": testschema.AttributeWithStringPlanModifiers{}, + }, + }, + }, + }, + }, + } + fwValue := types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ) + tfValue := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + "testblock": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testblockattr": tftypes.String, + }, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + "testblock": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testblockattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + "testblock": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testblockattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testblockattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ) + testConfig := tfsdk.Config{ + Raw: tfValue, + Schema: fwSchema, + } + testPlan := tfsdk.Plan{ + Raw: tfValue, + Schema: fwSchema, + } + testState := tfsdk.State{ + Raw: tfValue, + Schema: fwSchema, + } + + testCases := map[string]struct { + object fwschema.NestedBlockObject + request planmodifier.ObjectRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-pathexpression": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-config": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Config + expected := testConfig + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-configvalue": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.ConfigValue + expected := fwValue + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-plan": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.Plan + expected := testPlan + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-planvalue": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.PlanValue + expected := fwValue + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-private": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Private", + diff, + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.State + expected := testState + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "request-statevalue": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + got := req.StateValue + expected := fwValue + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + }, + "response-diagnostics": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + AttributePlan: fwValue, + }, + }, + "response-diagnostics-nested-attributes": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("test").AtName("testattr"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtName("testattr"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-diagnostics-nested-blocks": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("test").AtName("testblock"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test").AtName("testblock"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("newtestvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("newtestvalue"), + }, + ), + }, + ) + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("newtestvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("newtestvalue"), + }, + ), + }, + ), + }, + }, + "response-planvalue-nested-attributes": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.PlanValue = types.StringValue("newtestvalue") + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("newtestvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + }, + }, + "response-planvalue-nested-blocks": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("newtestvalue"), + }, + ) + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("testvalue"), + }, + ), + }, + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testblockattr": types.StringType, + }, + }, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + "testblock": types.ObjectValueMust( + map[string]attr.Type{ + "testblockattr": types.StringType, + }, + map[string]attr.Value{ + "testblockattr": types.StringValue("newtestvalue"), + }, + ), + }, + ), + }, + }, + "response-private": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-private-nested-attributes": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-private-nested-blocks": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // should not be removed + }, + }, + }, + "response-requiresreplace-nested-attributes": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringPlanModifiers{ + Required: true, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test").AtName("testattr"), + }, + }, + }, + "response-requiresreplace-nested-blocks": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + Blocks: map[string]fwschema.Block{ + "testblock": testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test").AtName("testblock"), + }, + }, + }, + "response-requiresreplace-update": { + object: testschema.NestedBlockObjectWithPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: planmodifier.ObjectRequest{ + Config: testConfig, + ConfigValue: fwValue, + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + Plan: testPlan, + PlanValue: fwValue, + State: testState, + StateValue: fwValue, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: fwValue, + RequiresReplace: path.Paths{ + path.Root("test"), // remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + NestedBlockObjectPlanModify(context.Background(), testCase.object, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + type testBlockPlanModifierNullList struct{} func (t testBlockPlanModifierNullList) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { diff --git a/internal/privatestate/data.go b/internal/privatestate/data.go index ecf610fdf..6fb38a608 100644 --- a/internal/privatestate/data.go +++ b/internal/privatestate/data.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "reflect" "strings" "unicode/utf8" @@ -241,6 +242,25 @@ type ProviderData struct { data map[string][]byte } +// Equal returns true if the given ProviderData is exactly equivalent. The +// internal data is compared byte-for-byte, not accounting for semantic +// equivalency such as JSON whitespace or property reordering. +func (d *ProviderData) Equal(o *ProviderData) bool { + if d == nil && o == nil { + return true + } + + if d == nil || o == nil { + return false + } + + if !reflect.DeepEqual(d.data, o.data) { + return false + } + + return true +} + // GetKey returns the private state data associated with the given key. // // If the key is reserved for framework usage, an error diagnostic diff --git a/internal/privatestate/data_test.go b/internal/privatestate/data_test.go index c8402a8be..07d66bce0 100644 --- a/internal/privatestate/data_test.go +++ b/internal/privatestate/data_test.go @@ -549,6 +549,95 @@ func TestNewProviderData(t *testing.T) { } } +func TestProviderDataEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + providerData *ProviderData + other *ProviderData + expected bool + }{ + "nil-nil": { + providerData: nil, + other: nil, + expected: true, + }, + "nil-empty": { + providerData: nil, + other: EmptyProviderData(context.Background()), + expected: false, + }, + "empty-nil": { + providerData: EmptyProviderData(context.Background()), + other: nil, + expected: false, + }, + "empty-data": { + providerData: EmptyProviderData(context.Background()), + other: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test": []byte(`{}`)}), + ), + expected: false, + }, + "data-empty": { + providerData: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test": []byte(`{}`)}), + ), + other: EmptyProviderData(context.Background()), + expected: false, + }, + "data-data-different-keys": { + providerData: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test1": []byte(`{}`)}), + ), + other: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test2": []byte(`{}`)}), + ), + expected: false, + }, + "data-data-different-values": { + providerData: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test": []byte(`{"subtest":true}`)}), + ), + other: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test": []byte(`{"subtest":false}`)}), + ), + expected: false, + }, + "data-data-equal": { + providerData: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test": []byte(`{}`)}), + ), + other: MustProviderData( + context.Background(), + MustMarshalToJson(map[string][]byte{"test": []byte(`{}`)}), + ), + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.providerData.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestProviderData_GetKey(t *testing.T) { testCases := map[string]struct { providerData *ProviderData diff --git a/internal/testing/testplanmodifier/bool.go b/internal/testing/testplanmodifier/bool.go new file mode 100644 index 000000000..e884ba49f --- /dev/null +++ b/internal/testing/testplanmodifier/bool.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Bool = &Bool{} + +// Declarative planmodifier.Bool for unit testing. +type Bool struct { + // Bool interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyBoolMethod func(context.Context, planmodifier.BoolRequest, *planmodifier.BoolResponse) +} + +// Description satisfies the planmodifier.Bool interface. +func (v Bool) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Bool interface. +func (v Bool) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Bool interface. +func (v Bool) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + if v.PlanModifyBoolMethod == nil { + return + } + + v.PlanModifyBoolMethod(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/doc.go b/internal/testing/testplanmodifier/doc.go new file mode 100644 index 000000000..01f4c8b0e --- /dev/null +++ b/internal/testing/testplanmodifier/doc.go @@ -0,0 +1,3 @@ +// Package testplanmodifier contains declarative resource/schema/planmodifier +// implementations for unit testing. +package testplanmodifier diff --git a/internal/testing/testplanmodifier/float64.go b/internal/testing/testplanmodifier/float64.go new file mode 100644 index 000000000..b80d13ba5 --- /dev/null +++ b/internal/testing/testplanmodifier/float64.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Float64 = &Float64{} + +// Declarative planmodifier.Float64 for unit testing. +type Float64 struct { + // Float64 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyFloat64Method func(context.Context, planmodifier.Float64Request, *planmodifier.Float64Response) +} + +// Description satisfies the planmodifier.Float64 interface. +func (v Float64) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Float64 interface. +func (v Float64) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Float64 interface. +func (v Float64) PlanModifyFloat64(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + if v.PlanModifyFloat64Method == nil { + return + } + + v.PlanModifyFloat64Method(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/int64.go b/internal/testing/testplanmodifier/int64.go new file mode 100644 index 000000000..0fc02e1b9 --- /dev/null +++ b/internal/testing/testplanmodifier/int64.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Int64 = &Int64{} + +// Declarative planmodifier.Int64 for unit testing. +type Int64 struct { + // Int64 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyInt64Method func(context.Context, planmodifier.Int64Request, *planmodifier.Int64Response) +} + +// Description satisfies the planmodifier.Int64 interface. +func (v Int64) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Int64 interface. +func (v Int64) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Int64 interface. +func (v Int64) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + if v.PlanModifyInt64Method == nil { + return + } + + v.PlanModifyInt64Method(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/list.go b/internal/testing/testplanmodifier/list.go new file mode 100644 index 000000000..a37d81a04 --- /dev/null +++ b/internal/testing/testplanmodifier/list.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.List = &List{} + +// Declarative planmodifier.List for unit testing. +type List struct { + // List interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyListMethod func(context.Context, planmodifier.ListRequest, *planmodifier.ListResponse) +} + +// Description satisfies the planmodifier.List interface. +func (v List) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.List interface. +func (v List) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.List interface. +func (v List) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + if v.PlanModifyListMethod == nil { + return + } + + v.PlanModifyListMethod(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/map.go b/internal/testing/testplanmodifier/map.go new file mode 100644 index 000000000..b1690cddd --- /dev/null +++ b/internal/testing/testplanmodifier/map.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Map = &Map{} + +// Declarative planmodifier.Map for unit testing. +type Map struct { + // Map interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyMapMethod func(context.Context, planmodifier.MapRequest, *planmodifier.MapResponse) +} + +// Description satisfies the planmodifier.Map interface. +func (v Map) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Map interface. +func (v Map) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Map interface. +func (v Map) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + if v.PlanModifyMapMethod == nil { + return + } + + v.PlanModifyMapMethod(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/number.go b/internal/testing/testplanmodifier/number.go new file mode 100644 index 000000000..edbcb3f4e --- /dev/null +++ b/internal/testing/testplanmodifier/number.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Number = &Number{} + +// Declarative planmodifier.Number for unit testing. +type Number struct { + // Number interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyNumberMethod func(context.Context, planmodifier.NumberRequest, *planmodifier.NumberResponse) +} + +// Description satisfies the planmodifier.Number interface. +func (v Number) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Number interface. +func (v Number) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Number interface. +func (v Number) PlanModifyNumber(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + if v.PlanModifyNumberMethod == nil { + return + } + + v.PlanModifyNumberMethod(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/object.go b/internal/testing/testplanmodifier/object.go new file mode 100644 index 000000000..db8a2d1f0 --- /dev/null +++ b/internal/testing/testplanmodifier/object.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Object = &Object{} + +// Declarative planmodifier.Object for unit testing. +type Object struct { + // Object interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyObjectMethod func(context.Context, planmodifier.ObjectRequest, *planmodifier.ObjectResponse) +} + +// Description satisfies the planmodifier.Object interface. +func (v Object) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Object interface. +func (v Object) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Object interface. +func (v Object) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + if v.PlanModifyObjectMethod == nil { + return + } + + v.PlanModifyObjectMethod(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/set.go b/internal/testing/testplanmodifier/set.go new file mode 100644 index 000000000..3be7625f6 --- /dev/null +++ b/internal/testing/testplanmodifier/set.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Set = &Set{} + +// Declarative planmodifier.Set for unit testing. +type Set struct { + // Set interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifySetMethod func(context.Context, planmodifier.SetRequest, *planmodifier.SetResponse) +} + +// Description satisfies the planmodifier.Set interface. +func (v Set) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Set interface. +func (v Set) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Set interface. +func (v Set) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + if v.PlanModifySetMethod == nil { + return + } + + v.PlanModifySetMethod(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/string.go b/internal/testing/testplanmodifier/string.go new file mode 100644 index 000000000..6b8cd64dd --- /dev/null +++ b/internal/testing/testplanmodifier/string.go @@ -0,0 +1,44 @@ +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.String = &String{} + +// Declarative planmodifier.String for unit testing. +type String struct { + // String interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyStringMethod func(context.Context, planmodifier.StringRequest, *planmodifier.StringResponse) +} + +// Description satisfies the planmodifier.String interface. +func (v String) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.String interface. +func (v String) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.String interface. +func (v String) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if v.PlanModifyStringMethod == nil { + return + } + + v.PlanModifyStringMethod(ctx, req, resp) +} diff --git a/internal/testing/testschema/attributewithboolplanmodifiers.go b/internal/testing/testschema/attributewithboolplanmodifiers.go new file mode 100644 index 000000000..ac21e84e5 --- /dev/null +++ b/internal/testing/testschema/attributewithboolplanmodifiers.go @@ -0,0 +1,84 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithBoolPlanModifiers = AttributeWithBoolPlanModifiers{} + +type AttributeWithBoolPlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Bool +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// BoolPlanModifiers satisfies the fwxschema.AttributeWithBoolPlanModifiers interface. +func (a AttributeWithBoolPlanModifiers) BoolPlanModifiers() []planmodifier.Bool { + return a.PlanModifiers +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithBoolPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) GetType() attr.Type { + return types.BoolType +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithfloat64planmodifiers.go b/internal/testing/testschema/attributewithfloat64planmodifiers.go new file mode 100644 index 000000000..c7194e8e5 --- /dev/null +++ b/internal/testing/testschema/attributewithfloat64planmodifiers.go @@ -0,0 +1,84 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithFloat64PlanModifiers = AttributeWithFloat64PlanModifiers{} + +type AttributeWithFloat64PlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Float64 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithFloat64PlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float64PlanModifiers satisfies the fwxschema.AttributeWithFloat64PlanModifiers interface. +func (a AttributeWithFloat64PlanModifiers) Float64PlanModifiers() []planmodifier.Float64 { + return a.PlanModifiers +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) GetType() attr.Type { + return types.Float64Type +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithint64planmodifiers.go b/internal/testing/testschema/attributewithint64planmodifiers.go new file mode 100644 index 000000000..48870a69b --- /dev/null +++ b/internal/testing/testschema/attributewithint64planmodifiers.go @@ -0,0 +1,84 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithInt64PlanModifiers = AttributeWithInt64PlanModifiers{} + +type AttributeWithInt64PlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Int64 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithInt64PlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) GetType() attr.Type { + return types.Int64Type +} + +// Int64PlanModifiers satisfies the fwxschema.AttributeWithInt64PlanModifiers interface. +func (a AttributeWithInt64PlanModifiers) Int64PlanModifiers() []planmodifier.Int64 { + return a.PlanModifiers +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithlistplanmodifiers.go b/internal/testing/testschema/attributewithlistplanmodifiers.go new file mode 100644 index 000000000..526ce4448 --- /dev/null +++ b/internal/testing/testschema/attributewithlistplanmodifiers.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithListPlanModifiers = AttributeWithListPlanModifiers{} + +type AttributeWithListPlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + ElementType attr.Type + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.List +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithListPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) GetType() attr.Type { + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsSensitive() bool { + return a.Sensitive +} + +// ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. +func (a AttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { + return a.PlanModifiers +} diff --git a/internal/testing/testschema/attributewithmapplanmodifiers.go b/internal/testing/testschema/attributewithmapplanmodifiers.go new file mode 100644 index 000000000..a57aff9e6 --- /dev/null +++ b/internal/testing/testschema/attributewithmapplanmodifiers.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithMapPlanModifiers = AttributeWithMapPlanModifiers{} + +type AttributeWithMapPlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + ElementType attr.Type + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Map +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithMapPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) GetType() attr.Type { + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsSensitive() bool { + return a.Sensitive +} + +// MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. +func (a AttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { + return a.PlanModifiers +} diff --git a/internal/testing/testschema/attributewithnumberplanmodifiers.go b/internal/testing/testschema/attributewithnumberplanmodifiers.go new file mode 100644 index 000000000..0829142f6 --- /dev/null +++ b/internal/testing/testschema/attributewithnumberplanmodifiers.go @@ -0,0 +1,84 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithNumberPlanModifiers = AttributeWithNumberPlanModifiers{} + +type AttributeWithNumberPlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Number +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithNumberPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) GetType() attr.Type { + return types.NumberType +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsSensitive() bool { + return a.Sensitive +} + +// NumberPlanModifiers satisfies the fwxschema.AttributeWithNumberPlanModifiers interface. +func (a AttributeWithNumberPlanModifiers) NumberPlanModifiers() []planmodifier.Number { + return a.PlanModifiers +} diff --git a/internal/testing/testschema/attributewithobjectplanmodifiers.go b/internal/testing/testschema/attributewithobjectplanmodifiers.go new file mode 100644 index 000000000..a28fea93a --- /dev/null +++ b/internal/testing/testschema/attributewithobjectplanmodifiers.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithObjectPlanModifiers = AttributeWithObjectPlanModifiers{} + +type AttributeWithObjectPlanModifiers struct { + AttributeTypes map[string]attr.Type + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Object +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithObjectPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) GetType() attr.Type { + return types.ObjectType{ + AttrTypes: a.AttributeTypes, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsSensitive() bool { + return a.Sensitive +} + +// ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. +func (a AttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { + return a.PlanModifiers +} diff --git a/internal/testing/testschema/attributewithsetplanmodifiers.go b/internal/testing/testschema/attributewithsetplanmodifiers.go new file mode 100644 index 000000000..598f5e884 --- /dev/null +++ b/internal/testing/testschema/attributewithsetplanmodifiers.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithSetPlanModifiers = AttributeWithSetPlanModifiers{} + +type AttributeWithSetPlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + ElementType attr.Type + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Set +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithSetPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) GetType() attr.Type { + return types.SetType{ + ElemType: a.ElementType, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsSensitive() bool { + return a.Sensitive +} + +// SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. +func (a AttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { + return a.PlanModifiers +} diff --git a/internal/testing/testschema/attributewithstringplanmodifiers.go b/internal/testing/testschema/attributewithstringplanmodifiers.go new file mode 100644 index 000000000..81aab0157 --- /dev/null +++ b/internal/testing/testschema/attributewithstringplanmodifiers.go @@ -0,0 +1,84 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.AttributeWithStringPlanModifiers = AttributeWithStringPlanModifiers{} + +type AttributeWithStringPlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.String +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithStringPlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) GetType() attr.Type { + return types.StringType +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsSensitive() bool { + return a.Sensitive +} + +// StringPlanModifiers satisfies the fwxschema.AttributeWithStringPlanModifiers interface. +func (a AttributeWithStringPlanModifiers) StringPlanModifiers() []planmodifier.String { + return a.PlanModifiers +} diff --git a/internal/testing/testschema/blockwithlistplanmodifiers.go b/internal/testing/testschema/blockwithlistplanmodifiers.go new file mode 100644 index 000000000..1a8fe08e6 --- /dev/null +++ b/internal/testing/testschema/blockwithlistplanmodifiers.go @@ -0,0 +1,89 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.BlockWithListPlanModifiers = BlockWithListPlanModifiers{} + +type BlockWithListPlanModifiers struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + MaxItems int64 + MinItems int64 + PlanModifiers []planmodifier.List +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return b.Type().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) Equal(o fwschema.Block) bool { + _, ok := o.(BlockWithListPlanModifiers) + + if !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetMaxItems() int64 { + return b.MaxItems +} + +// GetMinItems satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetMinItems() int64 { + return b.MinItems +} + +// GetNestedObject satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + } +} + +// GetNestingMode satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeList +} + +// ListPlanModifiers satisfies the fwxschema.BlockWithListPlanModifiers interface. +func (b BlockWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { + return b.PlanModifiers +} + +// Type satisfies the fwschema.Block interface. +func (b BlockWithListPlanModifiers) Type() attr.Type { + return types.ListType{ + ElemType: b.GetNestedObject().Type(), + } +} diff --git a/internal/testing/testschema/blockwithobjectplanmodifiers.go b/internal/testing/testschema/blockwithobjectplanmodifiers.go new file mode 100644 index 000000000..27f9a21e8 --- /dev/null +++ b/internal/testing/testschema/blockwithobjectplanmodifiers.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.BlockWithObjectPlanModifiers = BlockWithObjectPlanModifiers{} + +type BlockWithObjectPlanModifiers struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + MaxItems int64 + MinItems int64 + PlanModifiers []planmodifier.Object +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return b.Type().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) Equal(o fwschema.Block) bool { + _, ok := o.(BlockWithObjectPlanModifiers) + + if !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetMaxItems() int64 { + return b.MaxItems +} + +// GetMinItems satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetMinItems() int64 { + return b.MinItems +} + +// GetNestedObject satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObjectWithPlanModifiers{ + Attributes: b.Attributes, + Blocks: b.Blocks, + PlanModifiers: b.PlanModifiers, + } +} + +// GetNestingMode satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSingle +} + +// ObjectPlanModifiers satisfies the fwxschema.BlockWithObjectPlanModifiers interface. +func (b BlockWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { + return b.PlanModifiers +} + +// Type satisfies the fwschema.Block interface. +func (b BlockWithObjectPlanModifiers) Type() attr.Type { + return b.GetNestedObject().Type() +} diff --git a/internal/testing/testschema/blockwithsetplanmodifiers.go b/internal/testing/testschema/blockwithsetplanmodifiers.go new file mode 100644 index 000000000..b0f080dfa --- /dev/null +++ b/internal/testing/testschema/blockwithsetplanmodifiers.go @@ -0,0 +1,89 @@ +package testschema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ fwxschema.BlockWithSetPlanModifiers = BlockWithSetPlanModifiers{} + +type BlockWithSetPlanModifiers struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + MaxItems int64 + MinItems int64 + PlanModifiers []planmodifier.Set +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return b.Type().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) Equal(o fwschema.Block) bool { + _, ok := o.(BlockWithSetPlanModifiers) + + if !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetMaxItems() int64 { + return b.MaxItems +} + +// GetMinItems satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetMinItems() int64 { + return b.MinItems +} + +// GetNestedObject satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + } +} + +// GetNestingMode satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSet +} + +// SetPlanModifiers satisfies the fwxschema.BlockWithSetPlanModifiers interface. +func (b BlockWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { + return b.PlanModifiers +} + +// Type satisfies the fwschema.Block interface. +func (b BlockWithSetPlanModifiers) Type() attr.Type { + return types.SetType{ + ElemType: b.GetNestedObject().Type(), + } +} diff --git a/internal/testing/testschema/nested_attribute_object_with_planmodifiers.go b/internal/testing/testschema/nested_attribute_object_with_planmodifiers.go new file mode 100644 index 000000000..0cadd7660 --- /dev/null +++ b/internal/testing/testschema/nested_attribute_object_with_planmodifiers.go @@ -0,0 +1,87 @@ +package testschema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedAttributeObjectWithPlanModifiers = NestedAttributeObjectWithPlanModifiers{} + +type NestedAttributeObjectWithPlanModifiers struct { + Attributes map[string]fwschema.Attribute + PlanModifiers []planmodifier.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedAttributeObjectWithPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to NestedAttributeObjectWithPlanModifiers", step) + } + + attribute, ok := o.GetAttributes()[string(name)] + + if ok { + return attribute, nil + } + + return nil, fmt.Errorf("no attribute %q on NestedAttributeObjectWithPlanModifiers", name) + +} + +// Equal returns true if the given NestedAttributeObjectWithPlanModifiers is equivalent. +func (o NestedAttributeObjectWithPlanModifiers) Equal(other fwschema.NestedAttributeObject) bool { + if !o.Type().Equal(other.Type()) { + return false + } + + if len(o.GetAttributes()) != len(other.GetAttributes()) { + return false + } + + for name, oAttribute := range o.GetAttributes() { + otherAttribute, ok := other.GetAttributes()[name] + + if !ok { + return false + } + + if !oAttribute.Equal(otherAttribute) { + return false + } + } + + return true +} + +// GetAttributes returns the Attributes field value. +func (o NestedAttributeObjectWithPlanModifiers) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (o NestedAttributeObjectWithPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { + return o.PlanModifiers +} + +// Type returns the framework type of the NestedAttributeObjectWithPlanModifiers. +func (o NestedAttributeObjectWithPlanModifiers) Type() types.ObjectTypable { + attrTypes := make(map[string]attr.Type, len(o.Attributes)) + + for name, attribute := range o.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/internal/testing/testschema/nested_block_object_with_plan_modifiers.go b/internal/testing/testschema/nested_block_object_with_plan_modifiers.go new file mode 100644 index 000000000..ba3c23d85 --- /dev/null +++ b/internal/testing/testschema/nested_block_object_with_plan_modifiers.go @@ -0,0 +1,53 @@ +package testschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedBlockObjectWithPlanModifiers = NestedBlockObjectWithPlanModifiers{} + +type NestedBlockObjectWithPlanModifiers struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + PlanModifiers []planmodifier.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObjectWithPlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObjectWithPlanModifiers is equivalent. +func (o NestedBlockObjectWithPlanModifiers) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObjectWithPlanModifiers); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObjectWithPlanModifiers) GetAttributes() fwschema.UnderlyingAttributes { + return o.Attributes +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObjectWithPlanModifiers) GetBlocks() map[string]fwschema.Block { + return o.Blocks +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (o NestedBlockObjectWithPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { + return o.PlanModifiers +} + +// Type returns the framework type of the NestedBlockObjectWithPlanModifiers. +func (o NestedBlockObjectWithPlanModifiers) Type() types.ObjectTypable { + return fwschema.NestedBlockObjectType(o) +} diff --git a/resource/schema/planmodifier/bool.go b/resource/schema/planmodifier/bool.go new file mode 100644 index 000000000..8fe7d007e --- /dev/null +++ b/resource/schema/planmodifier/bool.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Bool is a schema validator for types.Bool attributes. +type Bool interface { + Describer + + // PlanModifyBool should perform the modification. + PlanModifyBool(context.Context, BoolRequest, *BoolResponse) +} + +// BoolRequest is a request for types.Bool schema plan modification. +type BoolRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Bool + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Bool + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Bool + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // BoolResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // BoolResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// BoolResponse is a response to a BoolRequest. +type BoolResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Bool + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyBool operation. + // This field is pre-populated from BoolRequest.Private and + // can be modified during the resource's PlanModifyBool operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/describer.go b/resource/schema/planmodifier/describer.go new file mode 100644 index 000000000..c29b6005e --- /dev/null +++ b/resource/schema/planmodifier/describer.go @@ -0,0 +1,29 @@ +package planmodifier + +import ( + "context" +) + +// Describer is the common documentation interface for extensible schema +// plan modifier functionality. +type Describer interface { + // Description should describe the plan modifier in plain text formatting. + // This information is used by provider logging and provider tooling such + // as documentation generation. + // + // The description should: + // - Begin with a lowercase or other character suitable for the middle of + // a sentence. + // - End without punctuation. + Description(context.Context) string + + // MarkdownDescription should describe the plan modifier in Markdown + // formatting. This information is used by provider logging and provider + // tooling such as documentation generation. + // + // The description should: + // - Begin with a lowercase or other character suitable for the middle of + // a sentence. + // - End without punctuation. + MarkdownDescription(context.Context) string +} diff --git a/resource/schema/planmodifier/doc.go b/resource/schema/planmodifier/doc.go new file mode 100644 index 000000000..6d3a66daf --- /dev/null +++ b/resource/schema/planmodifier/doc.go @@ -0,0 +1,29 @@ +// Package planmodifier contains schema plan modifier interfaces and +// implementations. These plan modifiers are used by resource/schema. +// +// Each attr.Type has a corresponding {TYPE}PlanModifer interface which +// implements concretely typed Modify{TYPE} methods, such as +// StringPlanModifer and ModifyString. +// +// The framework has to choose between plan modifier developers handling a +// concrete framework value type, such as types.Bool, or the framework +// interface for custom value types, such as types.BoolValuable. +// +// In the framework type model, the developer can immediately use the value. +// If the value was associated with a custom type and using the custom value +// type is desired, the developer must use the type's ValueFrom{TYPE} method. +// +// In the custom type model, the developer must always convert to a concreate +// type before using the value unless checking for null or unknown. Since any +// custom type may be passed due to the schema, it is possible, if not likely, +// that unknown concrete types will be passed to the plan modifier. +// +// The framework chooses to pass the framework value type. This prevents the +// potential for unexpected runtime panics and simplifies development for +// easier use cases where the framework type is sufficient. More advanced +// developers can choose to call the type's ValueFrom{TYPE} method to get the +// desired custom type in a plan modifier. +// +// PlanModifers that are not type dependent need to implement all interfaces, +// but can use shared logic to reduce implementation code. +package planmodifier diff --git a/resource/schema/planmodifier/float64.go b/resource/schema/planmodifier/float64.go new file mode 100644 index 000000000..cbdd2fb67 --- /dev/null +++ b/resource/schema/planmodifier/float64.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Float64 is a schema validator for types.Float64 attributes. +type Float64 interface { + Describer + + // PlanModifyFloat64 should perform the modification. + PlanModifyFloat64(context.Context, Float64Request, *Float64Response) +} + +// Float64Request is a request for types.Float64 schema plan modification. +type Float64Request struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Float64 + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Float64 + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Float64 + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // Float64Response.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // Float64Response.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// Float64Response is a response to a Float64Request. +type Float64Response struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Float64 + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyFloat64 operation. + // This field is pre-populated from Float64Request.Private and + // can be modified during the resource's PlanModifyFloat64 operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/int64.go b/resource/schema/planmodifier/int64.go new file mode 100644 index 000000000..70d78725f --- /dev/null +++ b/resource/schema/planmodifier/int64.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Int64 is a schema validator for types.Int64 attributes. +type Int64 interface { + Describer + + // PlanModifyInt64 should perform the modification. + PlanModifyInt64(context.Context, Int64Request, *Int64Response) +} + +// Int64Request is a request for types.Int64 schema plan modification. +type Int64Request struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Int64 + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Int64 + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Int64 + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // Int64Response.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // Int64Response.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// Int64Response is a response to a Int64Request. +type Int64Response struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Int64 + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyInt64 operation. + // This field is pre-populated from Int64Request.Private and + // can be modified during the resource's PlanModifyInt64 operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/list.go b/resource/schema/planmodifier/list.go new file mode 100644 index 000000000..971a09c0b --- /dev/null +++ b/resource/schema/planmodifier/list.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// List is a schema validator for types.List attributes. +type List interface { + Describer + + // PlanModifyList should perform the modification. + PlanModifyList(context.Context, ListRequest, *ListResponse) +} + +// ListRequest is a request for types.List schema plan modification. +type ListRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.List + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.List + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.List + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // ListResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // ListResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// ListResponse is a response to a ListRequest. +type ListResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.List + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyList operation. + // This field is pre-populated from ListRequest.Private and + // can be modified during the resource's PlanModifyList operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/map.go b/resource/schema/planmodifier/map.go new file mode 100644 index 000000000..9ddd51943 --- /dev/null +++ b/resource/schema/planmodifier/map.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Map is a schema validator for types.Map attributes. +type Map interface { + Describer + + // PlanModifyMap should perform the modification. + PlanModifyMap(context.Context, MapRequest, *MapResponse) +} + +// MapRequest is a request for types.Map schema plan modification. +type MapRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Map + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Map + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Map + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // MapResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // MapResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// MapResponse is a response to a MapRequest. +type MapResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Map + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyMap operation. + // This field is pre-populated from MapRequest.Private and + // can be modified during the resource's PlanModifyMap operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/number.go b/resource/schema/planmodifier/number.go new file mode 100644 index 000000000..210a5762f --- /dev/null +++ b/resource/schema/planmodifier/number.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Number is a schema validator for types.Number attributes. +type Number interface { + Describer + + // PlanModifyNumber should perform the modification. + PlanModifyNumber(context.Context, NumberRequest, *NumberResponse) +} + +// NumberRequest is a request for types.Number schema plan modification. +type NumberRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Number + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Number + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Number + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // NumberResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // NumberResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// NumberResponse is a response to a NumberRequest. +type NumberResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Number + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyNumber operation. + // This field is pre-populated from NumberRequest.Private and + // can be modified during the resource's PlanModifyNumber operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/object.go b/resource/schema/planmodifier/object.go new file mode 100644 index 000000000..1d2260bab --- /dev/null +++ b/resource/schema/planmodifier/object.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Object is a schema validator for types.Object attributes. +type Object interface { + Describer + + // PlanModifyObject should perform the modification. + PlanModifyObject(context.Context, ObjectRequest, *ObjectResponse) +} + +// ObjectRequest is a request for types.Object schema plan modification. +type ObjectRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Object + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Object + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Object + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // ObjectResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // ObjectResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// ObjectResponse is a response to a ObjectRequest. +type ObjectResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Object + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyObject operation. + // This field is pre-populated from ObjectRequest.Private and + // can be modified during the resource's PlanModifyObject operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/set.go b/resource/schema/planmodifier/set.go new file mode 100644 index 000000000..93fcf5667 --- /dev/null +++ b/resource/schema/planmodifier/set.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Set is a schema validator for types.Set attributes. +type Set interface { + Describer + + // PlanModifySet should perform the modification. + PlanModifySet(context.Context, SetRequest, *SetResponse) +} + +// SetRequest is a request for types.Set schema plan modification. +type SetRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Set + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Set + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Set + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // SetResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // SetResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// SetResponse is a response to a SetRequest. +type SetResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Set + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifySet operation. + // This field is pre-populated from SetRequest.Private and + // can be modified during the resource's PlanModifySet operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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/resource/schema/planmodifier/string.go b/resource/schema/planmodifier/string.go new file mode 100644 index 000000000..78cce9496 --- /dev/null +++ b/resource/schema/planmodifier/string.go @@ -0,0 +1,85 @@ +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// String is a schema validator for types.String attributes. +type String interface { + Describer + + // PlanModifyString should perform the modification. + PlanModifyString(context.Context, StringRequest, *StringResponse) +} + +// StringRequest is a request for types.String schema plan modification. +type StringRequest struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.String + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.String + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.String + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // StringResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // StringResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// StringResponse is a response to a StringRequest. +type StringResponse struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.String + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyString operation. + // This field is pre-populated from StringRequest.Private and + // can be modified during the resource's PlanModifyString operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // 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 +} From f5ab529291d2e3d2a352574dca03608f6fab373c Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 28 Nov 2022 22:23:07 -0500 Subject: [PATCH 2/4] Update CHANGELOG for #557 --- .changelog/{pending.txt => 557.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 557.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/557.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/557.txt From 782d61164ffbde946e5805a13be5ad1bb0a60b7d Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 29 Nov 2022 00:04:24 -0500 Subject: [PATCH 3/4] resource/schema: Initial package 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 `resource/schema` package, which contains schema interfaces and types relevant to resources. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization, plan modification, and validation. 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 resources. The framework design will also raise compiler-time errors for errant typing of validators. No changes are required for data handling in any other `resource.Resource` methods. 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/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) type ThingResource struct{} func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ Required: true, Validators: []validator.String{ stringvalidator.LengthBetween(3, 256), }, }, "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, }, }, Validators: []validator.Object{ /*...*/ }, }, Optional: true, Validators: []validator.List{ listvalidator.SizeAtMost(2), }, }, "single_nested_attribute": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "int64_attribute": schema.Int64Attribute{ Optional: true, }, }, Optional: true, }, }, Blocks: map[string]schema.Block{ "list_block": schema.ListNestedBlock{ NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "float64_attribute": schema.Float64Attribute{ Optional: true, Validators: []validator.Float64{ float64validator.OneOf(1.2, 2.4), }, }, }, Validators: []validator.Object{ /*...*/ }, }, Validators: []validator.List{ listvalidator.SizeAtMost(2), }, }, }, } } ``` To migrate a resource schema: - Add `github.com/hashicorp/terraform-plugin-framework/resource/schema` to the `import` statement - Switch the `resource.Resource` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package. Prior implementation: ```go func (r ThingResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{/* ... */}, nil } ``` Migrated implementation: ```go func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{/*...*/} } ``` If the resource requires no schema, the method can be entirely empty. - Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute` - Switch `map[string]tfsdk.Block` with `map[string]schema.Block` - Switch individual attribute and block 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 `schema.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 schema.XXXAttribute must be declared inside map[string]schema.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 schema.XXXAttribute must be declared inside map[string]schema.Attribute schema.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 schema.XXXAttribute must be declared inside map[string]schema.Attribute schema.SingleNestedAttribute{ Attributes: map[string]schema.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 schema.XXXAttribute must be declared inside map[string]schema.Attribute schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{/*...*/}, }, Required: true, } ``` Prior collection blocks type (`tfsdk.Block`) attribute implementation: ```go // The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block tfsdk.Block{ Attributes: map[string]tfsdk.Attribute{/*...*/}, Blocks: map[string]tfsdk.Block{/*...*/}, NestingMode: tfsdk.BlockNestingModeList, }, ``` Migrated implementation: ```go // The schema.XXXBlock must be declared inside map[string]schema.Block schema.ListNestedBlock{ NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{/*...*/}, Blocks: map[string]schema.Block{/*...*/}, }, } ``` --- .changelog/pending.txt | 7 + internal/fwserver/server.go | 41 +- .../fwserver/server_getproviderschema_test.go | 77 +- .../server_upgraderesourcestate_test.go | 66 +- .../server_validateresourceconfig_test.go | 78 +- .../server_applyresourcechange_test.go | 95 +- .../server_getproviderschema_test.go | 71 +- .../server_importresourcestate_test.go | 33 +- .../server_planresourcechange_test.go | 83 +- .../proto5server/server_readresource_test.go | 56 +- .../server_upgraderesourcestate_test.go | 34 +- .../server_validateresourcetypeconfig_test.go | 23 +- .../server_applyresourcechange_test.go | 95 +- .../server_getproviderschema_test.go | 71 +- .../server_importresourcestate_test.go | 33 +- .../server_planresourcechange_test.go | 83 +- .../proto6server/server_readresource_test.go | 56 +- .../server_upgraderesourcestate_test.go | 28 +- .../server_validateresourceconfig_test.go | 23 +- internal/testing/testprovider/resource.go | 24 +- resource/resource.go | 27 +- resource/schema.go | 24 + resource/schema/attribute.go | 33 + resource/schema/block.go | 27 + resource/schema/bool_attribute.go | 207 ++++ resource/schema/bool_attribute_test.go | 457 ++++++++ resource/schema/doc.go | 5 + resource/schema/float64_attribute.go | 210 ++++ resource/schema/float64_attribute_test.go | 456 ++++++++ resource/schema/int64_attribute.go | 210 ++++ resource/schema/int64_attribute_test.go | 456 ++++++++ resource/schema/list_attribute.go | 220 ++++ resource/schema/list_attribute_test.go | 461 ++++++++ resource/schema/list_nested_attribute.go | 247 ++++ resource/schema/list_nested_attribute_test.go | 623 ++++++++++ resource/schema/list_nested_block.go | 224 ++++ resource/schema/list_nested_block_test.go | 543 +++++++++ resource/schema/map_attribute.go | 223 ++++ resource/schema/map_attribute_test.go | 460 ++++++++ resource/schema/map_nested_attribute.go | 247 ++++ resource/schema/map_nested_attribute_test.go | 623 ++++++++++ resource/schema/nested_attribute.go | 11 + resource/schema/nested_attribute_object.go | 105 ++ .../schema/nested_attribute_object_test.go | 314 +++++ resource/schema/nested_block_object.go | 117 ++ resource/schema/nested_block_object_test.go | 401 +++++++ resource/schema/number_attribute.go | 211 ++++ resource/schema/number_attribute_test.go | 457 ++++++++ resource/schema/object_attribute.go | 222 ++++ resource/schema/object_attribute_test.go | 467 ++++++++ resource/schema/schema.go | 155 +++ resource/schema/schema_test.go | 1006 +++++++++++++++++ resource/schema/set_attribute.go | 218 ++++ resource/schema/set_attribute_test.go | 461 ++++++++ resource/schema/set_nested_attribute.go | 242 ++++ resource/schema/set_nested_attribute_test.go | 623 ++++++++++ resource/schema/set_nested_block.go | 224 ++++ resource/schema/set_nested_block_test.go | 543 +++++++++ resource/schema/single_nested_attribute.go | 263 +++++ .../schema/single_nested_attribute_test.go | 587 ++++++++++ resource/schema/single_nested_block.go | 247 ++++ resource/schema/single_nested_block_test.go | 543 +++++++++ resource/schema/string_attribute.go | 207 ++++ resource/schema/string_attribute_test.go | 457 ++++++++ 64 files changed, 14588 insertions(+), 553 deletions(-) create mode 100644 .changelog/pending.txt create mode 100644 resource/schema.go create mode 100644 resource/schema/attribute.go create mode 100644 resource/schema/block.go create mode 100644 resource/schema/bool_attribute.go create mode 100644 resource/schema/bool_attribute_test.go create mode 100644 resource/schema/doc.go create mode 100644 resource/schema/float64_attribute.go create mode 100644 resource/schema/float64_attribute_test.go create mode 100644 resource/schema/int64_attribute.go create mode 100644 resource/schema/int64_attribute_test.go create mode 100644 resource/schema/list_attribute.go create mode 100644 resource/schema/list_attribute_test.go create mode 100644 resource/schema/list_nested_attribute.go create mode 100644 resource/schema/list_nested_attribute_test.go create mode 100644 resource/schema/list_nested_block.go create mode 100644 resource/schema/list_nested_block_test.go create mode 100644 resource/schema/map_attribute.go create mode 100644 resource/schema/map_attribute_test.go create mode 100644 resource/schema/map_nested_attribute.go create mode 100644 resource/schema/map_nested_attribute_test.go create mode 100644 resource/schema/nested_attribute.go create mode 100644 resource/schema/nested_attribute_object.go create mode 100644 resource/schema/nested_attribute_object_test.go create mode 100644 resource/schema/nested_block_object.go create mode 100644 resource/schema/nested_block_object_test.go create mode 100644 resource/schema/number_attribute.go create mode 100644 resource/schema/number_attribute_test.go create mode 100644 resource/schema/object_attribute.go create mode 100644 resource/schema/object_attribute_test.go create mode 100644 resource/schema/schema.go create mode 100644 resource/schema/schema_test.go create mode 100644 resource/schema/set_attribute.go create mode 100644 resource/schema/set_attribute_test.go create mode 100644 resource/schema/set_nested_attribute.go create mode 100644 resource/schema/set_nested_attribute_test.go create mode 100644 resource/schema/set_nested_block.go create mode 100644 resource/schema/set_nested_block_test.go create mode 100644 resource/schema/single_nested_attribute.go create mode 100644 resource/schema/single_nested_attribute_test.go create mode 100644 resource/schema/single_nested_block.go create mode 100644 resource/schema/single_nested_block_test.go create mode 100644 resource/schema/string_attribute.go create mode 100644 resource/schema/string_attribute_test.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..45576479b --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,7 @@ +```release-note:note +resource: The `Resource` type `GetSchema` method has been deprecated. Use the `Schema` method instead. +``` + +```release-note:feature +resource/schema: New package which contains schema interfaces and types relevant to resources +``` diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 2ae010cf7..e4dd1bdd8 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -433,17 +433,42 @@ func (s *Server) ResourceSchemas(ctx context.Context) (map[string]fwschema.Schem for resourceTypeName, resourceFunc := range resourceFuncs { res := resourceFunc() - logging.FrameworkDebug(ctx, "Calling provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) - schema, diags := res.GetSchema(ctx) - logging.FrameworkDebug(ctx, "Called provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) + switch resourceIface := res.(type) { + case resource.ResourceWithSchema: + schemaReq := resource.SchemaRequest{} + schemaResp := resource.SchemaResponse{} - s.resourceSchemasDiags.Append(diags...) + logging.FrameworkDebug(ctx, "Calling provider defined Resource Schema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) + resourceIface.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkDebug(ctx, "Called provider defined Resource Schema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) - if s.resourceSchemasDiags.HasError() { - return s.resourceSchemas, s.resourceSchemasDiags - } + s.resourceSchemasDiags.Append(schemaResp.Diagnostics...) + + if s.resourceSchemasDiags.HasError() { + return s.resourceSchemas, s.resourceSchemasDiags + } - s.resourceSchemas[resourceTypeName] = schema + s.resourceSchemas[resourceTypeName] = schemaResp.Schema + case resource.ResourceWithGetSchema: + logging.FrameworkDebug(ctx, "Calling provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) + schema, diags := resourceIface.GetSchema(ctx) //nolint:staticcheck // Required internal usage until removal + logging.FrameworkDebug(ctx, "Called provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) + + s.resourceSchemasDiags.Append(diags...) + + if s.resourceSchemasDiags.HasError() { + return s.resourceSchemas, s.resourceSchemasDiags + } + + s.resourceSchemas[resourceTypeName] = schema + default: + s.resourceSchemasDiags.AddError( + "Resource Missing Schema", + "While attempting to load provider resource schemas, a resource was missing a Schema method. "+ + "This is always an issue in the provider and should be reported to the provider developers.\n\n"+ + "Resource Type Name: "+resourceTypeName, + ) + } } return s.resourceSchemas, s.resourceSchemasDiags diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index 3cbe191ea..46994d9c1 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider" "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" ) @@ -317,15 +318,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource1" @@ -334,15 +334,14 @@ func TestServerGetProviderSchema(t *testing.T) { }, func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource2" @@ -358,19 +357,17 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, Provider: &tfsdk.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ - "test_resource1": tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + "test_resource1": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, }, - "test_resource2": tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + "test_resource2": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, }, @@ -387,15 +384,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -404,15 +400,14 @@ func TestServerGetProviderSchema(t *testing.T) { }, func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -485,15 +480,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_resource" @@ -510,11 +504,10 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, Provider: &tfsdk.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ - "testprovidertype_resource": tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + "testprovidertype_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, }, diff --git a/internal/fwserver/server_upgraderesourcestate_test.go b/internal/fwserver/server_upgraderesourcestate_test.go index 9225f7243..b967e9fac 100644 --- a/internal/fwserver/server_upgraderesourcestate_test.go +++ b/internal/fwserver/server_upgraderesourcestate_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" "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" ) @@ -23,24 +24,21 @@ func TestServerUpgradeResourceState(t *testing.T) { t.Parallel() ctx := context.Background() - schema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "id": { - Type: types.StringType, + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ Computed: true, }, - "optional_attribute": { - Type: types.StringType, + "optional_attribute": schema.StringAttribute{ Optional: true, }, - "required_attribute": { - Type: types.StringType, + "required_attribute": schema.StringAttribute{ Required: true, }, }, Version: 1, // Must be above 0 } - schemaType := schema.Type().TerraformType(ctx) + schemaType := testSchema.Type().TerraformType(ctx) testCases := map[string]struct { server *fwserver.Server @@ -63,7 +61,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithConfigureAndUpgradeState{ ConfigureMethod: func(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { providerData, ok := req.ProviderData.(string) @@ -150,7 +148,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -159,7 +157,7 @@ func TestServerUpgradeResourceState(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.UpgradeResourceStateRequest{ - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.Resource{}, Version: 0, }, @@ -174,7 +172,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -268,7 +266,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -281,7 +279,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -341,7 +339,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -354,7 +352,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.Resource{}, Version: 0, }, @@ -378,7 +376,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -407,7 +405,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -458,7 +456,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -522,7 +520,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -536,7 +534,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "required_attribute": true, "nonexistent_attribute": "value", }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -600,7 +598,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -613,7 +611,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { @@ -646,8 +644,8 @@ func TestServerUpgradeResourceState(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -664,7 +662,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "flatmap": "is not supported", }, }, - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.Resource{}, Version: 1, // Must match current tfsdk.Schema version to trigger framework implementation }, @@ -688,8 +686,8 @@ func TestServerUpgradeResourceState(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -705,7 +703,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": "true", }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.Resource{}, Version: 1, // Must match current tfsdk.Schema version to trigger framework implementation }, @@ -716,7 +714,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -730,7 +728,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "required_attribute": "true", "nonexistent_attribute": "value", }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.Resource{}, Version: 1, // Must match current tfsdk.Schema version to trigger framework implementation }, @@ -741,7 +739,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, }, }, }, @@ -754,7 +752,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "id": "test-id-value", "required_attribute": true, }), - ResourceSchema: schema, + ResourceSchema: testSchema, Resource: &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{}, UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { diff --git a/internal/fwserver/server_validateresourceconfig_test.go b/internal/fwserver/server_validateresourceconfig_test.go index 2552b9a03..33d206f9a 100644 --- a/internal/fwserver/server_validateresourceconfig_test.go +++ b/internal/fwserver/server_validateresourceconfig_test.go @@ -8,8 +8,11 @@ import ( "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/internal/testing/testvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -28,11 +31,10 @@ func TestServerValidateResourceConfig(t *testing.T) { "test": tftypes.NewValue(tftypes.String, "test-value"), }) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -42,24 +44,15 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchema, } - testSchemaAttributeValidator := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + testSchemaAttributeValidator := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ Required: true, - Type: types.StringType, - Validators: []tfsdk.AttributeValidator{ - &testprovider.AttributeValidator{ - ValidateMethod: func(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { - var got types.String - - resp.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &got)...) - - if resp.Diagnostics.HasError() { - return - } - - if got.ValueString() != "test-value" { - resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.ValueString()) + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+req.ConfigValue.ValueString()) } }, }, @@ -73,15 +66,14 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchemaAttributeValidator, } - testSchemaAttributeValidatorError := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + testSchemaAttributeValidatorError := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ Required: true, - Type: types.StringType, - Validators: []tfsdk.AttributeValidator{ - &testprovider.AttributeValidator{ - ValidateMethod: func(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { - resp.Diagnostics.AddAttributeError(req.AttributePath, "error summary", "error detail") + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeError(req.Path, "error summary", "error detail") }, }, }, @@ -112,8 +104,8 @@ func TestServerValidateResourceConfig(t *testing.T) { request: &fwserver.ValidateResourceConfigRequest{ Config: &testConfig, Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, }, }, @@ -126,8 +118,8 @@ func TestServerValidateResourceConfig(t *testing.T) { request: &fwserver.ValidateResourceConfigRequest{ Config: &testConfigAttributeValidator, Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchemaAttributeValidator, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchemaAttributeValidator }, }, }, @@ -140,8 +132,8 @@ func TestServerValidateResourceConfig(t *testing.T) { request: &fwserver.ValidateResourceConfigRequest{ Config: &testConfigAttributeValidatorError, Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchemaAttributeValidatorError, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorError }, }, }, @@ -163,8 +155,8 @@ func TestServerValidateResourceConfig(t *testing.T) { Config: &testConfig, Resource: &testprovider.ResourceWithConfigValidators{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, }, ConfigValidatorsMethod: func(ctx context.Context) []resource.ConfigValidator { @@ -198,8 +190,8 @@ func TestServerValidateResourceConfig(t *testing.T) { Config: &testConfig, Resource: &testprovider.ResourceWithConfigValidators{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, }, ConfigValidatorsMethod: func(ctx context.Context) []resource.ConfigValidator { @@ -229,8 +221,8 @@ func TestServerValidateResourceConfig(t *testing.T) { Config: &testConfig, Resource: &testprovider.ResourceWithValidateConfig{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, }, ValidateConfigMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { @@ -258,8 +250,8 @@ func TestServerValidateResourceConfig(t *testing.T) { Config: &testConfig, Resource: &testprovider.ResourceWithValidateConfig{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, }, ValidateConfigMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { diff --git a/internal/proto5server/server_applyresourcechange_test.go b/internal/proto5server/server_applyresourcechange_test.go index 9990becb3..b50a8b7a3 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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" ) @@ -30,15 +31,13 @@ func TestServerApplyResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov5.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "test_required": { + "test_required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -85,8 +84,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -143,8 +142,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -202,8 +201,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -268,8 +267,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -327,8 +326,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -379,8 +378,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -435,8 +434,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -485,8 +484,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -534,8 +533,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -587,8 +586,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -641,8 +640,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -699,8 +698,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -741,8 +740,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -801,8 +800,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -861,8 +860,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -921,8 +920,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -986,8 +985,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1058,8 +1057,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1123,8 +1122,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1178,8 +1177,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1235,8 +1234,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index ed633b203..fd844b234 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -7,12 +7,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + 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/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" @@ -38,9 +39,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test1": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test1": datasourceschema.StringAttribute{ Required: true, }, }, @@ -54,9 +55,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test2": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test2": datasourceschema.StringAttribute{ Required: true, }, }, @@ -116,9 +117,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test1": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test1": datasourceschema.StringAttribute{ Required: true, }, }, @@ -132,9 +133,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test2": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test2": datasourceschema.StringAttribute{ Required: true, }, }, @@ -295,15 +296,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource1" @@ -312,15 +312,14 @@ func TestServerGetProviderSchema(t *testing.T) { }, func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource2" @@ -375,15 +374,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -392,15 +390,14 @@ func TestServerGetProviderSchema(t *testing.T) { }, func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto5server/server_importresourcestate_test.go b/internal/proto5server/server_importresourcestate_test.go index cf08de133..2e5f5c3e8 100644 --- a/internal/proto5server/server_importresourcestate_test.go +++ b/internal/proto5server/server_importresourcestate_test.go @@ -8,14 +8,12 @@ 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/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" ) func TestServerImportResourceState(t *testing.T) { @@ -35,19 +33,16 @@ func TestServerImportResourceState(t *testing.T) { "required": tftypes.NewValue(tftypes.String, nil), }) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "id": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "optional": { + "optional": schema.StringAttribute{ Optional: true, - Type: types.StringType, }, - "required": { + "required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -67,8 +62,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -110,8 +105,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -156,8 +151,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -195,8 +190,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index 4ed55f7d9..7bd6b7483 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" "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" @@ -28,15 +29,13 @@ func TestServerPlanResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov5.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "test_required": { + "test_required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -84,8 +83,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -135,8 +134,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -187,8 +186,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -243,8 +242,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -301,8 +300,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -352,8 +351,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -408,8 +407,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -453,8 +452,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -502,8 +501,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -553,8 +552,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -604,8 +603,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -653,8 +652,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -707,8 +706,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -761,8 +760,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -816,8 +815,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -875,8 +874,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -936,8 +935,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -990,8 +989,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index ad47d0f29..ffe661e75 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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" ) @@ -42,15 +43,13 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov5.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "test_required": { + "test_required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -69,9 +68,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -98,8 +95,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -140,9 +137,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -164,7 +159,18 @@ func TestServerReadResource(t *testing.T) { }, }, GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + return tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_computed": { + Computed: true, + Type: types.StringType, + }, + "test_required": { + Required: true, + Type: types.StringType, + }, + }, + }, nil }, }, }, @@ -186,9 +192,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -236,8 +240,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -281,8 +285,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -322,8 +326,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -354,9 +358,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, diff --git a/internal/proto5server/server_upgraderesourcestate_test.go b/internal/proto5server/server_upgraderesourcestate_test.go index ec46b94e5..a97975d31 100644 --- a/internal/proto5server/server_upgraderesourcestate_test.go +++ b/internal/proto5server/server_upgraderesourcestate_test.go @@ -5,12 +5,11 @@ 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/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" ) @@ -19,24 +18,21 @@ func TestServerUpgradeResourceState(t *testing.T) { t.Parallel() ctx := context.Background() - schema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "id": { - Type: types.StringType, + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ Computed: true, }, - "optional_attribute": { - Type: types.StringType, + "optional_attribute": schema.StringAttribute{ Optional: true, }, - "required_attribute": { - Type: types.StringType, + "required_attribute": schema.StringAttribute{ Required: true, }, }, Version: 1, // Must be above 0 } - schemaType := schema.Type().TerraformType(ctx) + schemaType := testSchema.Type().TerraformType(ctx) testCases := map[string]struct { server *Server @@ -62,8 +58,8 @@ func TestServerUpgradeResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -89,7 +85,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, } }, }, @@ -163,8 +159,8 @@ func TestServerUpgradeResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -219,8 +215,8 @@ func TestServerUpgradeResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -236,7 +232,7 @@ func TestServerUpgradeResourceState(t *testing.T) { "optional_attribute": tftypes.NewValue(tftypes.String, nil), "required_attribute": tftypes.NewValue(tftypes.String, "true"), }), - Schema: schema, + Schema: testSchema, } }, }, diff --git a/internal/proto5server/server_validateresourcetypeconfig_test.go b/internal/proto5server/server_validateresourcetypeconfig_test.go index 2a3a93f14..bbaf81b9f 100644 --- a/internal/proto5server/server_validateresourcetypeconfig_test.go +++ b/internal/proto5server/server_validateresourcetypeconfig_test.go @@ -5,12 +5,10 @@ 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/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -34,11 +32,10 @@ func TestServerValidateResourceTypeConfig(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -57,9 +54,7 @@ func TestServerValidateResourceTypeConfig(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -83,8 +78,8 @@ func TestServerValidateResourceTypeConfig(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -111,8 +106,8 @@ func TestServerValidateResourceTypeConfig(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithValidateConfig{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto6server/server_applyresourcechange_test.go b/internal/proto6server/server_applyresourcechange_test.go index 498eef020..e2eb03f31 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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" ) @@ -30,15 +31,13 @@ func TestServerApplyResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov6.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "test_required": { + "test_required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -85,8 +84,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -143,8 +142,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -202,8 +201,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -268,8 +267,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -327,8 +326,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -379,8 +378,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -435,8 +434,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -485,8 +484,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -534,8 +533,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -587,8 +586,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -641,8 +640,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -699,8 +698,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -741,8 +740,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -801,8 +800,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -861,8 +860,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -921,8 +920,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -986,8 +985,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1058,8 +1057,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1123,8 +1122,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1178,8 +1177,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -1235,8 +1234,8 @@ func TestServerApplyResourceChange(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index 46dfa4d6b..eb0fc0196 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -7,12 +7,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + 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/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" @@ -38,9 +39,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test1": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test1": datasourceschema.StringAttribute{ Required: true, }, }, @@ -54,9 +55,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test2": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test2": datasourceschema.StringAttribute{ Required: true, }, }, @@ -116,9 +117,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test1": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test1": datasourceschema.StringAttribute{ Required: true, }, }, @@ -132,9 +133,9 @@ func TestServerGetProviderSchema(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test2": schema.StringAttribute{ + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test2": datasourceschema.StringAttribute{ Required: true, }, }, @@ -295,15 +296,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource1" @@ -312,15 +312,14 @@ func TestServerGetProviderSchema(t *testing.T) { }, func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource2" @@ -375,15 +374,14 @@ func TestServerGetProviderSchema(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test1": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -392,15 +390,14 @@ func TestServerGetProviderSchema(t *testing.T) { }, func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test2": { + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ Required: true, - Type: types.StringType, }, }, - }, nil + } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto6server/server_importresourcestate_test.go b/internal/proto6server/server_importresourcestate_test.go index 4c6dea05e..b6a5e408a 100644 --- a/internal/proto6server/server_importresourcestate_test.go +++ b/internal/proto6server/server_importresourcestate_test.go @@ -8,14 +8,12 @@ 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/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" ) func TestServerImportResourceState(t *testing.T) { @@ -35,19 +33,16 @@ func TestServerImportResourceState(t *testing.T) { "required": tftypes.NewValue(tftypes.String, nil), }) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "id": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "optional": { + "optional": schema.StringAttribute{ Optional: true, - Type: types.StringType, }, - "required": { + "required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -67,8 +62,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -110,8 +105,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -156,8 +151,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -195,8 +190,8 @@ func TestServerImportResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithImportState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto6server/server_planresourcechange_test.go b/internal/proto6server/server_planresourcechange_test.go index 21dfb86b2..4a81b3c76 100644 --- a/internal/proto6server/server_planresourcechange_test.go +++ b/internal/proto6server/server_planresourcechange_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" "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" @@ -28,15 +29,13 @@ func TestServerPlanResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov6.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "test_required": { + "test_required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -84,8 +83,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -135,8 +134,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -187,8 +186,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -243,8 +242,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -301,8 +300,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -352,8 +351,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -408,8 +407,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -453,8 +452,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -502,8 +501,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -553,8 +552,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -604,8 +603,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -653,8 +652,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -707,8 +706,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -761,8 +760,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -816,8 +815,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -875,8 +874,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -936,8 +935,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -990,8 +989,8 @@ func TestServerPlanResourceChange(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithModifyPlan{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 9ee1b1228..0f799d17a 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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" ) @@ -42,15 +43,13 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov6.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test_computed": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, - "test_required": { + "test_required": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -69,9 +68,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -98,8 +95,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -140,9 +137,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -164,7 +159,18 @@ func TestServerReadResource(t *testing.T) { }, }, GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + return tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_computed": { + Computed: true, + Type: types.StringType, + }, + "test_required": { + Required: true, + Type: types.StringType, + }, + }, + }, nil }, }, }, @@ -186,9 +192,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -236,8 +240,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -281,8 +285,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -322,8 +326,8 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -354,9 +358,7 @@ func TestServerReadResource(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, diff --git a/internal/proto6server/server_upgraderesourcestate_test.go b/internal/proto6server/server_upgraderesourcestate_test.go index 832613914..bb15d36cd 100644 --- a/internal/proto6server/server_upgraderesourcestate_test.go +++ b/internal/proto6server/server_upgraderesourcestate_test.go @@ -5,12 +5,11 @@ 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/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" ) @@ -19,18 +18,15 @@ func TestServerUpgradeResourceState(t *testing.T) { t.Parallel() ctx := context.Background() - schema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "id": { - Type: types.StringType, + schema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ Computed: true, }, - "optional_attribute": { - Type: types.StringType, + "optional_attribute": schema.StringAttribute{ Optional: true, }, - "required_attribute": { - Type: types.StringType, + "required_attribute": schema.StringAttribute{ Required: true, }, }, @@ -62,8 +58,8 @@ func TestServerUpgradeResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -163,8 +159,8 @@ func TestServerUpgradeResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -219,8 +215,8 @@ func TestServerUpgradeResourceState(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithUpgradeState{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return schema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/proto6server/server_validateresourceconfig_test.go b/internal/proto6server/server_validateresourceconfig_test.go index 6cd980cdd..7fa89b75c 100644 --- a/internal/proto6server/server_validateresourceconfig_test.go +++ b/internal/proto6server/server_validateresourceconfig_test.go @@ -5,12 +5,10 @@ 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/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -34,11 +32,10 @@ func TestServerValidateResourceConfig(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } - testSchema := tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ - "test": { + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ Required: true, - Type: types.StringType, }, }, } @@ -57,9 +54,7 @@ func TestServerValidateResourceConfig(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{}, nil - }, + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, @@ -83,8 +78,8 @@ func TestServerValidateResourceConfig(t *testing.T) { return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" @@ -111,8 +106,8 @@ func TestServerValidateResourceConfig(t *testing.T) { func() resource.Resource { return &testprovider.ResourceWithValidateConfig{ Resource: &testprovider.Resource{ - GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" diff --git a/internal/testing/testprovider/resource.go b/internal/testing/testprovider/resource.go index b4c45adc6..961d4ccfe 100644 --- a/internal/testing/testprovider/resource.go +++ b/internal/testing/testprovider/resource.go @@ -3,9 +3,7 @@ package testprovider import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) var _ resource.Resource = &Resource{} @@ -13,12 +11,12 @@ var _ resource.Resource = &Resource{} // Declarative resource.Resource for unit testing. type Resource struct { // Resource interface methods - MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) - GetSchemaMethod func(context.Context) (tfsdk.Schema, diag.Diagnostics) - CreateMethod func(context.Context, resource.CreateRequest, *resource.CreateResponse) - DeleteMethod func(context.Context, resource.DeleteRequest, *resource.DeleteResponse) - ReadMethod func(context.Context, resource.ReadRequest, *resource.ReadResponse) - UpdateMethod func(context.Context, resource.UpdateRequest, *resource.UpdateResponse) + MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) + SchemaMethod func(context.Context, resource.SchemaRequest, *resource.SchemaResponse) + CreateMethod func(context.Context, resource.CreateRequest, *resource.CreateResponse) + DeleteMethod func(context.Context, resource.DeleteRequest, *resource.DeleteResponse) + ReadMethod func(context.Context, resource.ReadRequest, *resource.ReadResponse) + UpdateMethod func(context.Context, resource.UpdateRequest, *resource.UpdateResponse) } // Metadata satisfies the resource.Resource interface. @@ -30,13 +28,13 @@ func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, r r.MetadataMethod(ctx, req, resp) } -// GetSchema satisfies the resource.Resource interface. -func (r *Resource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { - if r.GetSchemaMethod == nil { - return tfsdk.Schema{}, nil +// Schema satisfies the resource.Resource interface. +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + if r.SchemaMethod == nil { + return } - return r.GetSchemaMethod(ctx) + r.SchemaMethod(ctx, req, resp) } // Create satisfies the resource.Resource interface. diff --git a/resource/resource.go b/resource/resource.go index 63ced26a0..e811da77c 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -8,7 +8,9 @@ import ( ) // Resource represents an instance of a managed resource type. This is the core -// interface that all resources must implement. +// interface that all resources must implement. Resources must also +// implement the Schema method or the deprecated GetSchema method. The Schema +// method will be required in a future version. // // Resources can optionally implement these additional concepts: // @@ -27,9 +29,6 @@ type Resource interface { // examplecloud_thing. Metadata(context.Context, MetadataRequest, *MetadataResponse) - // GetSchema returns the schema for this resource. - GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) - // Create is called when the provider must create a new resource. Config // and planned state values should be read from the // CreateRequest and new state values set on the CreateResponse. @@ -85,6 +84,17 @@ type ResourceWithConfigValidators interface { ConfigValidators(context.Context) []ConfigValidator } +// ResourceWithGetSchema is a temporary interface type that extends +// Resource to include the deprecated GetSchema method. +type ResourceWithGetSchema interface { + Resource + + // GetSchema returns the schema for this resource. + // + // Deprecated: Use Schema method instead. + GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) +} + // Optional interface on top of Resource that enables provider control over // the ImportResourceState RPC. This RPC is called by Terraform when the // `terraform import` command is executed. Afterwards, the ReadResource RPC @@ -127,6 +137,15 @@ type ResourceWithModifyPlan interface { ModifyPlan(context.Context, ModifyPlanRequest, *ModifyPlanResponse) } +// ResourceWithSchema is a temporary interface type that extends +// Resource to include the new Schema method. +type ResourceWithSchema interface { + Resource + + // Schema should return the schema for this resource. + Schema(context.Context, SchemaRequest, *SchemaResponse) +} + // Optional interface on top of Resource that enables provider control over // the UpgradeResourceState RPC. This RPC is automatically called by Terraform // when the current Schema type Version field is greater than the stored state. diff --git a/resource/schema.go b/resource/schema.go new file mode 100644 index 000000000..f1e59dce2 --- /dev/null +++ b/resource/schema.go @@ -0,0 +1,24 @@ +package resource + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +// SchemaRequest represents a request for the Resource to return its schema. +// An instance of this request struct is supplied as an argument to the +// Resource type Schema method. +type SchemaRequest struct{} + +// SchemaResponse represents a response to a SchemaRequest. An instance of this +// response struct is supplied as an argument to the Resource type Schema +// method. +type SchemaResponse struct { + // Schema is the schema of the data source. + Schema schema.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/resource/schema/attribute.go b/resource/schema/attribute.go new file mode 100644 index 000000000..b7d1e9127 --- /dev/null +++ b/resource/schema/attribute.go @@ -0,0 +1,33 @@ +package schema + +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/resource/schema/block.go b/resource/schema/block.go new file mode 100644 index 000000000..f6e27e642 --- /dev/null +++ b/resource/schema/block.go @@ -0,0 +1,27 @@ +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Block defines a structural field inside a Schema. Implementations in this +// package include: +// - ListNestedBlock +// - SetNestedBlock +// - SingleNestedBlock +// +// In practitioner configurations, an equals sign (=) cannot be used to set the +// value. Blocks are instead repeated as necessary, or require the use of +// [Dynamic Block Expressions]. +// +// Prefer NestedAttribute over Block. Blocks should typically be used for +// configuration compatibility with previously existing schemas from an older +// Terraform Plugin SDK. Efforts should be made to convert from Block to +// NestedAttribute as a breaking change for practitioners. +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Block interface { + fwschema.Block +} diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go new file mode 100644 index 000000000..32532f4ba --- /dev/null +++ b/resource/schema/bool_attribute.go @@ -0,0 +1,207 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = BoolAttribute{} + _ fwxschema.AttributeWithBoolPlanModifiers = BoolAttribute{} + _ fwxschema.AttributeWithBoolValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Bool + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Bool +} + +// 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) +} + +// BoolPlanModifiers returns the PlanModifiers field value. +func (a BoolAttribute) BoolPlanModifiers() []planmodifier.Bool { + return a.PlanModifiers +} + +// BoolValidators returns the Validators field value. +func (a BoolAttribute) BoolValidators() []validator.Bool { + return a.Validators +} + +// 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 returns the DeprecationMessage field value. +func (a BoolAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a BoolAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a BoolAttribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/resource/schema/bool_attribute_test.go b/resource/schema/bool_attribute_test.go new file mode 100644 index 000000000..c0e77c718 --- /dev/null +++ b/resource/schema/bool_attribute_test.go @@ -0,0 +1,457 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.BoolType"), + }, + "ElementKeyInt": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.BoolType"), + }, + "ElementKeyString": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.BoolType"), + }, + "ElementKeyValue": { + attribute: schema.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 TestBoolAttributeBoolPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected []planmodifier.Bool + }{ + "no-planmodifiers": { + attribute: schema.BoolAttribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.BoolAttribute{ + PlanModifiers: []planmodifier.Bool{}, + }, + expected: []planmodifier.Bool{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.BoolPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeBoolValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected []validator.Bool + }{ + "no-validators": { + attribute: schema.BoolAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.BoolAttribute{ + Validators: []validator.Bool{}, + }, + expected: []validator.Bool{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.BoolValidators() + + 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 schema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.BoolAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: schema.BoolAttribute{}, + other: schema.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 schema.BoolAttribute + expected string + }{ + "no-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: schema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: schema.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 schema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.BoolAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "required": { + attribute: schema.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 schema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.BoolAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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/resource/schema/doc.go b/resource/schema/doc.go new file mode 100644 index 000000000..14ee29ae7 --- /dev/null +++ b/resource/schema/doc.go @@ -0,0 +1,5 @@ +// Package schema contains all available schema functionality for resources. +// Resource schemas define the structure and value types for configuration, +// plan, and state data. Schemas are implemented via the resource.Resource type +// Schema method. +package schema diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go new file mode 100644 index 000000000..3f3692e37 --- /dev/null +++ b/resource/schema/float64_attribute.go @@ -0,0 +1,210 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float64Attribute{} + _ fwxschema.AttributeWithFloat64PlanModifiers = Float64Attribute{} + _ fwxschema.AttributeWithFloat64Validators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float64 + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Float64 +} + +// 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) +} + +// Float64PlanModifiers returns the PlanModifiers field value. +func (a Float64Attribute) Float64PlanModifiers() []planmodifier.Float64 { + return a.PlanModifiers +} + +// Float64Validators returns the Validators field value. +func (a Float64Attribute) Float64Validators() []validator.Float64 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a Float64Attribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a Float64Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/resource/schema/float64_attribute_test.go b/resource/schema/float64_attribute_test.go new file mode 100644 index 000000000..c4beb7c4e --- /dev/null +++ b/resource/schema/float64_attribute_test.go @@ -0,0 +1,456 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.Float64Type"), + }, + "ElementKeyInt": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.Float64Type"), + }, + "ElementKeyString": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.Float64Type"), + }, + "ElementKeyValue": { + attribute: schema.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 TestFloat64AttributeFloat64PlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected []planmodifier.Float64 + }{ + "no-planmodifiers": { + attribute: schema.Float64Attribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.Float64Attribute{ + PlanModifiers: []planmodifier.Float64{}, + }, + expected: []planmodifier.Float64{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float64PlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeFloat64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected []validator.Float64 + }{ + "no-validators": { + attribute: schema.Float64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float64Attribute{ + Validators: []validator.Float64{}, + }, + expected: []validator.Float64{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float64Validators() + + 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 schema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float64Attribute{}, + other: schema.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 schema.Float64Attribute + expected string + }{ + "no-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float64Attribute{}, + expected: types.Float64Type, + }, + // "custom-type": { + // attribute: schema.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 schema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float64Attribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.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 schema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float64Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go new file mode 100644 index 000000000..0efcf0762 --- /dev/null +++ b/resource/schema/int64_attribute.go @@ -0,0 +1,210 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int64Attribute{} + _ fwxschema.AttributeWithInt64PlanModifiers = Int64Attribute{} + _ fwxschema.AttributeWithInt64Validators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Int64 + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Int64 +} + +// 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 returns the DeprecationMessage field value. +func (a Int64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 +} + +// Int64PlanModifiers returns the PlanModifiers field value. +func (a Int64Attribute) Int64PlanModifiers() []planmodifier.Int64 { + return a.PlanModifiers +} + +// Int64Validators returns the Validators field value. +func (a Int64Attribute) Int64Validators() []validator.Int64 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int64Attribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a Int64Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/resource/schema/int64_attribute_test.go b/resource/schema/int64_attribute_test.go new file mode 100644 index 000000000..0968a371f --- /dev/null +++ b/resource/schema/int64_attribute_test.go @@ -0,0 +1,456 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.Int64Type"), + }, + "ElementKeyInt": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.Int64Type"), + }, + "ElementKeyString": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.Int64Type"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int64Attribute{}, + other: schema.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 schema.Int64Attribute + expected string + }{ + "no-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int64Attribute{}, + expected: types.Int64Type, + }, + // "custom-type": { + // attribute: schema.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 TestInt64AttributeInt64PlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected []planmodifier.Int64 + }{ + "no-planmodifiers": { + attribute: schema.Int64Attribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.Int64Attribute{ + PlanModifiers: []planmodifier.Int64{}, + }, + expected: []planmodifier.Int64{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int64PlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeInt64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected []validator.Int64 + }{ + "no-validators": { + attribute: schema.Int64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int64Attribute{ + Validators: []validator.Int64{}, + }, + expected: []validator.Int64{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int64Validators() + + 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 schema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int64Attribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.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 schema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int64Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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/resource/schema/list_attribute.go b/resource/schema/list_attribute.go new file mode 100644 index 000000000..e0333a402 --- /dev/null +++ b/resource/schema/list_attribute.go @@ -0,0 +1,220 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ListAttribute{} + _ fwxschema.AttributeWithListPlanModifiers = ListAttribute{} + _ fwxschema.AttributeWithListValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.List +} + +// 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 returns the DeprecationMessage field value. +func (a ListAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a ListAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a ListAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ListPlanModifiers returns the PlanModifiers field value. +func (a ListAttribute) ListPlanModifiers() []planmodifier.List { + return a.PlanModifiers +} + +// ListValidators returns the Validators field value. +func (a ListAttribute) ListValidators() []validator.List { + return a.Validators +} diff --git a/resource/schema/list_attribute_test.go b/resource/schema/list_attribute_test.go new file mode 100644 index 000000000..6a6edc4f3 --- /dev/null +++ b/resource/schema/list_attribute_test.go @@ -0,0 +1,461 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.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 schema.ListAttribute + expected string + }{ + "no-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.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 schema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.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 schema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.ListAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.ListAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.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 schema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.ListAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestListAttributeListPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected []planmodifier.List + }{ + "no-planmodifiers": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.ListAttribute{ + PlanModifiers: []planmodifier.List{}, + }, + expected: []planmodifier.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.ListAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go new file mode 100644 index 000000000..978dd952d --- /dev/null +++ b/resource/schema/list_nested_attribute.go @@ -0,0 +1,247 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = ListNestedAttribute{} + _ fwxschema.AttributeWithListPlanModifiers = ListNestedAttribute{} + _ fwxschema.AttributeWithListValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.List +} + +// 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 returns the DeprecationMessage field value. +func (a ListNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a ListNestedAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a ListNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ListPlanModifiers returns the PlanModifiers field value. +func (a ListNestedAttribute) ListPlanModifiers() []planmodifier.List { + return a.PlanModifiers +} + +// ListValidators returns the Validators field value. +func (a ListNestedAttribute) ListValidators() []validator.List { + return a.Validators +} diff --git a/resource/schema/list_nested_attribute_test.go b/resource/schema/list_nested_attribute_test.go new file mode 100644 index 000000000..bb871073a --- /dev/null +++ b/resource/schema/list_nested_attribute_test.go @@ -0,0 +1,623 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.ListNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.ListNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.ListNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-attributes": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.ListNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.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 schema.ListNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.ListNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.ListNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.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 schema.ListNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.ListNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.ListNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.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 schema.ListNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.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 schema.ListNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.ListNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestListNestedAttributeListPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected []planmodifier.List + }{ + "no-planmodifiers": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "planmodifiers": { + attribute: schema.ListNestedAttribute{ + PlanModifiers: []planmodifier.List{}, + }, + expected: []planmodifier.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.ListNestedAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/list_nested_block.go b/resource/schema/list_nested_block.go new file mode 100644 index 000000000..14e7e1c81 --- /dev/null +++ b/resource/schema/list_nested_block.go @@ -0,0 +1,224 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = ListNestedBlock{} + _ fwxschema.BlockWithListPlanModifiers = ListNestedBlock{} + _ fwxschema.BlockWithListValidators = ListNestedBlock{} +) + +// ListNestedBlock represents a block that is a list of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer ListNestedAttribute over ListNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # list of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type ListNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + NestedObject NestedBlockObject + + // 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.List +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyInt, otherwise returns an error. +func (b ListNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is ListNestedBlock +// and all fields are equal. +func (b ListNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(ListNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b ListNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b ListNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b ListNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems always returns 0. +// +// Deprecated: This method will be removed in the future. Use validators +// instead. +func (b ListNestedBlock) GetMaxItems() int64 { + return 0 +} + +// GetMinItems always returns 0. +// +// Deprecated: This method will be removed in the future. Use validators +// instead. +func (b ListNestedBlock) GetMinItems() int64 { + return 0 +} + +// GetNestedObject returns the NestedObject field value. +func (b ListNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeList. +func (b ListNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeList +} + +// ListPlanModifiers returns the PlanModifiers field value. +func (b ListNestedBlock) ListPlanModifiers() []planmodifier.List { + return b.PlanModifiers +} + +// ListValidators returns the Validators field value. +func (b ListNestedBlock) ListValidators() []validator.List { + return b.Validators +} + +// Type returns ListType of ObjectType or CustomType. +func (b ListNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.ListType{ + ElemType: b.NestedObject.Type(), + } +} diff --git a/resource/schema/list_nested_block_test.go b/resource/schema/list_nested_block_test.go new file mode 100644 index 000000000..964d18363 --- /dev/null +++ b/resource/schema/list_nested_block_test.go @@ -0,0 +1,543 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedBlock"), + }, + "ElementKeyInt": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedBlock"), + }, + "ElementKeyValue": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedBlock"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.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 TestListNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.ListNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithListValidators{}, + expected: false, + }, + "different-attributes": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.ListNestedBlock{ + 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.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.ListNestedBlock{ + 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.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetMaxItems(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected int64 + }{ + "0": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMaxItems() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetMinItems(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected int64 + }{ + "0": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMinItems() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockListPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected []planmodifier.List + }{ + "no-planmodifiers": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "planmodifiers": { + block: schema.ListNestedBlock{ + PlanModifiers: []planmodifier.List{}, + }, + expected: []planmodifier.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ListPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected []validator.List + }{ + "no-validators": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.ListNestedBlock{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected attr.Type + }{ + "base": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.ListNestedBlock{ + // 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.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go new file mode 100644 index 000000000..c99ee4c3c --- /dev/null +++ b/resource/schema/map_attribute.go @@ -0,0 +1,223 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = MapAttribute{} + _ fwxschema.AttributeWithMapPlanModifiers = MapAttribute{} + _ fwxschema.AttributeWithMapValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Map + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Map +} + +// 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 returns the DeprecationMessage field value. +func (a MapAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a MapAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a MapAttribute) IsSensitive() bool { + return a.Sensitive +} + +// MapPlanModifiers returns the PlanModifiers field value. +func (a MapAttribute) MapPlanModifiers() []planmodifier.Map { + return a.PlanModifiers +} + +// MapValidators returns the Validators field value. +func (a MapAttribute) MapValidators() []validator.Map { + return a.Validators +} diff --git a/resource/schema/map_attribute_test.go b/resource/schema/map_attribute_test.go new file mode 100644 index 000000000..6204e8271 --- /dev/null +++ b/resource/schema/map_attribute_test.go @@ -0,0 +1,460 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.MapAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapType"), + }, + "ElementKeyInt": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapType"), + }, + "ElementKeyString": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.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 schema.MapAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.MapAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.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 schema.MapAttribute + expected string + }{ + "no-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.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 schema.MapAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.MapAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: types.MapType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.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 schema.MapAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.MapAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.MapAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.MapAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.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 schema.MapAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.MapAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestMapAttributeMapPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected []planmodifier.Map + }{ + "no-planmodifiers": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.MapAttribute{ + PlanModifiers: []planmodifier.Map{}, + }, + expected: []planmodifier.Map{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} +func TestMapAttributeMapValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.MapAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go new file mode 100644 index 000000000..ac0df6a05 --- /dev/null +++ b/resource/schema/map_nested_attribute.go @@ -0,0 +1,247 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = MapNestedAttribute{} + _ fwxschema.AttributeWithMapPlanModifiers = MapNestedAttribute{} + _ fwxschema.AttributeWithMapValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Map + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Map +} + +// 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 returns the DeprecationMessage field value. +func (a MapNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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.NestingModeList +} + +// 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 returns the Computed field value. +func (a MapNestedAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a MapNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// MapPlanModifiers returns the PlanModifiers field value. +func (a MapNestedAttribute) MapPlanModifiers() []planmodifier.Map { + return a.PlanModifiers +} + +// MapValidators returns the Validators field value. +func (a MapNestedAttribute) MapValidators() []validator.Map { + return a.Validators +} diff --git a/resource/schema/map_nested_attribute_test.go b/resource/schema/map_nested_attribute_test.go new file mode 100644 index 000000000..c12007dc0 --- /dev/null +++ b/resource/schema/map_nested_attribute_test.go @@ -0,0 +1,623 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.MapNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.MapNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.MapNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-attributes": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.MapNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.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 schema.MapNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.MapNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.MapNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.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 schema.MapNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.MapNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.MapNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.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 schema.MapNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.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 schema.MapNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.MapNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestMapNestedAttributeMapNestedPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected []planmodifier.Map + }{ + "no-planmodifiers": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "planmodifiers": { + attribute: schema.MapNestedAttribute{ + PlanModifiers: []planmodifier.Map{}, + }, + expected: []planmodifier.Map{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeMapNestedValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.MapNestedAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/nested_attribute.go b/resource/schema/nested_attribute.go new file mode 100644 index 000000000..5429975da --- /dev/null +++ b/resource/schema/nested_attribute.go @@ -0,0 +1,11 @@ +package schema + +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/resource/schema/nested_attribute_object.go b/resource/schema/nested_attribute_object.go new file mode 100644 index 000000000..6fe14036a --- /dev/null +++ b/resource/schema/nested_attribute_object.go @@ -0,0 +1,105 @@ +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ fwxschema.NestedAttributeObjectWithPlanModifiers = NestedAttributeObject{} + _ fwxschema.NestedAttributeObjectWithValidators = 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 + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Object +} + +// 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) +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (o NestedAttributeObject) ObjectPlanModifiers() []planmodifier.Object { + return o.PlanModifiers +} + +// ObjectValidators returns the Validators field value. +func (o NestedAttributeObject) ObjectValidators() []validator.Object { + return o.Validators +} + +// 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/resource/schema/nested_attribute_object_test.go b/resource/schema/nested_attribute_object_test.go new file mode 100644 index 000000000..6c0843811 --- /dev/null +++ b/resource/schema/nested_attribute_object_test.go @@ -0,0 +1,314 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.NestedAttributeObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on NestedAttributeObject"), + }, + "ElementKeyInt": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedAttributeObject"), + }, + "ElementKeyString": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedAttributeObject"), + }, + "ElementKeyValue": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.NestedAttributeObject + other fwschema.NestedAttributeObject + expected bool + }{ + "different-attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.NestedAttributeObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedAttributeObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.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 TestNestedAttributeObjectObjectPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedAttributeObject + expected []planmodifier.Object + }{ + "no-planmodifiers": { + attribute: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "planmodifiers": { + attribute: schema.NestedAttributeObject{ + PlanModifiers: []planmodifier.Object{}, + }, + expected: []planmodifier.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} +func TestNestedAttributeObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedAttributeObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedAttributeObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + 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 schema.NestedAttributeObject + expected attr.Type + }{ + "base": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + // "custom-type": { + // block: schema.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/resource/schema/nested_block_object.go b/resource/schema/nested_block_object.go new file mode 100644 index 000000000..11d09ee6f --- /dev/null +++ b/resource/schema/nested_block_object.go @@ -0,0 +1,117 @@ +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ fwxschema.NestedBlockObjectWithPlanModifiers = NestedBlockObject{} + _ fwxschema.NestedBlockObjectWithValidators = NestedBlockObject{} +) + +// NestedBlockObject is the object containing the underlying attributes and +// blocks for a ListNestedBlock or SetNestedBlock. When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. +// +// This object enables customizing and simplifying details within its parent +// Block, therefore it cannot have Terraform schema fields such as Description, +// etc. +type NestedBlockObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // 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 + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObject is equivalent. +func (o NestedBlockObject) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObject); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObject) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(o.Blocks) +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (o NestedBlockObject) ObjectPlanModifiers() []planmodifier.Object { + return o.PlanModifiers +} + +// ObjectValidators returns the Validators field value. +func (o NestedBlockObject) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedBlockObject. +func (o NestedBlockObject) Type() types.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedBlockObjectType(o) +} diff --git a/resource/schema/nested_block_object_test.go b/resource/schema/nested_block_object_test.go new file mode 100644 index 000000000..7dc79de49 --- /dev/null +++ b/resource/schema/nested_block_object_test.go @@ -0,0 +1,401 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on NestedBlockObject"), + }, + "ElementKeyInt": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedBlockObject"), + }, + "ElementKeyString": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedBlockObject"), + }, + "ElementKeyValue": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedBlockObject"), + }, + } + + 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 TestNestedBlockObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + other fwschema.NestedBlockObject + expected bool + }{ + "different-attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 TestNestedBlockObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedBlockObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.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 TestNestedBlockObjectGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected map[string]fwschema.Block + }{ + "no-blocks": { + object: schema.NestedBlockObject{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectObjectPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedBlockObject + expected []planmodifier.Object + }{ + "no-planmodifiers": { + attribute: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "planmodifiers": { + attribute: schema.NestedBlockObject{ + PlanModifiers: []planmodifier.Object{}, + }, + expected: []planmodifier.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedBlockObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedBlockObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected attr.Type + }{ + "base": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.NestedBlockObject{ + // 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/resource/schema/number_attribute.go b/resource/schema/number_attribute.go new file mode 100644 index 000000000..4e720224d --- /dev/null +++ b/resource/schema/number_attribute.go @@ -0,0 +1,211 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = NumberAttribute{} + _ fwxschema.AttributeWithNumberPlanModifiers = NumberAttribute{} + _ fwxschema.AttributeWithNumberValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Number + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Number +} + +// 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 returns the DeprecationMessage field value. +func (a NumberAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a NumberAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a NumberAttribute) IsSensitive() bool { + return a.Sensitive +} + +// NumberPlanModifiers returns the PlanModifiers field value. +func (a NumberAttribute) NumberPlanModifiers() []planmodifier.Number { + return a.PlanModifiers +} + +// NumberValidators returns the Validators field value. +func (a NumberAttribute) NumberValidators() []validator.Number { + return a.Validators +} diff --git a/resource/schema/number_attribute_test.go b/resource/schema/number_attribute_test.go new file mode 100644 index 000000000..c4ad986af --- /dev/null +++ b/resource/schema/number_attribute_test.go @@ -0,0 +1,457 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.NumberType"), + }, + "ElementKeyInt": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.NumberType"), + }, + "ElementKeyString": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.NumberType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.NumberAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: schema.NumberAttribute{}, + other: schema.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 schema.NumberAttribute + expected string + }{ + "no-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: schema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: schema.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 schema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.NumberAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "required": { + attribute: schema.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 schema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.NumberAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestNumberAttributeNumberPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected []planmodifier.Number + }{ + "no-planmodifiers": { + attribute: schema.NumberAttribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.NumberAttribute{ + PlanModifiers: []planmodifier.Number{}, + }, + expected: []planmodifier.Number{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.NumberPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeNumberValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected []validator.Number + }{ + "no-validators": { + attribute: schema.NumberAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.NumberAttribute{ + Validators: []validator.Number{}, + }, + expected: []validator.Number{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.NumberValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go new file mode 100644 index 000000000..cf3e8409c --- /dev/null +++ b/resource/schema/object_attribute.go @@ -0,0 +1,222 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ObjectAttribute{} + _ fwxschema.AttributeWithObjectPlanModifiers = ObjectAttribute{} + _ fwxschema.AttributeWithObjectValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Object +} + +// 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 returns the DeprecationMessage field value. +func (a ObjectAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a ObjectAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a ObjectAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (a ObjectAttribute) ObjectPlanModifiers() []planmodifier.Object { + return a.PlanModifiers +} + +// ObjectValidators returns the Validators field value. +func (a ObjectAttribute) ObjectValidators() []validator.Object { + return a.Validators +} diff --git a/resource/schema/object_attribute_test.go b/resource/schema/object_attribute_test.go new file mode 100644 index 000000000..744b11a7c --- /dev/null +++ b/resource/schema/object_attribute_test.go @@ -0,0 +1,467 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.ObjectAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("testattr"), + expected: types.StringType, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.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: schema.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: schema.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: schema.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 schema.ObjectAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ObjectAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.ObjectAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attribute-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.BoolType}}, + expected: false, + }, + "equal": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.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 schema.ObjectAttribute + expected string + }{ + "no-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "description": { + attribute: schema.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 schema.ObjectAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.ObjectAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, + }, + // "custom-type": { + // attribute: schema.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 schema.ObjectAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "computed": { + attribute: schema.ObjectAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.ObjectAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.ObjectAttribute + expected bool + }{ + "not-required": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "required": { + attribute: schema.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 schema.ObjectAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "sensitive": { + attribute: schema.ObjectAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestObjectAttributeObjectPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected []planmodifier.Object + }{ + "no-planmodifiers": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.ObjectAttribute{ + PlanModifiers: []planmodifier.Object{}, + }, + expected: []planmodifier.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: nil, + }, + "validators": { + attribute: schema.ObjectAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/schema.go b/resource/schema/schema.go new file mode 100644 index 000000000..ce19d9c78 --- /dev/null +++ b/resource/schema/schema.go @@ -0,0 +1,155 @@ +package schema + +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 resource data. This type +// is used as the resource.SchemaResponse type Schema field, which is +// implemented by the resource.DataSource type Schema 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 + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this resource 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 resource is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this resource. The warning diagnostic + // summary is automatically set to "Resource Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Use examplecloud_other resource instead. This resource + // will be removed in the next major version of the provider." + // - "Remove this resource as it no longer is valid and + // will be removed in the next major version of the provider." + // + DeprecationMessage string + + // Version indicates the current version of the resource schema. Resource + // schema versioning enables state upgrades in conjunction with the + // [resource.ResourceWithStateUpgrades] interface. Versioning is only + // required if there is a breaking change involving existing state data, + // such as changing an attribute or block type in a manner that is + // incompatible with the Terraform type. + // + // Versions are conventionally only incremented by one each release. + Version int64 +} + +// 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 returns the Blocks field value. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(s.Blocks) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (s Schema) GetDeprecationMessage() string { + return s.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (s Schema) GetDescription() string { + return s.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (s Schema) GetMarkdownDescription() string { + return s.MarkdownDescription +} + +// GetVersion returns the Version field value. +func (s Schema) GetVersion() int64 { + return s.Version +} + +// 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 resource 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 +} + +// schemaBlocks is a resource to fwschema type conversion function. +func schemaBlocks(blocks map[string]Block) map[string]fwschema.Block { + result := make(map[string]fwschema.Block, len(blocks)) + + for name, block := range blocks { + result[name] = block + } + + return result +} diff --git a/resource/schema/schema_test.go b/resource/schema/schema_test.go new file mode 100644 index 000000000..2383d6036 --- /dev/null +++ b/resource/schema/schema_test.go @@ -0,0 +1,1006 @@ +package schema_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/resource/schema" + "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 schema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: schema.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 schema.Schema", + ), + }, + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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 schema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("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: "+fwschema.ErrPathIsBlock.Error(), + ), + }, + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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 schema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "empty-nil": { + schema: schema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "nil": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: nil, + expectedErr: fwschema.ErrPathIsBlock.Error(), + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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 schema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: schema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.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 schema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: schema.Schema{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + 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 schema.Schema + expected string + }{ + "no-deprecation-message": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + schema: schema.Schema{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Schema + expected string + }{ + "no-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + schema: schema.Schema{ + 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.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 schema.Schema + expected string + }{ + "no-markdown-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + schema: schema.Schema{ + 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.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 schema.Schema + expected int64 + }{ + "no-version": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + "version": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Version: 1, + }, + expected: 1, + }, + } + + 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 schema.Schema + expected attr.Type + }{ + "base": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": 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 schema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_block_nested": schema.StringAttribute{}, + }, + }, + }, + "set_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_block_nested": schema.StringAttribute{}, + }, + }, + }, + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("list_block"), + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list_block_nested": types.StringType, + }, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.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: schema.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: schema.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: schema.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 schema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: schema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_block_nested": schema.StringAttribute{}, + }, + }, + }, + "set_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_block_nested": schema.StringAttribute{}, + }, + }, + }, + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("list_block"), + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list_block_nested": types.StringType, + }, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.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: schema.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: schema.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: schema.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/resource/schema/set_attribute.go b/resource/schema/set_attribute.go new file mode 100644 index 000000000..820e32011 --- /dev/null +++ b/resource/schema/set_attribute.go @@ -0,0 +1,218 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = SetAttribute{} + _ fwxschema.AttributeWithSetPlanModifiers = SetAttribute{} + _ fwxschema.AttributeWithSetValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Set +} + +// 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 returns the DeprecationMessage field value. +func (a SetAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a SetAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a SetAttribute) IsSensitive() bool { + return a.Sensitive +} + +// SetPlanModifiers returns the PlanModifiers field value. +func (a SetAttribute) SetPlanModifiers() []planmodifier.Set { + return a.PlanModifiers +} + +// SetValidators returns the Validators field value. +func (a SetAttribute) SetValidators() []validator.Set { + return a.Validators +} diff --git a/resource/schema/set_attribute_test.go b/resource/schema/set_attribute_test.go new file mode 100644 index 000000000..54a63595b --- /dev/null +++ b/resource/schema/set_attribute_test.go @@ -0,0 +1,461 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.SetAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetType"), + }, + "ElementKeyInt": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetType"), + }, + "ElementKeyString": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.SetAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.SetAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.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 schema.SetAttribute + expected string + }{ + "no-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.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 schema.SetAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.SetAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: types.SetType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.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 schema.SetAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.SetAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.SetAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.SetAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.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 schema.SetAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.SetAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestSetAttributeSetPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected []planmodifier.Set + }{ + "no-planmodifiers": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.SetAttribute{ + PlanModifiers: []planmodifier.Set{}, + }, + expected: []planmodifier.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.SetAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go new file mode 100644 index 000000000..d9c1813aa --- /dev/null +++ b/resource/schema/set_nested_attribute.go @@ -0,0 +1,242 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SetNestedAttribute{} + _ fwxschema.AttributeWithSetPlanModifiers = SetNestedAttribute{} + _ fwxschema.AttributeWithSetValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Set +} + +// 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 returns the DeprecationMessage field value. +func (a SetNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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.NestingModeList +} + +// 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 returns the Computed field value. +func (a SetNestedAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a SetNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// SetPlanModifiers returns the PlanModifiers field value. +func (a SetNestedAttribute) SetPlanModifiers() []planmodifier.Set { + return a.PlanModifiers +} + +// SetValidators returns the Validators field value. +func (a SetNestedAttribute) SetValidators() []validator.Set { + return a.Validators +} diff --git a/resource/schema/set_nested_attribute_test.go b/resource/schema/set_nested_attribute_test.go new file mode 100644 index 000000000..a460fd694 --- /dev/null +++ b/resource/schema/set_nested_attribute_test.go @@ -0,0 +1,623 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.SetNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.SetNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.SetNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-attributes": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.SetNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.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 schema.SetNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.SetNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.SetNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.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 schema.SetNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.SetNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.SetNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.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 schema.SetNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.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 schema.SetNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.SetNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestSetNestedAttributeSetPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected []planmodifier.Set + }{ + "no-planmodifiers": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "planmodifiers": { + attribute: schema.SetNestedAttribute{ + PlanModifiers: []planmodifier.Set{}, + }, + expected: []planmodifier.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SetNestedAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/set_nested_block.go b/resource/schema/set_nested_block.go new file mode 100644 index 000000000..b0da55236 --- /dev/null +++ b/resource/schema/set_nested_block.go @@ -0,0 +1,224 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = SetNestedBlock{} + _ fwxschema.BlockWithSetPlanModifiers = SetNestedBlock{} + _ fwxschema.BlockWithSetValidators = SetNestedBlock{} +) + +// SetNestedBlock represents a block that is a set of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer SetNestedAttribute over SetNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # set of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a set of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SetNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + NestedObject NestedBlockObject + + // 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Set +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyValue, otherwise returns an error. +func (b SetNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is SetNestedBlock +// and all fields are equal. +func (b SetNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SetNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SetNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SetNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SetNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems always returns 0. +// +// Deprecated: This method will be removed in the future. Use validators +// instead. +func (b SetNestedBlock) GetMaxItems() int64 { + return 0 +} + +// GetMinItems always returns 0. +// +// Deprecated: This method will be removed in the future. Use validators +// instead. +func (b SetNestedBlock) GetMinItems() int64 { + return 0 +} + +// GetNestedObject returns the NestedObject field value. +func (b SetNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeSet. +func (b SetNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSet +} + +// SetPlanModifiers returns the PlanModifiers field value. +func (b SetNestedBlock) SetPlanModifiers() []planmodifier.Set { + return b.PlanModifiers +} + +// SetValidators returns the Validators field value. +func (b SetNestedBlock) SetValidators() []validator.Set { + return b.Validators +} + +// Type returns SetType of ObjectType or CustomType. +func (b SetNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.SetType{ + ElemType: b.NestedObject.Type(), + } +} diff --git a/resource/schema/set_nested_block_test.go b/resource/schema/set_nested_block_test.go new file mode 100644 index 000000000..71f3ae6b3 --- /dev/null +++ b/resource/schema/set_nested_block_test.go @@ -0,0 +1,543 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedBlock"), + }, + "ElementKeyString": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.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 TestSetNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SetNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithSetValidators{}, + expected: false, + }, + "different-attributes": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.SetNestedBlock{ + 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.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SetNestedBlock{ + 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.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetMaxItems(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected int64 + }{ + "0": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMaxItems() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetMinItems(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected int64 + }{ + "0": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMinItems() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockSetPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected []planmodifier.Set + }{ + "no-planmodifiers": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "planmodifiers": { + block: schema.SetNestedBlock{ + PlanModifiers: []planmodifier.Set{}, + }, + expected: []planmodifier.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.SetPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected []validator.Set + }{ + "no-validators": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SetNestedBlock{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SetNestedBlock{ + // 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.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go new file mode 100644 index 000000000..9fdffc6cc --- /dev/null +++ b/resource/schema/single_nested_attribute.go @@ -0,0 +1,263 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SingleNestedAttribute{} + _ fwxschema.AttributeWithObjectPlanModifiers = SingleNestedAttribute{} + _ fwxschema.AttributeWithObjectValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Object +} + +// 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 returns the DeprecationMessage field value. +func (a SingleNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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, CustomType, and Validators field values. +func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return NestedAttributeObject{ + Attributes: a.Attributes, + CustomType: a.CustomType, + Validators: a.Validators, + } +} + +// GetNestingMode always returns NestingModeList. +func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeList +} + +// 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 returns the Computed field value. +func (a SingleNestedAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a SingleNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (a SingleNestedAttribute) ObjectPlanModifiers() []planmodifier.Object { + return a.PlanModifiers +} + +// ObjectValidators returns the Validators field value. +func (a SingleNestedAttribute) ObjectValidators() []validator.Object { + return a.Validators +} diff --git a/resource/schema/single_nested_attribute_test.go b/resource/schema/single_nested_attribute_test.go new file mode 100644 index 000000000..b9ad563c9 --- /dev/null +++ b/resource/schema/single_nested_attribute_test.go @@ -0,0 +1,587 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.SingleNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on SingleNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.SingleNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attributes": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.SingleNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SingleNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.SingleNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + attribute: schema.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 schema.SingleNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.SingleNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.SingleNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + // "custom-type": { + // attribute: schema.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 schema.SingleNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.SingleNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.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 schema.SingleNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "required": { + attribute: schema.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 schema.SingleNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.SingleNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestSingleNestedAttributeObjectPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected []planmodifier.Object + }{ + "no-planmodifiers": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "planmodifiers": { + attribute: schema.SingleNestedAttribute{ + PlanModifiers: []planmodifier.Object{}, + }, + expected: []planmodifier.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SingleNestedAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/single_nested_block.go b/resource/schema/single_nested_block.go new file mode 100644 index 000000000..17c3d1a89 --- /dev/null +++ b/resource/schema/single_nested_block.go @@ -0,0 +1,247 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = SingleNestedBlock{} + _ fwxschema.BlockWithObjectPlanModifiers = SingleNestedBlock{} + _ fwxschema.BlockWithObjectValidators = SingleNestedBlock{} +) + +// SingleNestedBlock represents a block that is a single object where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Object +// as the value type unless the CustomType field is set. +// +// Prefer SingleNestedAttribute over SingleNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block only once using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # single block +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_block.nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SingleNestedBlock struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Object +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (b SingleNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedBlock", step) + } + + if attribute, ok := b.Attributes[string(name)]; ok { + return attribute, nil + } + + if block, ok := b.Blocks[string(name)]; ok { + return block, nil + } + + return nil, fmt.Errorf("no attribute or block %q on SingleNestedBlock", name) +} + +// Equal returns true if the given Attribute is b SingleNestedBlock +// and all fields are equal. +func (b SingleNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SingleNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SingleNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SingleNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SingleNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems always returns 0. +// +// Deprecated: This method will be removed in the future. +func (b SingleNestedBlock) GetMaxItems() int64 { + return 0 +} + +// GetMinItems always returns 0. +// +// Deprecated: This method will be removed in the future. +func (b SingleNestedBlock) GetMinItems() int64 { + return 0 +} + +// GetNestedObject returns a generated NestedBlockObject from the +// Attributes, CustomType, and Validators field values. +func (b SingleNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + CustomType: b.CustomType, + Validators: b.Validators, + } +} + +// GetNestingMode always returns BlockNestingModeSingle. +func (b SingleNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSingle +} + +// ObjectPlanModifiers returns the PlanModifiers field value. +func (b SingleNestedBlock) ObjectPlanModifiers() []planmodifier.Object { + return b.PlanModifiers +} + +// ObjectValidators returns the Validators field value. +func (b SingleNestedBlock) ObjectValidators() []validator.Object { + return b.Validators +} + +// Type returns ObjectType or CustomType. +func (b SingleNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + attrTypes := make(map[string]attr.Type, len(b.Attributes)+len(b.Blocks)) + + for name, attribute := range b.Attributes { + attrTypes[name] = attribute.GetType() + } + + for name, block := range b.Blocks { + attrTypes[name] = block.Type() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/resource/schema/single_nested_block_test.go b/resource/schema/single_nested_block_test.go new file mode 100644 index 000000000..7cf0eb82b --- /dev/null +++ b/resource/schema/single_nested_block_test.go @@ -0,0 +1,543 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on SingleNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedBlock"), + }, + "ElementKeyString": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedBlock"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.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 TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SingleNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.BlockWithObjectValidators{}, + expected: false, + }, + "different-attributes": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + block: schema.SingleNestedBlock{ + 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.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SingleNestedBlock{ + 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.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetMaxItems(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected int64 + }{ + "0": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMaxItems() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetMinItems(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected int64 + }{ + "0": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMinItems() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockObjectPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected []planmodifier.Object + }{ + "no-planmodifiers": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "planmodifiers": { + block: schema.SingleNestedBlock{ + PlanModifiers: []planmodifier.Object{}, + }, + expected: []planmodifier.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ObjectPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected []validator.Object + }{ + "no-validators": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SingleNestedBlock{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SingleNestedBlock{ + // 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.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go new file mode 100644 index 000000000..bd59176c0 --- /dev/null +++ b/resource/schema/string_attribute.go @@ -0,0 +1,207 @@ +package schema + +import ( + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = StringAttribute{} + _ fwxschema.AttributeWithStringPlanModifiers = StringAttribute{} + _ fwxschema.AttributeWithStringValidators = 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 + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.String + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.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 returns the DeprecationMessage field value. +func (a StringAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// 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 returns the Computed field value. +func (a StringAttribute) IsComputed() bool { + return a.Computed +} + +// 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 returns the Sensitive field value. +func (a StringAttribute) IsSensitive() bool { + return a.Sensitive +} + +// StringPlanModifiers returns the PlanModifiers field value. +func (a StringAttribute) StringPlanModifiers() []planmodifier.String { + return a.PlanModifiers +} + +// StringValidators returns the Validators field value. +func (a StringAttribute) StringValidators() []validator.String { + return a.Validators +} diff --git a/resource/schema/string_attribute_test.go b/resource/schema/string_attribute_test.go new file mode 100644 index 000000000..d1a9115b6 --- /dev/null +++ b/resource/schema/string_attribute_test.go @@ -0,0 +1,457 @@ +package schema_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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 schema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to types.StringType"), + }, + "ElementKeyInt": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to types.StringType"), + }, + "ElementKeyString": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to types.StringType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.StringAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: schema.StringAttribute{}, + other: schema.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 schema.StringAttribute + expected string + }{ + "no-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.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 schema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: schema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: schema.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 schema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.StringAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.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 schema.StringAttribute + expected bool + }{ + "not-required": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "required": { + attribute: schema.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 schema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.StringAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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) + } + }) + } +} + +func TestStringAttributeStringPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []planmodifier.String + }{ + "no-planmodifiers": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.StringAttribute{ + PlanModifiers: []planmodifier.String{}, + }, + expected: []planmodifier.String{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeStringValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []validator.String + }{ + "no-validators": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.StringAttribute{ + Validators: []validator.String{}, + }, + expected: []validator.String{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 6e0093809c4f6b106cba8c3ca2b4f82ad81884ce Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 29 Nov 2022 08:57:19 -0500 Subject: [PATCH 4/4] Update CHANGELOG for #558 --- .changelog/{pending.txt => 558.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 558.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/558.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/558.txt