From 28f480428f577ed1f440209145938d526b82a1e6 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 29 Nov 2022 08:55:08 -0500 Subject: [PATCH] resource/schema/planmodifier: New type-specific plan modifiers package (#557) 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/557.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/557.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/557.txt b/.changelog/557.txt new file mode 100644 index 000000000..2a47657bb --- /dev/null +++ b/.changelog/557.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 +}