From dd1fa322a717d4cc66f679c5ba400da900b5534b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 9 Mar 2019 12:27:39 -0800 Subject: [PATCH] command/format: Support list/map blocks with dynamic-typed attrs Our null-to-empty normalization was previously assuming these would always be collection types, but that isn't true when a block contains something dynamic since we must then use tuple or object types instead to properly represent all of the individual element types. --- command/format/diff.go | 59 +++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/command/format/diff.go b/command/format/diff.go index 00179da88..c2f4e7804 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -299,12 +299,8 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config // For the sake of handling nested blocks, we'll treat a null list // the same as an empty list since the config language doesn't // distinguish these anyway. - if old.IsNull() { - old = cty.ListValEmpty(old.Type().ElementType()) - } - if new.IsNull() { - new = cty.ListValEmpty(new.Type().ElementType()) - } + old = ctyNullBlockListAsEmpty(old) + new = ctyNullBlockListAsEmpty(new) oldItems := ctyCollectionValues(old) newItems := ctyCollectionValues(new) @@ -358,12 +354,8 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config // For the sake of handling nested blocks, we'll treat a null set // the same as an empty set since the config language doesn't // distinguish these anyway. - if old.IsNull() { - old = cty.SetValEmpty(old.Type().ElementType()) - } - if new.IsNull() { - new = cty.SetValEmpty(new.Type().ElementType()) - } + old = ctyNullBlockSetAsEmpty(old) + new = ctyNullBlockSetAsEmpty(new) oldItems := ctyCollectionValues(old) newItems := ctyCollectionValues(new) @@ -1101,3 +1093,46 @@ func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path { copy(newPath, path) return newPath } + +// ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil +// or returns an empty value of a suitable type to serve as a placeholder for it. +// +// In particular, this function handles the special situation where a "list" is +// actually represented as a tuple type where nested blocks contain +// dynamically-typed values. +func ctyNullBlockListAsEmpty(in cty.Value) cty.Value { + if !in.IsNull() { + return in + } + if ty := in.Type(); ty.IsListType() { + return cty.ListValEmpty(ty.ElementType()) + } + return cty.EmptyTupleVal // must need a tuple, then +} + +// ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil +// or returns an empty value of a suitable type to serve as a placeholder for it. +// +// In particular, this function handles the special situation where a "map" is +// actually represented as an object type where nested blocks contain +// dynamically-typed values. +func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value { + if !in.IsNull() { + return in + } + if ty := in.Type(); ty.IsMapType() { + return cty.MapValEmpty(ty.ElementType()) + } + return cty.EmptyObjectVal // must need an object, then +} + +// ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil +// or returns an empty value of a suitable type to serve as a placeholder for it. +func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value { + if !in.IsNull() { + return in + } + // Dynamically-typed attributes are not supported inside blocks backed by + // sets, so our result here is always a set. + return cty.SetValEmpty(in.Type().ElementType()) +}