package convert import ( "github.com/hashicorp/go-cty/cty" ) // The current unify implementation is somewhat inefficient, but we accept this // under the assumption that it will generally be used with small numbers of // types and with types of reasonable complexity. However, it does have a // "happy path" where all of the given types are equal. // // This function is likely to have poor performance in cases where any given // types are very complex (lots of deeply-nested structures) or if the list // of types itself is very large. In particular, it will walk the nested type // structure under the given types several times, especially when given a // list of types for which unification is not possible, since each permutation // will be tried to determine that result. func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { if len(types) == 0 { // Degenerate case return cty.NilType, nil } // If all of the given types are of the same structural kind, we may be // able to construct a new type that they can all be unified to, even if // that is not one of the given types. We must try this before the general // behavior below because in unsafe mode we can convert an object type to // a subset of that type, which would be a much less useful conversion for // unification purposes. { mapCt := 0 objectCt := 0 tupleCt := 0 dynamicCt := 0 for _, ty := range types { switch { case ty.IsMapType(): mapCt++ case ty.IsObjectType(): objectCt++ case ty.IsTupleType(): tupleCt++ case ty == cty.DynamicPseudoType: dynamicCt++ default: break } } switch { case mapCt > 0 && (mapCt+dynamicCt) == len(types): return unifyMapTypes(types, unsafe, dynamicCt > 0) case objectCt > 0 && (objectCt+dynamicCt) == len(types): return unifyObjectTypes(types, unsafe, dynamicCt > 0) case tupleCt > 0 && (tupleCt+dynamicCt) == len(types): return unifyTupleTypes(types, unsafe, dynamicCt > 0) case objectCt > 0 && tupleCt > 0: // Can never unify object and tuple types since they have incompatible kinds return cty.NilType, nil } } prefOrder := sortTypes(types) // sortTypes gives us an order where earlier items are preferable as // our result type. We'll now walk through these and choose the first // one we encounter for which conversions exist for all source types. conversions := make([]Conversion, len(types)) Preferences: for _, wantTypeIdx := range prefOrder { wantType := types[wantTypeIdx] for i, tryType := range types { if i == wantTypeIdx { // Don't need to convert our wanted type to itself conversions[i] = nil continue } if tryType.Equals(wantType) { conversions[i] = nil continue } if unsafe { conversions[i] = GetConversionUnsafe(tryType, wantType) } else { conversions[i] = GetConversion(tryType, wantType) } if conversions[i] == nil { // wantType is not a suitable unification type, so we'll // try the next one in our preference order. continue Preferences } } return wantType, conversions } // If we fall out here, no unification is possible return cty.NilType, nil } func unifyMapTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { // If we had any dynamic types in the input here then we can't predict // what path we'll take through here once these become known types, so // we'll conservatively produce DynamicVal for these. if hasDynamic { return unifyAllAsDynamic(types) } elemTypes := make([]cty.Type, 0, len(types)) for _, ty := range types { elemTypes = append(elemTypes, ty.ElementType()) } retElemType, _ := unify(elemTypes, unsafe) if retElemType == cty.NilType { return cty.NilType, nil } retTy := cty.Map(retElemType) conversions := make([]Conversion, len(types)) for i, ty := range types { if ty.Equals(retTy) { continue } if unsafe { conversions[i] = GetConversionUnsafe(ty, retTy) } else { conversions[i] = GetConversion(ty, retTy) } if conversions[i] == nil { // Shouldn't be reachable, since we were able to unify return cty.NilType, nil } } return retTy, conversions } func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { // If we had any dynamic types in the input here then we can't predict // what path we'll take through here once these become known types, so // we'll conservatively produce DynamicVal for these. if hasDynamic { return unifyAllAsDynamic(types) } // There are two different ways we can succeed here: // - If all of the given object types have the same set of attribute names // and the corresponding types are all unifyable, then we construct that // type. // - If the given object types have different attribute names or their // corresponding types are not unifyable, we'll instead try to unify // all of the attribute types together to produce a map type. // // Our unification behavior is intentionally stricter than our conversion // behavior for subset object types because user intent is different with // unification use-cases: it makes sense to allow {"foo":true} to convert // to emptyobjectval, but unifying an object with an attribute with the // empty object type should be an error because unifying to the empty // object type would be suprising and useless. firstAttrs := types[0].AttributeTypes() for _, ty := range types[1:] { thisAttrs := ty.AttributeTypes() if len(thisAttrs) != len(firstAttrs) { // If number of attributes is different then there can be no // object type in common. return unifyObjectTypesToMap(types, unsafe) } for name := range thisAttrs { if _, ok := firstAttrs[name]; !ok { // If attribute names don't exactly match then there can be // no object type in common. return unifyObjectTypesToMap(types, unsafe) } } } // If we get here then we've proven that all of the given object types // have exactly the same set of attribute names, though the types may // differ. retAtys := make(map[string]cty.Type) atysAcross := make([]cty.Type, len(types)) for name := range firstAttrs { for i, ty := range types { atysAcross[i] = ty.AttributeType(name) } retAtys[name], _ = unify(atysAcross, unsafe) if retAtys[name] == cty.NilType { // Cannot unify this attribute alone, which means that unification // of everything down to a map type can't be possible either. return cty.NilType, nil } } retTy := cty.Object(retAtys) conversions := make([]Conversion, len(types)) for i, ty := range types { if ty.Equals(retTy) { continue } if unsafe { conversions[i] = GetConversionUnsafe(ty, retTy) } else { conversions[i] = GetConversion(ty, retTy) } if conversions[i] == nil { // Shouldn't be reachable, since we were able to unify return unifyObjectTypesToMap(types, unsafe) } } return retTy, conversions } func unifyObjectTypesToMap(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { // This is our fallback case for unifyObjectTypes, where we see if we can // construct a map type that can accept all of the attribute types. var atys []cty.Type for _, ty := range types { for _, aty := range ty.AttributeTypes() { atys = append(atys, aty) } } ety, _ := unify(atys, unsafe) if ety == cty.NilType { return cty.NilType, nil } retTy := cty.Map(ety) conversions := make([]Conversion, len(types)) for i, ty := range types { if ty.Equals(retTy) { continue } if unsafe { conversions[i] = GetConversionUnsafe(ty, retTy) } else { conversions[i] = GetConversion(ty, retTy) } if conversions[i] == nil { return cty.NilType, nil } } return retTy, conversions } func unifyTupleTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { // If we had any dynamic types in the input here then we can't predict // what path we'll take through here once these become known types, so // we'll conservatively produce DynamicVal for these. if hasDynamic { return unifyAllAsDynamic(types) } // There are two different ways we can succeed here: // - If all of the given tuple types have the same sequence of element types // and the corresponding types are all unifyable, then we construct that // type. // - If the given tuple types have different element types or their // corresponding types are not unifyable, we'll instead try to unify // all of the elements types together to produce a list type. firstEtys := types[0].TupleElementTypes() for _, ty := range types[1:] { thisEtys := ty.TupleElementTypes() if len(thisEtys) != len(firstEtys) { // If number of elements is different then there can be no // tuple type in common. return unifyTupleTypesToList(types, unsafe) } } // If we get here then we've proven that all of the given tuple types // have the same number of elements, though the types may differ. retEtys := make([]cty.Type, len(firstEtys)) atysAcross := make([]cty.Type, len(types)) for idx := range firstEtys { for tyI, ty := range types { atysAcross[tyI] = ty.TupleElementTypes()[idx] } retEtys[idx], _ = unify(atysAcross, unsafe) if retEtys[idx] == cty.NilType { // Cannot unify this element alone, which means that unification // of everything down to a map type can't be possible either. return cty.NilType, nil } } retTy := cty.Tuple(retEtys) conversions := make([]Conversion, len(types)) for i, ty := range types { if ty.Equals(retTy) { continue } if unsafe { conversions[i] = GetConversionUnsafe(ty, retTy) } else { conversions[i] = GetConversion(ty, retTy) } if conversions[i] == nil { // Shouldn't be reachable, since we were able to unify return unifyTupleTypesToList(types, unsafe) } } return retTy, conversions } func unifyTupleTypesToList(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { // This is our fallback case for unifyTupleTypes, where we see if we can // construct a list type that can accept all of the element types. var etys []cty.Type for _, ty := range types { for _, ety := range ty.TupleElementTypes() { etys = append(etys, ety) } } ety, _ := unify(etys, unsafe) if ety == cty.NilType { return cty.NilType, nil } retTy := cty.List(ety) conversions := make([]Conversion, len(types)) for i, ty := range types { if ty.Equals(retTy) { continue } if unsafe { conversions[i] = GetConversionUnsafe(ty, retTy) } else { conversions[i] = GetConversion(ty, retTy) } if conversions[i] == nil { // Shouldn't be reachable, since we were able to unify return unifyObjectTypesToMap(types, unsafe) } } return retTy, conversions } func unifyAllAsDynamic(types []cty.Type) (cty.Type, []Conversion) { conversions := make([]Conversion, len(types)) for i := range conversions { conversions[i] = func(cty.Value) (cty.Value, error) { return cty.DynamicVal, nil } } return cty.DynamicPseudoType, conversions }