Skip to content

Commit

Permalink
convert: Retain concrete types when converting from cty.DynamicPseudo…
Browse files Browse the repository at this point in the history
…Type to concrete
  • Loading branch information
liamcervante authored Nov 9, 2022
1 parent 81703b1 commit 5a9fd44
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 2 deletions.
4 changes: 2 additions & 2 deletions cty/convert/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
104 changes: 104 additions & 0 deletions cty/convert/conversion_dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
85 changes: 85 additions & 0 deletions cty/convert/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 5a9fd44

Please sign in to comment.