diff --git a/cty/convert/conversion.go b/cty/convert/conversion.go index ededc5f3..541b9a49 100644 --- a/cty/convert/conversion.go +++ b/cty/convert/conversion.go @@ -43,14 +43,14 @@ func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion { out = out.WithoutOptionalAttributesDeep() if !isKnown { - return cty.UnknownVal(out), nil + return cty.UnknownVal(dynamicReplace(in.Type(), out)), nil } if isNull { // We'll pass through nulls, albeit type converted, and let // the caller deal with whatever handling they want to do in // case null values are considered valid in some applications. - return cty.NullVal(out), nil + return cty.NullVal(dynamicReplace(in.Type(), out)), nil } } diff --git a/cty/convert/conversion_dynamic.go b/cty/convert/conversion_dynamic.go index 4d19cf6c..3b554e01 100644 --- a/cty/convert/conversion_dynamic.go +++ b/cty/convert/conversion_dynamic.go @@ -31,3 +31,107 @@ func dynamicFixup(wantType cty.Type) conversion { func dynamicPassthrough(in cty.Value, path cty.Path) (cty.Value, error) { return in, nil } + +// dynamicReplace aims to return the out type unchanged, but if it finds a +// dynamic type either directly or in any descendent elements it replaces them +// with the equivalent type from in. +// +// This function assumes that in and out are compatible from a Convert +// perspective, and will panic if it finds that they are not. For example if +// in is an object and out is a map, this function will still attempt to iterate +// through both as if they were the same. +func dynamicReplace(in, out cty.Type) cty.Type { + if in == cty.DynamicPseudoType || in == cty.NilType { + // Short circuit this case, there's no point worrying about this if in + // is a dynamic type or a nil type. Out is the best we can do. + return out + } + + switch { + case out == cty.DynamicPseudoType: + // So replace out with in. + return in + case out.IsPrimitiveType(), out.IsCapsuleType(): + // out is not dynamic and it doesn't contain descendent elements so just + // return it unchanged. + return out + case out.IsMapType(): + var elemType cty.Type + + // Maps are compatible with other maps or objects. + if in.IsMapType() { + elemType = dynamicReplace(in.ElementType(), out.ElementType()) + } + + if in.IsObjectType() { + var types []cty.Type + for _, t := range in.AttributeTypes() { + types = append(types, t) + } + unifiedType, _ := unify(types, true) + elemType = dynamicReplace(unifiedType, out.ElementType()) + } + + return cty.Map(elemType) + case out.IsObjectType(): + // Objects are compatible with other objects and maps. + outTypes := map[string]cty.Type{} + if in.IsMapType() { + for attr, attrType := range out.AttributeTypes() { + outTypes[attr] = dynamicReplace(in.ElementType(), attrType) + } + } + + if in.IsObjectType() { + for attr, attrType := range out.AttributeTypes() { + if !in.HasAttribute(attr) { + // If in does not have this attribute, then it is an + // optional attribute and there is nothing we can do except + // to return the type from out even if it is dynamic. + outTypes[attr] = attrType + continue + } + outTypes[attr] = dynamicReplace(in.AttributeType(attr), attrType) + } + } + + return cty.Object(outTypes) + case out.IsSetType(): + var elemType cty.Type + + // Sets are compatible with other sets, lists, tuples. + if in.IsSetType() || in.IsListType() { + elemType = dynamicReplace(in.ElementType(), out.ElementType()) + } + + if in.IsTupleType() { + unifiedType, _ := unify(in.TupleElementTypes(), true) + elemType = dynamicReplace(unifiedType, out.ElementType()) + } + + return cty.Set(elemType) + case out.IsListType(): + var elemType cty.Type + + // Lists are compatible with other lists, sets, and tuples. + if in.IsSetType() || in.IsListType() { + elemType = dynamicReplace(in.ElementType(), out.ElementType()) + } + + if in.IsTupleType() { + unifiedType, _ := unify(in.TupleElementTypes(), true) + elemType = dynamicReplace(unifiedType, out.ElementType()) + } + + return cty.List(elemType) + case out.IsTupleType(): + // Tuples are only compatible with other tuples + var types []cty.Type + for ix := 0; ix < len(out.TupleElementTypes()); ix++ { + types = append(types, dynamicReplace(in.TupleElementType(ix), out.TupleElementType(ix))) + } + return cty.Tuple(types) + default: + panic("unrecognized type " + out.FriendlyName()) + } +} diff --git a/cty/convert/public_test.go b/cty/convert/public_test.go index 97f4f71f..44be53be 100644 --- a/cty/convert/public_test.go +++ b/cty/convert/public_test.go @@ -1514,6 +1514,91 @@ func TestConvert(t *testing.T) { })), }), }, + // Collections should prefer concrete types over dynamic types. + { + Value: cty.ListValEmpty(cty.Number), + Type: cty.List(cty.DynamicPseudoType), + Want: cty.ListValEmpty(cty.Number), + }, + { + Value: cty.NullVal(cty.List(cty.Number)), + Type: cty.List(cty.DynamicPseudoType), + Want: cty.NullVal(cty.List(cty.Number)), + }, + { + Value: cty.NullVal(cty.List(cty.Number)), + Type: cty.Set(cty.DynamicPseudoType), + Want: cty.NullVal(cty.Set(cty.Number)), + }, + { + Value: cty.MapValEmpty(cty.Number), + Type: cty.Map(cty.DynamicPseudoType), + Want: cty.MapValEmpty(cty.Number), + }, + { + Value: cty.NullVal(cty.Map(cty.Number)), + Type: cty.Map(cty.DynamicPseudoType), + Want: cty.NullVal(cty.Map(cty.Number)), + }, + { + Value: cty.NullVal(cty.Map(cty.Number)), + Type: cty.Object(map[string]cty.Type{ + "a": cty.DynamicPseudoType, + }), + Want: cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.Number, + })), + }, + { + Value: cty.SetValEmpty(cty.Number), + Type: cty.Set(cty.DynamicPseudoType), + Want: cty.SetValEmpty(cty.Number), + }, + { + Value: cty.NullVal(cty.Set(cty.Number)), + Type: cty.Set(cty.DynamicPseudoType), + Want: cty.NullVal(cty.Set(cty.Number)), + }, + { + Value: cty.NullVal(cty.Set(cty.Number)), + Type: cty.List(cty.DynamicPseudoType), + Want: cty.NullVal(cty.List(cty.Number)), + }, + { + Value: cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.String, + })), + Type: cty.Map(cty.DynamicPseudoType), + Want: cty.NullVal(cty.Map(cty.String)), + }, + { + Value: cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.Object(map[string]cty.Type{ + "b": cty.String, + }), + })), + Type: cty.Object(map[string]cty.Type{ + "a": cty.Object(map[string]cty.Type{ + "b": cty.DynamicPseudoType, + }), + }), + Want: cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.Object(map[string]cty.Type{ + "b": cty.String, + }), + })), + }, + { + Value: cty.NullVal(cty.Tuple([]cty.Type{ + cty.String, + })), + Type: cty.Tuple([]cty.Type{ + cty.DynamicPseudoType, + }), + Want: cty.NullVal(cty.Tuple([]cty.Type{ + cty.String, + })), + }, // We should strip optional attributes out of types even if they match. { Value: cty.MapVal(map[string]cty.Value{