package convert import ( "github.com/zclconf/go-cty/cty" ) // conversionCollectionToList returns a conversion that will apply the given // conversion to all of the elements of a collection (something that supports // ForEachElement and LengthInt) and then returns the result as a list. // // "conv" can be nil if the elements are expected to already be of the // correct type and just need to be re-wrapped into a list. (For example, // if we're converting from a set into a list of the same element type.) func conversionCollectionToList(ety cty.Type, conv conversion) conversion { return func(val cty.Value, path cty.Path) (cty.Value, error) { if !val.Length().IsKnown() { // If the input collection has an unknown length (which is true // for a set containing unknown values) then our result must be // an unknown list, because we can't predict how many elements // the resulting list should have. return cty.UnknownVal(cty.List(val.Type().ElementType())), nil } elems := make([]cty.Value, 0, val.LengthInt()) i := int64(0) elemPath := append(path.Copy(), nil) it := val.ElementIterator() for it.Next() { _, val := it.Element() var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: cty.NumberIntVal(i), } if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems = append(elems, val) i++ } if len(elems) == 0 { // Prefer a concrete type over a dynamic type when returning an // empty list if ety == cty.DynamicPseudoType { return cty.ListValEmpty(val.Type().ElementType()), nil } return cty.ListValEmpty(ety), nil } if !cty.CanListVal(elems) { return cty.NilVal, path.NewErrorf("element types must all match for conversion to list") } return cty.ListVal(elems), nil } } // conversionCollectionToSet returns a conversion that will apply the given // conversion to all of the elements of a collection (something that supports // ForEachElement and LengthInt) and then returns the result as a set. // // "conv" can be nil if the elements are expected to already be of the // correct type and just need to be re-wrapped into a set. (For example, // if we're converting from a list into a set of the same element type.) func conversionCollectionToSet(ety cty.Type, conv conversion) conversion { return func(val cty.Value, path cty.Path) (cty.Value, error) { elems := make([]cty.Value, 0, val.LengthInt()) i := int64(0) elemPath := append(path.Copy(), nil) it := val.ElementIterator() for it.Next() { _, val := it.Element() var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: cty.NumberIntVal(i), } if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems = append(elems, val) i++ } if len(elems) == 0 { // Prefer a concrete type over a dynamic type when returning an // empty set if ety == cty.DynamicPseudoType { return cty.SetValEmpty(val.Type().ElementType()), nil } return cty.SetValEmpty(ety), nil } if !cty.CanSetVal(elems) { return cty.NilVal, path.NewErrorf("element types must all match for conversion to set") } return cty.SetVal(elems), nil } } // conversionCollectionToMap returns a conversion that will apply the given // conversion to all of the elements of a collection (something that supports // ForEachElement and LengthInt) and then returns the result as a map. // // "conv" can be nil if the elements are expected to already be of the // correct type and just need to be re-wrapped into a map. func conversionCollectionToMap(ety cty.Type, conv conversion) conversion { return func(val cty.Value, path cty.Path) (cty.Value, error) { elems := make(map[string]cty.Value, 0) elemPath := append(path.Copy(), nil) it := val.ElementIterator() for it.Next() { key, val := it.Element() var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: key, } keyStr, err := Convert(key, cty.String) if err != nil { // Should never happen, because keys can only be numbers or // strings and both can convert to string. return cty.DynamicVal, elemPath.NewErrorf("cannot convert key type %s to string for map", key.Type().FriendlyName()) } if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems[keyStr.AsString()] = val } if len(elems) == 0 { // Prefer a concrete type over a dynamic type when returning an // empty map if ety == cty.DynamicPseudoType { return cty.MapValEmpty(val.Type().ElementType()), nil } return cty.MapValEmpty(ety), nil } if ety.IsCollectionType() || ety.IsObjectType() { var err error if elems, err = conversionUnifyCollectionElements(elems, path, false); err != nil { return cty.NilVal, err } } if !cty.CanMapVal(elems) { return cty.NilVal, path.NewErrorf("element types must all match for conversion to map") } return cty.MapVal(elems), nil } } // conversionTupleToSet returns a conversion that will take a value of the // given tuple type and return a set of the given element type. // // Will panic if the given tupleType isn't actually a tuple type. func conversionTupleToSet(tupleType cty.Type, setEty cty.Type, unsafe bool) conversion { tupleEtys := tupleType.TupleElementTypes() if len(tupleEtys) == 0 { // Empty tuple short-circuit return func(val cty.Value, path cty.Path) (cty.Value, error) { return cty.SetValEmpty(setEty), nil } } if setEty == cty.DynamicPseudoType { // This is a special case where the caller wants us to find // a suitable single type that all elements can convert to, if // possible. setEty, _ = unify(tupleEtys, unsafe) if setEty == cty.NilType { return nil } // If the set element type after unification is still the dynamic // type, the only way this can result in a valid set is if all values // are of dynamic type if setEty == cty.DynamicPseudoType { for _, tupleEty := range tupleEtys { if !tupleEty.Equals(cty.DynamicPseudoType) { return nil } } } } elemConvs := make([]conversion, len(tupleEtys)) for i, tupleEty := range tupleEtys { if tupleEty.Equals(setEty) { // no conversion required continue } elemConvs[i] = getConversion(tupleEty, setEty, unsafe) if elemConvs[i] == nil { // If any of our element conversions are impossible, then the our // whole conversion is impossible. return nil } } // If we fall out here then a conversion is possible, using the // element conversions in elemConvs return func(val cty.Value, path cty.Path) (cty.Value, error) { elems := make([]cty.Value, 0, len(elemConvs)) elemPath := append(path.Copy(), nil) i := int64(0) it := val.ElementIterator() for it.Next() { _, val := it.Element() var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: cty.NumberIntVal(i), } conv := elemConvs[i] if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems = append(elems, val) i++ } if !cty.CanSetVal(elems) { return cty.NilVal, path.NewErrorf("element types must all match for conversion to set") } return cty.SetVal(elems), nil } } // conversionTupleToList returns a conversion that will take a value of the // given tuple type and return a list of the given element type. // // Will panic if the given tupleType isn't actually a tuple type. func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion { tupleEtys := tupleType.TupleElementTypes() if len(tupleEtys) == 0 { // Empty tuple short-circuit return func(val cty.Value, path cty.Path) (cty.Value, error) { return cty.ListValEmpty(listEty), nil } } if listEty == cty.DynamicPseudoType { // This is a special case where the caller wants us to find // a suitable single type that all elements can convert to, if // possible. listEty, _ = unify(tupleEtys, unsafe) if listEty == cty.NilType { return nil } // If the list element type after unification is still the dynamic // type, the only way this can result in a valid list is if all values // are of dynamic type if listEty == cty.DynamicPseudoType { for _, tupleEty := range tupleEtys { if !tupleEty.Equals(cty.DynamicPseudoType) { return nil } } } } elemConvs := make([]conversion, len(tupleEtys)) for i, tupleEty := range tupleEtys { if tupleEty.Equals(listEty) { // no conversion required continue } elemConvs[i] = getConversion(tupleEty, listEty, unsafe) if elemConvs[i] == nil { // If any of our element conversions are impossible, then the our // whole conversion is impossible. return nil } } // If we fall out here then a conversion is possible, using the // element conversions in elemConvs return func(val cty.Value, path cty.Path) (cty.Value, error) { elems := make([]cty.Value, 0, len(elemConvs)) elemTys := make([]cty.Type, 0, len(elems)) elemPath := append(path.Copy(), nil) i := int64(0) it := val.ElementIterator() for it.Next() { _, val := it.Element() var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: cty.NumberIntVal(i), } conv := elemConvs[i] if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems = append(elems, val) elemTys = append(elemTys, val.Type()) i++ } elems, err := conversionUnifyListElements(elems, elemPath, unsafe) if err != nil { return cty.NilVal, err } if !cty.CanListVal(elems) { return cty.NilVal, path.NewErrorf("element types must all match for conversion to list") } return cty.ListVal(elems), nil } } // conversionObjectToMap returns a conversion that will take a value of the // given object type and return a map of the given element type. // // Will panic if the given objectType isn't actually an object type. func conversionObjectToMap(objectType cty.Type, mapEty cty.Type, unsafe bool) conversion { objectAtys := objectType.AttributeTypes() if len(objectAtys) == 0 { // Empty object short-circuit return func(val cty.Value, path cty.Path) (cty.Value, error) { return cty.MapValEmpty(mapEty), nil } } if mapEty == cty.DynamicPseudoType { // This is a special case where the caller wants us to find // a suitable single type that all elements can convert to, if // possible. objectAtysList := make([]cty.Type, 0, len(objectAtys)) for _, aty := range objectAtys { objectAtysList = append(objectAtysList, aty) } mapEty, _ = unify(objectAtysList, unsafe) if mapEty == cty.NilType { return nil } } elemConvs := make(map[string]conversion, len(objectAtys)) for name, objectAty := range objectAtys { if objectAty.Equals(mapEty) { // no conversion required continue } elemConvs[name] = getConversion(objectAty, mapEty, unsafe) if elemConvs[name] == nil { // If any of our element conversions are impossible, then the our // whole conversion is impossible. return nil } } // If we fall out here then a conversion is 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)) elemPath := append(path.Copy(), nil) it := val.ElementIterator() for it.Next() { name, val := it.Element() var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: name, } conv := elemConvs[name.AsString()] if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems[name.AsString()] = val } if mapEty.IsCollectionType() || mapEty.IsObjectType() { var err error if elems, err = conversionUnifyCollectionElements(elems, path, unsafe); err != nil { return cty.NilVal, err } } if !cty.CanMapVal(elems) { return cty.NilVal, path.NewErrorf("attribute types must all match for conversion to map") } return cty.MapVal(elems), nil } } // conversionMapToObject returns a conversion that will take a value of the // given map type and return an object of the given type. The object attribute // types must all be compatible with the map element type. // // Will panic if the given mapType and objType are not maps and objects // respectively. func conversionMapToObject(mapType cty.Type, objType cty.Type, unsafe bool) conversion { objectAtys := objType.AttributeTypes() mapEty := mapType.ElementType() elemConvs := make(map[string]conversion, len(objectAtys)) for name, objectAty := range objectAtys { if objectAty.Equals(mapEty) { // no conversion required continue } 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. return nil } } // If we fall out here then a conversion is 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)) elemPath := append(path.Copy(), nil) it := val.ElementIterator() for it.Next() { name, val := it.Element() // if there is no corresponding attribute, we skip this key if _, ok := objectAtys[name.AsString()]; !ok { continue } var err error elemPath[len(elemPath)-1] = cty.IndexStep{ Key: name, } conv := elemConvs[name.AsString()] if conv != nil { val, err = conv(val, elemPath) if err != nil { return cty.NilVal, err } } elems[name.AsString()] = val } for name, aty := range objectAtys { if _, exists := elems[name]; !exists { if optional := objType.AttributeOptional(name); optional { elems[name] = cty.NullVal(aty) } else { return cty.NilVal, path.NewErrorf("map has no element for required attribute %q", name) } } } return cty.ObjectVal(elems), nil } } func conversionUnifyCollectionElements(elems map[string]cty.Value, path cty.Path, unsafe bool) (map[string]cty.Value, error) { elemTypes := make([]cty.Type, 0, len(elems)) for _, elem := range elems { elemTypes = append(elemTypes, elem.Type()) } unifiedType, _ := unify(elemTypes, unsafe) if unifiedType == cty.NilType { return nil, path.NewErrorf("cannot find a common base type for all elements") } unifiedElems := make(map[string]cty.Value) elemPath := append(path.Copy(), nil) for name, elem := range elems { if elem.Type().Equals(unifiedType) { unifiedElems[name] = elem continue } conv := getConversion(elem.Type(), unifiedType, unsafe) if conv == nil { } elemPath[len(elemPath)-1] = cty.IndexStep{ Key: cty.StringVal(name), } val, err := conv(elem, elemPath) if err != nil { return nil, err } unifiedElems[name] = val } return unifiedElems, nil } func conversionUnifyListElements(elems []cty.Value, path cty.Path, unsafe bool) ([]cty.Value, error) { elemTypes := make([]cty.Type, len(elems)) for i, elem := range elems { elemTypes[i] = elem.Type() } unifiedType, _ := unify(elemTypes, unsafe) if unifiedType == cty.NilType { return nil, path.NewErrorf("cannot find a common base type for all elements") } ret := make([]cty.Value, len(elems)) elemPath := append(path.Copy(), nil) for i, elem := range elems { if elem.Type().Equals(unifiedType) { ret[i] = elem continue } conv := getConversion(elem.Type(), unifiedType, unsafe) if conv == nil { } elemPath[len(elemPath)-1] = cty.IndexStep{ Key: cty.NumberIntVal(int64(i)), } val, err := conv(elem, elemPath) if err != nil { return nil, err } ret[i] = val } return ret, nil }