package convert import ( "github.com/zclconf/go-cty/cty" ) // conversion is an internal variant of Conversion that carries around // a cty.Path to be used in error responses. type conversion func(cty.Value, cty.Path) (cty.Value, error) func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion { conv := getConversionKnown(in, out, unsafe) if conv == nil { return nil } // Wrap the conversion in some standard checks that we don't want to // have to repeat in every conversion function. var ret conversion ret = func(in cty.Value, path cty.Path) (cty.Value, error) { if in.IsMarked() { // We must unmark during the conversion and then re-apply the // same marks to the result. in, inMarks := in.Unmark() v, err := ret(in, path) if v != cty.NilVal { v = v.WithMarks(inMarks) } return v, err } if out == cty.DynamicPseudoType { // Conversion to DynamicPseudoType always just passes through verbatim. return in, nil } if isKnown, isNull := in.IsKnown(), in.IsNull(); !isKnown || isNull { // Avoid constructing unknown or null values with types which // include optional attributes. Known or non-null object values // will be passed to a conversion function which drops the optional // attributes from the type. Unknown and null pass through values // must do the same to ensure that homogeneous collections have a // single element type. out = out.WithoutOptionalAttributesDeep() if !isKnown { return prepareUnknownResult(in.Range(), 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(dynamicReplace(in.Type(), out)), nil } } return conv(in, path) } return ret } func getConversionKnown(in cty.Type, out cty.Type, unsafe bool) conversion { switch { case out == cty.DynamicPseudoType: // Conversion *to* DynamicPseudoType means that the caller wishes // to allow any type in this position, so we'll produce a do-nothing // conversion that just passes through the value as-is. return dynamicPassthrough case unsafe && in == cty.DynamicPseudoType: // Conversion *from* DynamicPseudoType means that we have a value // whose type isn't yet known during type checking. For these we will // assume that conversion will succeed and deal with any errors that // result (which is why we can only do this when "unsafe" is set). return dynamicFixup(out) case in.IsPrimitiveType() && out.IsPrimitiveType(): conv := primitiveConversionsSafe[in][out] if conv != nil { return conv } if unsafe { return primitiveConversionsUnsafe[in][out] } return nil case out.IsObjectType() && in.IsObjectType(): return conversionObjectToObject(in, out, unsafe) case out.IsTupleType() && in.IsTupleType(): return conversionTupleToTuple(in, out, unsafe) case out.IsListType() && (in.IsListType() || in.IsSetType()): inEty := in.ElementType() outEty := out.ElementType() if inEty.Equals(outEty) { // This indicates that we're converting from list to set with // the same element type, so we don't need an element converter. return conversionCollectionToList(outEty, nil) } convEty := getConversion(inEty, outEty, unsafe) if convEty == nil { return nil } return conversionCollectionToList(outEty, convEty) case out.IsSetType() && (in.IsListType() || in.IsSetType()): if in.IsListType() && !unsafe { // Conversion from list to map is unsafe because it will lose // information: the ordering will not be preserved, and any // duplicate elements will be conflated. return nil } inEty := in.ElementType() outEty := out.ElementType() convEty := getConversion(inEty, outEty, unsafe) if inEty.Equals(outEty) { // This indicates that we're converting from set to list with // the same element type, so we don't need an element converter. return conversionCollectionToSet(outEty, nil) } if convEty == nil { return nil } return conversionCollectionToSet(outEty, convEty) case out.IsMapType() && in.IsMapType(): inEty := in.ElementType() outEty := out.ElementType() convEty := getConversion(inEty, outEty, unsafe) if convEty == nil { return nil } return conversionCollectionToMap(outEty, convEty) case out.IsListType() && in.IsTupleType(): outEty := out.ElementType() return conversionTupleToList(in, outEty, unsafe) case out.IsSetType() && in.IsTupleType(): outEty := out.ElementType() return conversionTupleToSet(in, outEty, unsafe) case out.IsMapType() && in.IsObjectType(): outEty := out.ElementType() return conversionObjectToMap(in, outEty, unsafe) case out.IsObjectType() && in.IsMapType(): if !unsafe { // Converting a map to an object is an "unsafe" conversion, // because we don't know if all the map keys will correspond to // object attributes. return nil } return conversionMapToObject(in, out, unsafe) case in.IsCapsuleType() || out.IsCapsuleType(): if !unsafe { // Capsule types can only participate in "unsafe" conversions, // because we don't know enough about their conversion behaviors // to be sure that they will always be safe. return nil } if in.Equals(out) { // conversion to self is never allowed return nil } if out.IsCapsuleType() { if fn := out.CapsuleOps().ConversionTo; fn != nil { return conversionToCapsule(in, out, fn) } } if in.IsCapsuleType() { if fn := in.CapsuleOps().ConversionFrom; fn != nil { return conversionFromCapsule(in, out, fn) } } // No conversion operation is available, then. return nil default: return nil } } // retConversion wraps a conversion (internal type) so it can be returned // as a Conversion (public type). func retConversion(conv conversion) Conversion { if conv == nil { return nil } return func(in cty.Value) (cty.Value, error) { return conv(in, cty.Path(nil)) } } // prepareUnknownResult can apply value refinements to a returned unknown value // in certain cases where characteristics of the source value or type can // transfer into range constraints on the result value. func prepareUnknownResult(sourceRange cty.ValueRange, targetTy cty.Type) cty.Value { sourceTy := sourceRange.TypeConstraint() ret := cty.UnknownVal(targetTy) if sourceRange.DefinitelyNotNull() { ret = ret.RefineNotNull() } switch { case sourceTy.IsObjectType() && targetTy.IsMapType(): // A map built from an object type always has the same number of // elements as the source type has attributes. return ret.Refine().CollectionLength(len(sourceTy.AttributeTypes())).NewValue() case sourceTy.IsTupleType() && targetTy.IsListType(): // A list built from a typle type always has the same number of // elements as the source type has elements. return ret.Refine().CollectionLength(sourceTy.Length()).NewValue() case sourceTy.IsTupleType() && targetTy.IsSetType(): // When building a set from a tuple type we can't exactly constrain // the length because some elements might coalesce, but we can // guarantee an upper limit. We can also guarantee at least one // element if the tuple isn't empty. switch l := sourceTy.Length(); l { case 0, 1: return ret.Refine().CollectionLength(l).NewValue() default: return ret.Refine(). CollectionLengthLowerBound(1). CollectionLengthUpperBound(sourceTy.Length()). NewValue() } case sourceTy.IsCollectionType() && targetTy.IsCollectionType(): // NOTE: We only reach this function if there is an available // conversion between the source and target type, so we don't // need to repeat element type compatibility checks and such here. // // If the source value already has a refined length then we'll // transfer those refinements to the result, because conversion // does not change length (aside from set element coalescing). b := ret.Refine() if targetTy.IsSetType() { if sourceRange.LengthLowerBound() > 0 { // If the source has at least one element then the result // must always have at least one too, because value coalescing // cannot totally empty the set. b = b.CollectionLengthLowerBound(1) } } else { b = b.CollectionLengthLowerBound(sourceRange.LengthLowerBound()) } b = b.CollectionLengthUpperBound(sourceRange.LengthUpperBound()) return b.NewValue() default: return ret } }