Skip to content

Commit 5a9fd44

Browse files
authored
convert: Retain concrete types when converting from cty.DynamicPseudoType to concrete
1 parent 81703b1 commit 5a9fd44

File tree

3 files changed

+191
-2
lines changed

3 files changed

+191
-2
lines changed

cty/convert/conversion.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion {
4343
out = out.WithoutOptionalAttributesDeep()
4444

4545
if !isKnown {
46-
return cty.UnknownVal(out), nil
46+
return cty.UnknownVal(dynamicReplace(in.Type(), out)), nil
4747
}
4848

4949
if isNull {
5050
// We'll pass through nulls, albeit type converted, and let
5151
// the caller deal with whatever handling they want to do in
5252
// case null values are considered valid in some applications.
53-
return cty.NullVal(out), nil
53+
return cty.NullVal(dynamicReplace(in.Type(), out)), nil
5454
}
5555
}
5656

cty/convert/conversion_dynamic.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,107 @@ func dynamicFixup(wantType cty.Type) conversion {
3131
func dynamicPassthrough(in cty.Value, path cty.Path) (cty.Value, error) {
3232
return in, nil
3333
}
34+
35+
// dynamicReplace aims to return the out type unchanged, but if it finds a
36+
// dynamic type either directly or in any descendent elements it replaces them
37+
// with the equivalent type from in.
38+
//
39+
// This function assumes that in and out are compatible from a Convert
40+
// perspective, and will panic if it finds that they are not. For example if
41+
// in is an object and out is a map, this function will still attempt to iterate
42+
// through both as if they were the same.
43+
func dynamicReplace(in, out cty.Type) cty.Type {
44+
if in == cty.DynamicPseudoType || in == cty.NilType {
45+
// Short circuit this case, there's no point worrying about this if in
46+
// is a dynamic type or a nil type. Out is the best we can do.
47+
return out
48+
}
49+
50+
switch {
51+
case out == cty.DynamicPseudoType:
52+
// So replace out with in.
53+
return in
54+
case out.IsPrimitiveType(), out.IsCapsuleType():
55+
// out is not dynamic and it doesn't contain descendent elements so just
56+
// return it unchanged.
57+
return out
58+
case out.IsMapType():
59+
var elemType cty.Type
60+
61+
// Maps are compatible with other maps or objects.
62+
if in.IsMapType() {
63+
elemType = dynamicReplace(in.ElementType(), out.ElementType())
64+
}
65+
66+
if in.IsObjectType() {
67+
var types []cty.Type
68+
for _, t := range in.AttributeTypes() {
69+
types = append(types, t)
70+
}
71+
unifiedType, _ := unify(types, true)
72+
elemType = dynamicReplace(unifiedType, out.ElementType())
73+
}
74+
75+
return cty.Map(elemType)
76+
case out.IsObjectType():
77+
// Objects are compatible with other objects and maps.
78+
outTypes := map[string]cty.Type{}
79+
if in.IsMapType() {
80+
for attr, attrType := range out.AttributeTypes() {
81+
outTypes[attr] = dynamicReplace(in.ElementType(), attrType)
82+
}
83+
}
84+
85+
if in.IsObjectType() {
86+
for attr, attrType := range out.AttributeTypes() {
87+
if !in.HasAttribute(attr) {
88+
// If in does not have this attribute, then it is an
89+
// optional attribute and there is nothing we can do except
90+
// to return the type from out even if it is dynamic.
91+
outTypes[attr] = attrType
92+
continue
93+
}
94+
outTypes[attr] = dynamicReplace(in.AttributeType(attr), attrType)
95+
}
96+
}
97+
98+
return cty.Object(outTypes)
99+
case out.IsSetType():
100+
var elemType cty.Type
101+
102+
// Sets are compatible with other sets, lists, tuples.
103+
if in.IsSetType() || in.IsListType() {
104+
elemType = dynamicReplace(in.ElementType(), out.ElementType())
105+
}
106+
107+
if in.IsTupleType() {
108+
unifiedType, _ := unify(in.TupleElementTypes(), true)
109+
elemType = dynamicReplace(unifiedType, out.ElementType())
110+
}
111+
112+
return cty.Set(elemType)
113+
case out.IsListType():
114+
var elemType cty.Type
115+
116+
// Lists are compatible with other lists, sets, and tuples.
117+
if in.IsSetType() || in.IsListType() {
118+
elemType = dynamicReplace(in.ElementType(), out.ElementType())
119+
}
120+
121+
if in.IsTupleType() {
122+
unifiedType, _ := unify(in.TupleElementTypes(), true)
123+
elemType = dynamicReplace(unifiedType, out.ElementType())
124+
}
125+
126+
return cty.List(elemType)
127+
case out.IsTupleType():
128+
// Tuples are only compatible with other tuples
129+
var types []cty.Type
130+
for ix := 0; ix < len(out.TupleElementTypes()); ix++ {
131+
types = append(types, dynamicReplace(in.TupleElementType(ix), out.TupleElementType(ix)))
132+
}
133+
return cty.Tuple(types)
134+
default:
135+
panic("unrecognized type " + out.FriendlyName())
136+
}
137+
}

cty/convert/public_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,91 @@ func TestConvert(t *testing.T) {
15141514
})),
15151515
}),
15161516
},
1517+
// Collections should prefer concrete types over dynamic types.
1518+
{
1519+
Value: cty.ListValEmpty(cty.Number),
1520+
Type: cty.List(cty.DynamicPseudoType),
1521+
Want: cty.ListValEmpty(cty.Number),
1522+
},
1523+
{
1524+
Value: cty.NullVal(cty.List(cty.Number)),
1525+
Type: cty.List(cty.DynamicPseudoType),
1526+
Want: cty.NullVal(cty.List(cty.Number)),
1527+
},
1528+
{
1529+
Value: cty.NullVal(cty.List(cty.Number)),
1530+
Type: cty.Set(cty.DynamicPseudoType),
1531+
Want: cty.NullVal(cty.Set(cty.Number)),
1532+
},
1533+
{
1534+
Value: cty.MapValEmpty(cty.Number),
1535+
Type: cty.Map(cty.DynamicPseudoType),
1536+
Want: cty.MapValEmpty(cty.Number),
1537+
},
1538+
{
1539+
Value: cty.NullVal(cty.Map(cty.Number)),
1540+
Type: cty.Map(cty.DynamicPseudoType),
1541+
Want: cty.NullVal(cty.Map(cty.Number)),
1542+
},
1543+
{
1544+
Value: cty.NullVal(cty.Map(cty.Number)),
1545+
Type: cty.Object(map[string]cty.Type{
1546+
"a": cty.DynamicPseudoType,
1547+
}),
1548+
Want: cty.NullVal(cty.Object(map[string]cty.Type{
1549+
"a": cty.Number,
1550+
})),
1551+
},
1552+
{
1553+
Value: cty.SetValEmpty(cty.Number),
1554+
Type: cty.Set(cty.DynamicPseudoType),
1555+
Want: cty.SetValEmpty(cty.Number),
1556+
},
1557+
{
1558+
Value: cty.NullVal(cty.Set(cty.Number)),
1559+
Type: cty.Set(cty.DynamicPseudoType),
1560+
Want: cty.NullVal(cty.Set(cty.Number)),
1561+
},
1562+
{
1563+
Value: cty.NullVal(cty.Set(cty.Number)),
1564+
Type: cty.List(cty.DynamicPseudoType),
1565+
Want: cty.NullVal(cty.List(cty.Number)),
1566+
},
1567+
{
1568+
Value: cty.NullVal(cty.Object(map[string]cty.Type{
1569+
"a": cty.String,
1570+
})),
1571+
Type: cty.Map(cty.DynamicPseudoType),
1572+
Want: cty.NullVal(cty.Map(cty.String)),
1573+
},
1574+
{
1575+
Value: cty.NullVal(cty.Object(map[string]cty.Type{
1576+
"a": cty.Object(map[string]cty.Type{
1577+
"b": cty.String,
1578+
}),
1579+
})),
1580+
Type: cty.Object(map[string]cty.Type{
1581+
"a": cty.Object(map[string]cty.Type{
1582+
"b": cty.DynamicPseudoType,
1583+
}),
1584+
}),
1585+
Want: cty.NullVal(cty.Object(map[string]cty.Type{
1586+
"a": cty.Object(map[string]cty.Type{
1587+
"b": cty.String,
1588+
}),
1589+
})),
1590+
},
1591+
{
1592+
Value: cty.NullVal(cty.Tuple([]cty.Type{
1593+
cty.String,
1594+
})),
1595+
Type: cty.Tuple([]cty.Type{
1596+
cty.DynamicPseudoType,
1597+
}),
1598+
Want: cty.NullVal(cty.Tuple([]cty.Type{
1599+
cty.String,
1600+
})),
1601+
},
15171602
// We should strip optional attributes out of types even if they match.
15181603
{
15191604
Value: cty.MapVal(map[string]cty.Value{

0 commit comments

Comments
 (0)