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 +}