From e7a700c94e995ebfd87b60359535125b1bdd09b7 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Tue, 18 Oct 2022 13:33:11 +0200 Subject: [PATCH] Allow maps to convert into objects when missing optional attributes are not compatible --- cty/convert/conversion_collection.go | 50 +++++++++++++++++++++++++--- cty/convert/public_test.go | 14 ++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/cty/convert/conversion_collection.go b/cty/convert/conversion_collection.go index c21f4183..55db1a4a 100644 --- a/cty/convert/conversion_collection.go +++ b/cty/convert/conversion_collection.go @@ -448,13 +448,26 @@ func conversionMapToObject(mapType cty.Type, objType cty.Type, unsafe bool) conv elemConvs[name] = getConversion(mapEty, objectAty, unsafe) if elemConvs[name] == nil { - // If any of our element conversions are impossible, then the our - // whole conversion is impossible. + // This means that this conversion is impossible. Typically, we + // would give up at this point and declare the whole conversion + // impossible. But, if this attribute is optional then maybe we will + // be able to do this conversion anyway provided the actual concrete + // map doesn't have this value set. + + if objType.AttributeOptional(name) { + // This attribute is optional, so let's leave this conversion in + // as a nil, and we can error later if we actually have to + // convert this. + continue + } + + // Otherwise, give up. This conversion is impossible as we have a + // required attribute that doesn't match the map's inner type. return nil } } - // If we fall out here then a conversion is possible, using the + // If we fall out here then a conversion may be possible, using the // element conversions in elemConvs return func(val cty.Value, path cty.Path) (cty.Value, error) { elems := make(map[string]cty.Value, len(elemConvs)) @@ -474,12 +487,39 @@ func conversionMapToObject(mapType cty.Type, objType cty.Type, unsafe bool) conv Key: name, } - conv := elemConvs[name.AsString()] - if conv != nil { + // There are 3 cases here: + // 1. This attribute is not in elemConvs + // 2. This attribute is in elemConvs and is not nil + // 3. This attribute is in elemConvs and is nil. + + // In case 1, we do not enter any of the branches below. This case + // means the attribute type is the same between the map and the + // object, and we don't need to do any conversion. + + if conv, ok := elemConvs[name.AsString()]; conv != nil { + // This is case 2. The attribute type is different between the + // map and the object, and we know how to convert between them. + // So, we reset val to be the converted value and carry on. val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } + } else if ok { + // This is case 3 and it is an error. The attribute types are + // different between the map and the object, but we cannot + // convert between them. + // + // Now typically, this would be picked earlier on when we were + // building elemConvs. However, in the case of optional + // attributes there was a chance we could still convert the + // overall object even if this particular attribute was not + // convertable. This is because it could have not been set in + // the map, and we could skip over it here and set a null value. + // + // Since we reached this branch, we know that map did actually + // contain a non-convertable optional attribute. This means we + // error. + return cty.NilVal, path.NewErrorf("attribute %s cannot be converted: %s", name.AsString(), MismatchMessage(val.Type(), objType.AttributeType(name.AsString()))) } elems[name.AsString()] = val diff --git a/cty/convert/public_test.go b/cty/convert/public_test.go index 134d0a05..e2244404 100644 --- a/cty/convert/public_test.go +++ b/cty/convert/public_test.go @@ -1011,6 +1011,20 @@ func TestConvert(t *testing.T) { }), WantError: false, }, + { + Value: cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("boop"), + "c": cty.StringVal("foobar"), + }), + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "a": cty.String, + "b": cty.String, + "c": cty.Object(map[string]cty.Type{ + "d": cty.String, + }), + }, []string{"b", "c"}), + WantError: true, + }, { Value: cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{