diff --git a/internal/command/jsonformat/change/block.go b/internal/command/jsonformat/change/block.go new file mode 100644 index 0000000000..5eab3aad61 --- /dev/null +++ b/internal/command/jsonformat/change/block.go @@ -0,0 +1,126 @@ +package change + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/plans" +) + +var ( + importantAttributes = []string{ + "id", + } +) + +func importantAttribute(attr string) bool { + for _, attribute := range importantAttributes { + if attribute == attr { + return true + } + } + return false +} + +func Block(attributes map[string]Change, blocks map[string][]Change) Renderer { + maximumKeyLen := 0 + for key := range attributes { + if len(key) > maximumKeyLen { + maximumKeyLen = len(key) + } + } + + return &blockRenderer{ + attributes: attributes, + blocks: blocks, + maximumKeyLen: maximumKeyLen, + } +} + +type blockRenderer struct { + NoWarningsRenderer + + attributes map[string]Change + blocks map[string][]Change + maximumKeyLen int +} + +func (renderer blockRenderer) Render(change Change, indent int, opts RenderOpts) string { + unchangedAttributes := 0 + unchangedBlocks := 0 + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("{%s\n", change.forcesReplacement())) + for _, importantKey := range importantAttributes { + if attribute, ok := renderer.attributes[importantKey]; ok { + if attribute.action == plans.NoOp { + buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", change.indent(indent+1), attribute.emptySymbol(), renderer.maximumKeyLen, importantKey, attribute.Render(indent+1, opts))) + continue + } + buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", change.indent(indent+1), format.DiffActionSymbol(attribute.action), renderer.maximumKeyLen, importantKey, attribute.Render(indent+1, opts))) + } + } + + var attributeKeys []string + for key := range renderer.attributes { + attributeKeys = append(attributeKeys, key) + } + sort.Strings(attributeKeys) + + for _, key := range attributeKeys { + if importantAttribute(key) { + continue + } + attribute := renderer.attributes[key] + if attribute.action == plans.NoOp && !opts.showUnchangedChildren { + unchangedAttributes++ + continue + } + + for _, warning := range attribute.Warnings(indent + 1) { + buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", change.indent(indent+1), format.DiffActionSymbol(attribute.action), renderer.maximumKeyLen, key, attribute.Render(indent+1, opts))) + } + + if unchangedAttributes > 0 { + buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("attribute", unchangedAttributes))) + } + + var blockKeys []string + for key := range renderer.blocks { + blockKeys = append(blockKeys, key) + } + sort.Strings(blockKeys) + + for _, key := range blockKeys { + blocks := renderer.blocks[key] + + foundChangedBlock := false + for _, block := range blocks { + if block.action == plans.NoOp && !opts.showUnchangedChildren { + unchangedBlocks++ + continue + } + + if !foundChangedBlock && len(renderer.attributes) > 0 { + buf.WriteString("\n") + foundChangedBlock = true + } + + for _, warning := range block.Warnings(indent + 1) { + buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s %s %s\n", change.indent(indent+1), format.DiffActionSymbol(block.action), key, block.Render(indent+1, opts))) + } + } + + if unchangedBlocks > 0 { + buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("block", unchangedBlocks))) + } + + buf.WriteString(fmt.Sprintf("%s%s }", change.indent(indent), change.emptySymbol())) + return buf.String() +} diff --git a/internal/command/jsonformat/change/list.go b/internal/command/jsonformat/change/list.go index 7ac66be735..c900c5b48d 100644 --- a/internal/command/jsonformat/change/list.go +++ b/internal/command/jsonformat/change/list.go @@ -74,7 +74,7 @@ func (renderer listRenderer) Render(change Change, indent int, opts RenderOpts) // what happens here. if len(unchangedElements) > 0 { lastElement := unchangedElements[len(unchangedElements)-1] - buf.WriteString(fmt.Sprintf("%s %s,\n", change.indent(indent+1), lastElement.Render(indent+1, unchangedElementOpts))) + buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), lastElement.emptySymbol(), lastElement.Render(indent+1, unchangedElementOpts))) } // We now reset the unchanged elements list, we've printed out a // count of all the elements we skipped so we start counting from @@ -96,7 +96,7 @@ func (renderer listRenderer) Render(change Change, indent int, opts RenderOpts) buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning)) } if element.action == plans.NoOp { - buf.WriteString(fmt.Sprintf("%s %s,\n", change.indent(indent+1), element.Render(indent+1, unchangedElementOpts))) + buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), element.emptySymbol(), element.Render(indent+1, unchangedElementOpts))) } else { buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), format.DiffActionSymbol(element.action), element.Render(indent+1, elementOpts))) } diff --git a/internal/command/jsonformat/change/renderer_test.go b/internal/command/jsonformat/change/renderer_test.go index 2548e5fb35..15f89e4044 100644 --- a/internal/command/jsonformat/change/renderer_test.go +++ b/internal/command/jsonformat/change/renderer_test.go @@ -1157,6 +1157,320 @@ func TestRenderers(t *testing.T) { ] `, }, + "create_empty_block": { + change: Change{ + renderer: Block(nil, nil), + action: plans.Create, + }, + expected: ` +{ + }`, + }, + "create_populated_block": { + change: Change{ + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"root\"")), + action: plans.Create, + }, + "boolean": { + renderer: Primitive(nil, strptr("true")), + action: plans.Create, + }, + }, map[string][]Change{ + "nested_block": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"one\"")), + action: plans.Create, + }, + }, nil), + action: plans.Create, + }, + }, + "nested_block_two": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"two\"")), + action: plans.Create, + }, + }, nil), + action: plans.Create, + }, + }, + }), + action: plans.Create, + }, + expected: ` +{ + + boolean = true + + string = "root" + + + nested_block { + + string = "one" + } + + + nested_block_two { + + string = "two" + } + }`, + }, + "update_empty_block": { + change: Change{ + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"root\"")), + action: plans.Create, + }, + "boolean": { + renderer: Primitive(nil, strptr("true")), + action: plans.Create, + }, + }, map[string][]Change{ + "nested_block": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"one\"")), + action: plans.Create, + }, + }, nil), + action: plans.Create, + }, + }, + "nested_block_two": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"two\"")), + action: plans.Create, + }, + }, nil), + action: plans.Create, + }, + }, + }), + action: plans.Update, + }, + expected: ` +{ + + boolean = true + + string = "root" + + + nested_block { + + string = "one" + } + + + nested_block_two { + + string = "two" + } + }`, + }, + "update_populated_block": { + change: Change{ + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"root\"")), + action: plans.Create, + }, + "boolean": { + renderer: Primitive(strptr("false"), strptr("true")), + action: plans.Update, + }, + }, map[string][]Change{ + "nested_block": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"one\"")), + action: plans.NoOp, + }, + }, nil), + action: plans.NoOp, + }, + }, + "nested_block_two": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(nil, strptr("\"two\"")), + action: plans.Create, + }, + }, nil), + action: plans.Create, + }, + }, + }), + action: plans.Update, + }, + expected: ` +{ + ~ boolean = false -> true + + string = "root" + + + nested_block_two { + + string = "two" + } + # (1 unchanged block hidden) + }`, + }, + "clear_populated_block": { + change: Change{ + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"root\""), nil), + action: plans.Delete, + }, + "boolean": { + renderer: Primitive(strptr("true"), nil), + action: plans.Delete, + }, + }, map[string][]Change{ + "nested_block": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"one\""), nil), + action: plans.Delete, + }, + }, nil), + action: plans.Delete, + }, + }, + "nested_block_two": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"two\""), nil), + action: plans.Delete, + }, + }, nil), + action: plans.Delete, + }, + }, + }), + action: plans.Update, + }, + expected: ` +{ + - boolean = true -> null + - string = "root" -> null + + - nested_block { + - string = "one" -> null + } + + - nested_block_two { + - string = "two" -> null + } + }`, + }, + "delete_populated_block": { + change: Change{ + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"root\""), nil), + action: plans.Delete, + }, + "boolean": { + renderer: Primitive(strptr("true"), nil), + action: plans.Delete, + }, + }, map[string][]Change{ + "nested_block": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"one\""), nil), + action: plans.Delete, + }, + }, nil), + action: plans.Delete, + }, + }, + "nested_block_two": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"two\""), nil), + action: plans.Delete, + }, + }, nil), + action: plans.Delete, + }, + }, + }), + action: plans.Delete, + }, + expected: ` +{ + - boolean = true -> null + - string = "root" -> null + + - nested_block { + - string = "one" -> null + } + + - nested_block_two { + - string = "two" -> null + } + }`, + }, + "delete_empty_block": { + change: Change{ + renderer: Block(nil, nil), + action: plans.Delete, + }, + expected: ` +{ + }`, + }, + "block_always_includes_important_attributes": { + change: Change{ + renderer: Block(map[string]Change{ + "id": { + renderer: Primitive(strptr("\"root\""), strptr("\"root\"")), + action: plans.NoOp, + }, + "boolean": { + renderer: Primitive(strptr("false"), strptr("false")), + action: plans.NoOp, + }, + }, map[string][]Change{ + "nested_block": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"one\""), strptr("\"one\"")), + action: plans.NoOp, + }, + }, nil), + action: plans.NoOp, + }, + }, + "nested_block_two": { + { + renderer: Block(map[string]Change{ + "string": { + renderer: Primitive(strptr("\"two\""), strptr("\"two\"")), + action: plans.NoOp, + }, + }, nil), + action: plans.NoOp, + }, + }, + }), + action: plans.NoOp, + }, + expected: ` +{ + id = "root" + # (1 unchanged attribute hidden) + # (2 unchanged blocks hidden) + }`, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { diff --git a/internal/command/jsonformat/change/testing.go b/internal/command/jsonformat/change/testing.go index 51e2536b3c..11368eefc7 100644 --- a/internal/command/jsonformat/change/testing.go +++ b/internal/command/jsonformat/change/testing.go @@ -186,7 +186,60 @@ func ValidateSet(elements []ValidateChangeFunc, action plans.Action, replace boo for ix := 0; ix < len(elements); ix++ { elements[ix](t, set.elements[ix]) } + } +} + +func ValidateBlock(attributes map[string]ValidateChangeFunc, blocks map[string][]ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc { + return func(t *testing.T, change Change) { + validateChange(t, change, action, replace) + + block, ok := change.renderer.(*blockRenderer) + if !ok { + t.Fatalf("invalid renderer type: %T", change.renderer) + } + if len(block.attributes) != len(attributes) || len(block.blocks) != len(blocks) { + t.Fatalf("expected %d attributes and %d blocks but found %d attributes and %d blocks", len(attributes), len(blocks), len(block.attributes), len(block.blocks)) + } + + var missingAttributes []string + var missingBlocks []string + + for key, expected := range attributes { + actual, ok := block.attributes[key] + if !ok { + missingAttributes = append(missingAttributes, key) + } + + if len(missingAttributes) > 0 { + continue + } + + expected(t, actual) + } + + for key, expected := range blocks { + actual, ok := block.blocks[key] + if !ok { + missingBlocks = append(missingBlocks, key) + } + + if len(missingAttributes) > 0 || len(missingBlocks) > 0 { + continue + } + + if len(expected) != len(actual) { + t.Fatalf("expected %d blocks for %s but found %d", len(expected), key, len(actual)) + } + + for ix := range expected { + expected[ix](t, actual[ix]) + } + } + + if len(missingAttributes) > 0 || len(missingBlocks) > 0 { + t.Fatalf("missing the following attributes: %s, and the following blocks: %s", strings.Join(missingAttributes, ", "), strings.Join(missingBlocks, ", ")) + } } } diff --git a/internal/command/jsonformat/differ/block.go b/internal/command/jsonformat/differ/block.go index 3b0dea8461..b39fbfd1f2 100644 --- a/internal/command/jsonformat/differ/block.go +++ b/internal/command/jsonformat/differ/block.go @@ -3,8 +3,54 @@ package differ import ( "github.com/hashicorp/terraform/internal/command/jsonformat/change" "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" ) -func (v Value) ComputeChangeForBlock(block *jsonprovider.Block) change.Change { - panic("not implemented") +func (v Value) computeChangeForBlock(block *jsonprovider.Block) change.Change { + current := v.getDefaultActionForIteration() + + blockValue := v.asMap() + + attributes := make(map[string]change.Change) + for key, attr := range block.Attributes { + childValue := blockValue.getChild(key) + childChange := childValue.ComputeChange(attr) + if childChange.GetAction() == plans.NoOp && childValue.Before == nil && childValue.After == nil { + // Don't record nil values at all in blocks. + continue + } + + attributes[key] = childChange + current = compareActions(current, childChange.GetAction()) + } + + blocks := make(map[string][]change.Change) + for key, blockType := range block.BlockTypes { + childValue := blockValue.getChild(key) + childChanges, next := childValue.computeChangesForBlockType(blockType) + if next == plans.NoOp && childValue.Before == nil && childValue.After == nil { + // Don't record nil values at all in blocks. + continue + } + blocks[key] = childChanges + current = compareActions(current, next) + } + + return change.New(change.Block(attributes, blocks), current, v.replacePath()) +} + +func (v Value) computeChangesForBlockType(blockType *jsonprovider.BlockType) ([]change.Change, plans.Action) { + switch blockType.NestingMode { + case "set": + return v.computeBlockChangesAsSet(blockType.Block) + case "list": + return v.computeBlockChangesAsList(blockType.Block) + case "map": + return v.computeBlockChangesAsMap(blockType.Block) + case "single", "group": + ch := v.ComputeChange(blockType.Block) + return []change.Change{ch}, ch.GetAction() + default: + panic("unrecognized nesting mode: " + blockType.NestingMode) + } } diff --git a/internal/command/jsonformat/differ/list.go b/internal/command/jsonformat/differ/list.go index 93bc5ee61e..dcc2894126 100644 --- a/internal/command/jsonformat/differ/list.go +++ b/internal/command/jsonformat/differ/list.go @@ -7,12 +7,13 @@ import ( "github.com/hashicorp/terraform/internal/command/jsonformat/change" "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" ) func (v Value) computeAttributeChangeAsList(elementType cty.Type) change.Change { var elements []change.Change current := v.getDefaultActionForIteration() - v.processList(elementType, func(value Value) { + v.processList(elementType.IsObjectType(), func(value Value) { element := value.ComputeChange(elementType) elements = append(elements, element) current = compareActions(current, element.GetAction()) @@ -31,6 +32,17 @@ func (v Value) computeAttributeChangeAsNestedList(attributes map[string]*jsonpro return change.New(change.NestedList(elements), current, v.replacePath()) } +func (v Value) computeBlockChangesAsList(block *jsonprovider.Block) ([]change.Change, plans.Action) { + var elements []change.Change + current := v.getDefaultActionForIteration() + v.processNestedList(func(value Value) { + element := value.ComputeChange(block) + elements = append(elements, element) + current = compareActions(current, element.GetAction()) + }) + return elements, current +} + func (v Value) processNestedList(process func(value Value)) { sliceValue := v.asSlice() for ix := 0; ix < len(sliceValue.Before) || ix < len(sliceValue.After); ix++ { @@ -38,7 +50,7 @@ func (v Value) processNestedList(process func(value Value)) { } } -func (v Value) processList(elementType cty.Type, process func(value Value)) { +func (v Value) processList(isObjType bool, process func(value Value)) { sliceValue := v.asSlice() lcs := lcs(sliceValue.Before, sliceValue.After) @@ -48,7 +60,7 @@ func (v Value) processList(elementType cty.Type, process func(value Value)) { // longest common subsequence. We are going to just say that all of // these have been deleted. for beforeIx < len(sliceValue.Before) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.Before[beforeIx], lcs[lcsIx])) { - isObjectDiff := elementType.IsObjectType() && afterIx < len(sliceValue.After) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.After[afterIx], lcs[lcsIx])) + isObjectDiff := isObjType && afterIx < len(sliceValue.After) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.After[afterIx], lcs[lcsIx])) if isObjectDiff { process(sliceValue.getChild(beforeIx, afterIx, false)) beforeIx++ diff --git a/internal/command/jsonformat/differ/map.go b/internal/command/jsonformat/differ/map.go index 5f4884b86c..12aff23de4 100644 --- a/internal/command/jsonformat/differ/map.go +++ b/internal/command/jsonformat/differ/map.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform/internal/command/jsonformat/change" "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" ) func (v Value) computeAttributeChangeAsMap(elementType cty.Type) change.Change { @@ -29,6 +30,17 @@ func (v Value) computeAttributeChangeAsNestedMap(attributes map[string]*jsonprov return change.New(change.Map(elements), current, v.replacePath()) } +func (v Value) computeBlockChangesAsMap(block *jsonprovider.Block) ([]change.Change, plans.Action) { + current := v.getDefaultActionForIteration() + var elements []change.Change + v.processMap(func(key string, value Value) { + element := value.ComputeChange(block) + elements = append(elements, element) + current = compareActions(current, element.GetAction()) + }) + return elements, current +} + func (v Value) processMap(process func(key string, value Value)) { mapValue := v.asMap() diff --git a/internal/command/jsonformat/differ/set.go b/internal/command/jsonformat/differ/set.go index 300419cd97..defa48d8f3 100644 --- a/internal/command/jsonformat/differ/set.go +++ b/internal/command/jsonformat/differ/set.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform/internal/command/jsonformat/change" "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" ) func (v Value) computeAttributeChangeAsSet(elementType cty.Type) change.Change { @@ -31,6 +32,17 @@ func (v Value) computeAttributeChangeAsNestedSet(attributes map[string]*jsonprov return change.New(change.Set(elements), current, v.replacePath()) } +func (v Value) computeBlockChangesAsSet(block *jsonprovider.Block) ([]change.Change, plans.Action) { + var elements []change.Change + current := v.getDefaultActionForIteration() + v.processSet(true, func(value Value) { + element := value.ComputeChange(block) + elements = append(elements, element) + current = compareActions(current, element.GetAction()) + }) + return elements, current +} + func (v Value) processSet(propagateReplace bool, process func(value Value)) { sliceValue := v.asSlice() diff --git a/internal/command/jsonformat/differ/value.go b/internal/command/jsonformat/differ/value.go index 33161a75eb..3825f23d8c 100644 --- a/internal/command/jsonformat/differ/value.go +++ b/internal/command/jsonformat/differ/value.go @@ -18,7 +18,7 @@ import ( // jsonprovider). // // A Value can be converted into a change.Change, ready for rendering, with the -// computeChangeForAttribute, ComputeChangeForOutput, and ComputeChangeForBlock +// computeChangeForAttribute, ComputeChangeForOutput, and computeChangeForBlock // functions. // // The Before and After fields are actually go-cty values, but we cannot convert @@ -121,6 +121,8 @@ func (v Value) ComputeChange(changeType interface{}) change.Change { return v.computeChangeForType(concrete) case map[string]*jsonprovider.Attribute: return v.computeAttributeChangeAsNestedObject(concrete) + case *jsonprovider.Block: + return v.computeChangeForBlock(concrete) default: panic(fmt.Sprintf("unrecognized change type: %T", changeType)) } @@ -185,6 +187,10 @@ func (v Value) getDefaultActionForIteration() plans.Action { // to convert a NoOp default action into an Update based on the actions of a // values children. func compareActions(current, next plans.Action) plans.Action { + if next == plans.NoOp { + return current + } + if current != next { return plans.Update } diff --git a/internal/command/jsonformat/differ/value_test.go b/internal/command/jsonformat/differ/value_test.go index 39b8136910..7f39d4c59d 100644 --- a/internal/command/jsonformat/differ/value_test.go +++ b/internal/command/jsonformat/differ/value_test.go @@ -871,6 +871,343 @@ func TestValue_ObjectAttributes(t *testing.T) { } } +func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) { + // This function tests manipulating simple attributes and blocks within + // blocks. It automatically tests these operations within the contexts of + // different block types. + + tcs := map[string]struct { + before interface{} + after interface{} + block *jsonprovider.Block + validate change.ValidateChangeFunc + validateSet []change.ValidateChangeFunc + }{ + "create_attribute": { + before: map[string]interface{}{}, + after: map[string]interface{}{ + "attribute_one": "new", + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, nil, plans.Update, false), + validateSet: []change.ValidateChangeFunc{ + change.ValidateBlock(nil, nil, plans.Delete, false), + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, nil, plans.Create, false), + }, + }, + "update_attribute": { + before: map[string]interface{}{ + "attribute_one": "old", + }, + after: map[string]interface{}{ + "attribute_one": "new", + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false), + }, nil, plans.Update, false), + validateSet: []change.ValidateChangeFunc{ + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, nil, plans.Delete, false), + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, nil, plans.Create, false), + }, + }, + "delete_attribute": { + before: map[string]interface{}{ + "attribute_one": "old", + }, + after: map[string]interface{}{}, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, nil, plans.Update, false), + validateSet: []change.ValidateChangeFunc{ + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, nil, plans.Delete, false), + change.ValidateBlock(nil, nil, plans.Create, false), + }, + }, + "create_block": { + before: map[string]interface{}{}, + after: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "new", + }, + }, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_one": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "single", + }, + }, + }, + validate: change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, nil, plans.Create, false), + }, + }, plans.Update, false), + validateSet: []change.ValidateChangeFunc{ + change.ValidateBlock(nil, nil, plans.Delete, false), + change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, nil, plans.Create, false), + }, + }, plans.Create, false), + }, + }, + "update_block": { + before: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "old", + }, + }, + after: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "new", + }, + }, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_one": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "single", + }, + }, + }, + validate: change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false), + }, nil, plans.Update, false), + }, + }, plans.Update, false), + validateSet: []change.ValidateChangeFunc{ + change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, nil, plans.Delete, false), + }, + }, plans.Delete, false), + change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, nil, plans.Create, false), + }, + }, plans.Create, false), + }, + }, + "delete_block": { + before: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "old", + }, + }, + after: map[string]interface{}{}, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_one": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "single", + }, + }, + }, + validate: change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, nil, plans.Delete, false), + }, + }, plans.Update, false), + validateSet: []change.ValidateChangeFunc{ + change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_one": { + change.ValidateBlock(map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, nil, plans.Delete, false), + }, + }, plans.Delete, false), + change.ValidateBlock(nil, nil, plans.Create, false), + }, + }, + } + for name, tmp := range tcs { + tc := tmp + + t.Run(name, func(t *testing.T) { + t.Run("single", func(t *testing.T) { + input := Value{ + Before: map[string]interface{}{ + "block_type": tc.before, + }, + After: map[string]interface{}{ + "block_type": tc.after, + }, + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "single", + }, + }, + } + + validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_type": { + tc.validate, + }, + }, plans.Update, false) + validate(t, input.ComputeChange(block)) + }) + t.Run("map", func(t *testing.T) { + input := Value{ + Before: map[string]interface{}{ + "block_type": map[string]interface{}{ + "one": tc.before, + }, + }, + After: map[string]interface{}{ + "block_type": map[string]interface{}{ + "one": tc.after, + }, + }, + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "map", + }, + }, + } + + validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_type": { + tc.validate, + }, + }, plans.Update, false) + validate(t, input.ComputeChange(block)) + }) + t.Run("list", func(t *testing.T) { + input := Value{ + Before: map[string]interface{}{ + "block_type": []interface{}{ + tc.before, + }, + }, + After: map[string]interface{}{ + "block_type": []interface{}{ + tc.after, + }, + }, + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "list", + }, + }, + } + + validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_type": { + tc.validate, + }, + }, plans.Update, false) + validate(t, input.ComputeChange(block)) + }) + t.Run("set", func(t *testing.T) { + input := Value{ + Before: map[string]interface{}{ + "block_type": []interface{}{ + tc.before, + }, + }, + After: map[string]interface{}{ + "block_type": []interface{}{ + tc.after, + }, + }, + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "set", + }, + }, + } + + validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{ + "block_type": func() []change.ValidateChangeFunc { + if tc.validateSet != nil { + return tc.validateSet + } + return []change.ValidateChangeFunc{tc.validate} + }(), + }, plans.Update, false) + validate(t, input.ComputeChange(block)) + }) + }) + } +} + func TestValue_PrimitiveAttributes(t *testing.T) { // This function tests manipulating primitives: creating, deleting and // updating. It also automatically tests these operations within the