v2/tools/generator/internal/conversions/property_conversions.go (1,657 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package conversions
import (
"fmt"
"go/token"
"github.com/dave/dst"
"github.com/rotisserie/eris"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astbuilder"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel"
)
// PropertyConversion generates the AST for a given property conversion.
// reader is an expression to read the original value.
// writer is a function that accepts an expression for reading a value and creates one or more
// statements to write that value.
// Both of these might be complex expressions, possibly involving indexing into arrays or maps.
type PropertyConversion func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error)
// PropertyConversionFactory represents factory methods that can be used to create a PropertyConversion for a specific
// pair of properties
// source is the property conversion endpoint that will be read
// destination is the property conversion endpoint that will be written
// ctx contains additional information that may be needed when creating the property conversion
//
// The factory should return one of three result sets:
// - For a fatal error, one that guarantees no conversion can be generated, return (nil, error)
// This will abort the conversion process and return an error for logging.
// - For a valid conversion, return (conversion, nil)
// - When no conversion could be generated by this factory, return (nil, nil) to delegate to another factory
//
// Each conversion should be written with lead predicates to make sure that it only fires in the correct circumstances.
// This requires, in particular, that most conversions check for optionality and bag items and exit early when those are
// found.
// Phrased another way, conversions should not rely on the order of listing in propertyConversionFactories in order to
// generate the correct code; any conversion that relies on being "protected" from particular situations by having other
// conversions earlier in the list held by propertyConversionFactories is brittle and likely to generate the incorrect
// code if the order of items in the list is modified.
type PropertyConversionFactory func(
source *TypedConversionEndpoint,
destination *TypedConversionEndpoint,
conversionContext *PropertyConversionContext) (PropertyConversion, error)
// A list of all known type conversion factory methods
var propertyConversionFactories []PropertyConversionFactory
func init() {
propertyConversionFactories = []PropertyConversionFactory{
// Property bag items
pullFromBagItem,
writeToBagItem,
// Primitive definitions and aliases
assignPrimitiveFromPrimitive,
assignAliasedPrimitiveFromAliasedPrimitive,
// Handcrafted implementations in genruntime
assignHandcraftedImplementations,
// Some conversions are forbidden and we just skip them
neuterForbiddenConversions,
// Collection Types
assignArrayFromArray,
assignMapFromMap,
// Enumerations
assignEnumFromEnum,
assignPrimitiveFromEnum,
// Complex object definitions
assignObjectDirectlyFromObject,
assignObjectDirectlyToObject,
assignInlineObjectsViaIntermediateObject,
assignNonInlineObjectsViaPivotObject,
// Known definitions
assignUserAssignedIdentityMapFromArray,
copyKnownType(astmodel.KnownResourceReferenceType, "Copy", returnsValue),
copyKnownType(astmodel.ResourceReferenceType, "Copy", returnsValue),
copyKnownType(astmodel.SecretReferenceType, "Copy", returnsValue),
copyKnownType(astmodel.SecretMapReferenceType, "Copy", returnsValue),
copyKnownType(astmodel.SecretDestinationType, "Copy", returnsValue),
copyKnownType(astmodel.ConfigMapReferenceType, "Copy", returnsValue),
copyKnownType(astmodel.ConfigMapDestinationType, "Copy", returnsValue),
copyKnownType(astmodel.DestinationExpressionType, "DeepCopy", returnsReference),
copyKnownType(astmodel.ArbitraryOwnerReference, "Copy", returnsValue),
copyKnownType(astmodel.ConditionType, "Copy", returnsValue),
copyKnownType(astmodel.JSONType, "DeepCopy", returnsReference),
copyKnownType(astmodel.ObjectMetaType, "DeepCopy", returnsReference),
// Meta-conversions
assignFromOptional,
assignToOptional,
assignToEnumeration,
assignFromAliasedType,
assignToAliasedType,
}
}
// CreateTypeConversion tries to create a type conversion between the two provided definitions, using
// all of the available type conversion functions in priority order to do so.
//
// The method works by considering the conversion requested by sourceEndpoint & destinationEndpoint,
// with recursive calls breaking the conversion down into multiple steps that are then combined.
//
// Example:
//
// CreateTypeConversion() is called to create a conversion from an optional string to an optional
// Sku, where Sku is a new type based on string:
//
// source *string => destination *Sku
//
// assuming
//
// type Sku string
//
// assignFromOptional can handle the optionality of sourceEndpoint and makes a recursive call
// to CreateTypeConversion() with the simpler target:
//
// source string => destination *Sku
//
// assignToOptional can handle the optionality of destinationEndpoint and makes a recursive
// call to CreateTypeConversion() with a simpler target:
//
// source string => destination Sku
//
// assignToAliasedPrimitive can handle the type conversion of string to Sku, and makes
// a recursive call to CreateTypeConversion() with a simpler target:
//
// source string => destination string
//
// assignPrimitiveFromPrimitive can handle primitive values, and generates a
// conversion that does a simple assignment:
//
// destination = source
//
// assignToAliasedPrimitive injects the necessary type conversion:
//
// destination = Sku(source)
//
// assignToOptional injects a local variable and takes it's address
//
// sku := Sku(source)
// destination = &sku
//
// finally, assignFromOptional injects the check to see if we have a value to assign in the
// first place, assigning a suitable zero value if we don't:
//
// if source != nil {
// sku := Sku(source)
// destination := &sku
// } else {
//
// destination := ""
// }
func CreateTypeConversion(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
var result PropertyConversion
var err error
for _, f := range propertyConversionFactories {
result, err = f(sourceEndpoint, destinationEndpoint, conversionContext)
if err != nil {
// Fatal error, no conversion possible
break
}
if result != nil {
// Conversion found, return it
return result, nil
}
}
// No conversion found, we need to generate a useful error message, wrapping any existing error.
// If the endpoints are in different packages, we want both to be qualfied with their package name.
// We finesse this by cross wiring the packges passed so that we only get simplified descriptions if they are
// in the same package.
describe := func(subject astmodel.Type, ref astmodel.Type) string {
if tn, ok := astmodel.AsInternalTypeName(ref); ok {
return astmodel.DebugDescription(subject, tn.InternalPackageReference())
}
return astmodel.DebugDescription(subject)
}
srcType := sourceEndpoint.Type()
dstType := destinationEndpoint.Type()
msg := fmt.Sprintf("no conversion found to assign %q from %q",
describe(dstType, srcType),
describe(srcType, dstType))
if err != nil {
err = eris.Wrap(err, msg)
} else {
err = eris.New(msg)
}
return nil, err
}
// NameOfPropertyAssignmentFunction returns the name of the property assignment function
func NameOfPropertyAssignmentFunction(
baseName string,
parameterType astmodel.TypeName,
direction Direction,
idFactory astmodel.IdentifierFactory,
) string {
nameOfOtherType := idFactory.CreateIdentifier(parameterType.Name(), astmodel.Exported)
dir := direction.SelectString("From", "To")
return fmt.Sprintf("%s_%s_%s", baseName, dir, nameOfOtherType)
}
// directAssignmentPropertyConversion is a helper function for creating a conversion that does a direct assignment
func directAssignmentPropertyConversion(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
_ *astmodel.KnownLocalsSet,
_ *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
return writer(reader), nil
}
// writeToBagItem will generate a conversion where the destination is in our property bag
//
// # For non-optional sources, the value is directly added
//
// <propertyBag>.Add(<propertyName>, <source>)
//
// For optional sources, the value is only added if non-nil; if nil, we remove any existing item
//
// if <source> != nil {
// <propertyBag>.Add(<propertyName>, *<source>)
// } else {
//
// <propertyBag>.Remove(<propertyName>)
// }
//
// For slice and slice sources, the value is only added if it is non-empty; if empty we remove any existing item
//
// if len(<source>) > 0 {
// <propertyBag>.Add(<propertyName>, <source>)
// } else {
//
// <propertyBag>.Remove(<propertyName>)
// }
//
// If the type within the property bag differs from the source type, a type conversion is recursively sought
func writeToBagItem(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require destination to be a property bag item
destinationBagItem, destinationIsBagItem := AsPropertyBagMemberType(destinationEndpoint.Type())
if !destinationIsBagItem {
// Destination is not optional
return nil, nil
}
// Work out our source type, and whether it's optional
actualSourceType := sourceEndpoint.Type()
sourceOptional, sourceIsOptional := astmodel.AsOptionalType(actualSourceType)
if sourceIsOptional {
actualSourceType = sourceOptional.BaseType()
sourceEndpoint = sourceEndpoint.WithType(actualSourceType)
}
// If the item in the bag is exactly the same type as our source, we don't need any other conversion.
// We don't want to recursively look for more expensive conversions if we don't need to.
// Plus, conversions are designed to isolate the source and destination from each other (so that changes to one
// don't impact the other), but with the property bag everything gets immediately serialized so everything is
// already nicely isolated.
// On the other hand, if the types are different, we need to look for a conversion.
conversion := directAssignmentPropertyConversion
if !astmodel.TypeEquals(destinationBagItem.Element(), actualSourceType) {
// Look for a conversion between the bag item and our source
bagItemEndpoint := destinationEndpoint.WithType(destinationBagItem.Element())
c, err := CreateTypeConversion(sourceEndpoint, bagItemEndpoint, conversionContext)
if err != nil {
return nil, err
}
if c == nil {
return nil, nil
}
conversion = c
}
_, sourceIsMap := astmodel.AsMapType(actualSourceType)
_, sourceIsSlice := astmodel.AsArrayType(actualSourceType)
return func(
reader dst.Expr,
_ func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// propertyBag.Add(<propertyName>, <source>)
createAddToBag := func(expr dst.Expr) []dst.Stmt {
addToBag := astbuilder.CallQualifiedFuncAsStmt(
conversionContext.PropertyBagName(),
"Add",
astbuilder.StringLiteral(destinationEndpoint.Name()),
expr)
return astbuilder.Statements(addToBag)
}
// propertyBag.Remove(<propertyName>)
removeFromBag := astbuilder.CallQualifiedFuncAsStmt(
conversionContext.PropertyBagName(),
"Remove",
astbuilder.StringLiteral(destinationEndpoint.Name()))
// condition is a test to use to see whether we have a value to write to the property bag
// If we unilaterally write to the bag, this will be nil
var condition dst.Expr
// If optional source, check for nil and only store if we have a value
if sourceIsOptional {
// if <reader> != nil {
condition = astbuilder.NotNil(reader)
// To read the actual value, we need to dereference the pointer
reader = astbuilder.Dereference(reader)
// We're wrapping the conversion in a nested block, so any locals are independent
knownLocals = knownLocals.Clone()
}
// If slice or map, check for non-empty and only store if we have a value
if sourceIsSlice || sourceIsMap {
// if len(<mapOrSlice>) > 0 {
condition = astbuilder.NotEmpty(reader)
// We're wrapping the conversion in a nested block, so any locals are independent
knownLocals = knownLocals.Clone()
}
// Create the conversion to use to write to the bag
addToBag, err := conversion(
reader,
createAddToBag,
knownLocals,
generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"unable to convert %s to %s writing property bag",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
// If we only conditionally write to the bag, we need wrap with an if statement
if condition != nil {
writer := astbuilder.SimpleIfElse(
condition,
addToBag,
astbuilder.Statements(removeFromBag))
return astbuilder.Statements(writer), nil
}
// Otherwise, just add the value to the bag
return astbuilder.Statements(addToBag), nil
}, nil
}
// assignToOptional will generate a conversion where the destination is optional, if the
// underlying type of the destination is compatible with the source.
//
// <destination> = &<source>
func assignToOptional(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require destination to not be a bag item
if destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require destination to be optional
destinationOptional, destinationIsOptional := astmodel.AsOptionalType(destinationEndpoint.Type())
if !destinationIsOptional {
// Destination is not optional
return nil, nil
}
// Require source to be non-optional
// (to ensure that assignFromOptional triggers first when handling option to optional conversion)
if sourceEndpoint.IsOptional() {
return nil, nil
}
// Require a conversion between the unwrapped type and our source
unwrappedEndpoint := destinationEndpoint.WithType(destinationOptional.Element())
conversion, err := CreateTypeConversion(sourceEndpoint, unwrappedEndpoint, conversionContext)
if err != nil {
return nil, err
}
if conversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// Create a writer that uses the address of the passed expression
// If expr isn't a plain identifier (implying a local variable), we introduce one
// This both allows us to avoid aliasing and complies with Go language semantics
addrOfWriter := func(expr dst.Expr) []dst.Stmt {
if _, ok := expr.(*dst.Ident); ok {
return writer(astbuilder.AddrOf(expr))
}
// Only obtain our local variable name after we know we need it
// (this avoids reserving the name and not using it, which can interact with other conversions)
local := knownLocals.CreateSingularLocal(destinationEndpoint.Name(), "", "Temp")
assignment := astbuilder.ShortDeclaration(local, expr)
writing := writer(astbuilder.AddrOf(dst.NewIdent(local)))
return astbuilder.Statements(assignment, writing)
}
return conversion(reader, addrOfWriter, knownLocals, generationContext)
}, nil
}
// pullFromBagItem will populate a property from a property bag
//
// if <propertyBag>.Contains(<sourceName>) {
// var <value> <destinationType>
// err := <propertyBag>.Pull(<sourceName>, &<value>)
// if err != nil {
// return errors.Wrapf(err, ...)
// }
//
// <destination> = <value>
// } else {
//
// <destination> = <zero>
// }
//
// If the type within the property bag differs from the destination type, a type conversion is recursively sought
func pullFromBagItem(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require source to be a bag item
sourceBagItem, sourceIsBagItem := AsPropertyBagMemberType(sourceEndpoint.Type())
if !sourceIsBagItem {
return nil, nil
}
// Work out our destination type, and whether it's optional
actualDestinationType := destinationEndpoint.Type()
destinationOptional, destinationIsOptional := astmodel.AsOptionalType(actualDestinationType)
if destinationIsOptional {
actualDestinationType = destinationOptional.BaseType()
}
// If the item in the bag is exactly the same type as our destination, we don't need any other conversion
// We don't want to recursively look for more expensive conversions if we don't need to.
// Plus, conversions are designed to isolate the source and destination from each other (so that changes to one
// don't impact the other), but with the property bag everything gets immediately serialized so everything is
// already nicely isolated.
// On the other hand, if the types are different, we need to look for a conversion
conversion := directAssignmentPropertyConversion
typesDiffer := !astmodel.TypeEquals(sourceBagItem.Element(), actualDestinationType)
if typesDiffer {
// Look for a conversion between the bag item and our source
bagItemEndpoint := sourceEndpoint.WithType(sourceBagItem.Element())
c, err := CreateTypeConversion(bagItemEndpoint, destinationEndpoint, conversionContext)
if err != nil {
return nil, err
}
if c == nil {
return nil, nil
}
conversion = c
}
errIdent := dst.NewIdent("err")
return func(
_ dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// our first parameter is an expression to read the value from our original instance, but in this case we're
// going to read from the property bag, so we're ignoring it.
// Work out a name for our local variable
// We use different defaults when doing a conversion to make the local naming clearer in the generated code
var local string
if typesDiffer {
local = knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "FromBag", "ReadFromBag")
} else {
local = knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "", "Read")
}
errorsPkg := generationContext.MustGetImportedPackageName(astmodel.ErisReference)
// propertyBag.Contains("<sourceName>")
condition := astbuilder.CallQualifiedFunc(
conversionContext.PropertyBagName(),
"Contains",
astbuilder.StringLiteral(sourceEndpoint.Name()))
// var <local> <sourceBagItemType>
sourceBagItemExpr, err := sourceBagItem.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"converting %s to %s reading property bag",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
declare := astbuilder.NewVariableWithType(
local,
sourceBagItemExpr)
// We're wrapping the conversion in a nested block, so any locals are independent
knownLocals = knownLocals.Clone()
// We have to do this at render time in order to ensure the first conversion generated
// declares 'err', not a later one
tok := token.ASSIGN
if knownLocals.TryCreateLocal("err") {
tok = token.DEFINE
}
// err := <propertyBag>.Pull(<sourceName>, &<local>)
pull := astbuilder.AssignmentStatement(
dst.NewIdent("err"),
tok,
astbuilder.CallQualifiedFunc(
conversionContext.PropertyBagName(),
"Pull",
astbuilder.StringLiteral(sourceEndpoint.Name()),
astbuilder.AddrOf(dst.NewIdent(local))))
// if err != nil {
// return ...
// }
returnIfErr := astbuilder.ReturnIfNotNil(
errIdent,
astbuilder.WrappedErrorf(
errorsPkg,
"pulling '%s' from propertyBag",
sourceEndpoint.Name()))
returnIfErr.Decorations().After = dst.EmptyLine
var reader dst.Expr
if destinationIsOptional {
reader = astbuilder.AddrOf(dst.NewIdent(local))
} else {
reader = dst.NewIdent(local)
}
// Create the actual code to store the value
assignValue, err := conversion(reader, writer, knownLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"unable to convert %s to %s reading property bag",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
// Generate code to clear the value if we don't have one
assignZero := writer(
destinationEndpoint.Type().AsZero(conversionContext.Types(),
generationContext))
// if <condition> {
// <declare, pull, returnIfErr, assignValue>
// } else {
// <assignZero>
// }
ifStatement := astbuilder.SimpleIfElse(
condition,
astbuilder.Statements(declare, pull, returnIfErr, assignValue),
assignZero)
return astbuilder.Statements(ifStatement), nil
}, nil
}
// assignFromOptional will handle the case where the source type may be missing (nil)
//
// <original> := <source>
//
// if <original> != nil {
// <destination> = *<original>
// } else {
//
// <destination> = <zero>
// }
//
// Must trigger before assignToOptional so we generate the right zero values; to enforce this, assignToOptional includes
// a predicate check that the source is NOT optional, allowing this conversion to trigger first.
func assignFromOptional(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require source to not be a bag item
if sourceEndpoint.IsBagItem() {
return nil, nil
}
// Require source to be optional
sourceOptional, sourceIsOptional := astmodel.AsOptionalType(sourceEndpoint.Type())
if !sourceIsOptional {
return nil, nil
}
// Require a conversion between the unwrapped type and our source
unwrappedEndpoint := sourceEndpoint.WithType(sourceOptional.Element())
conversion, err := CreateTypeConversion(
unwrappedEndpoint,
destinationEndpoint,
conversionContext)
if err != nil {
return nil, err
}
if conversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
var cacheOriginal dst.Stmt
var actualReader dst.Expr
// If the value we're reading is a local or a field, it's cheap to read and we can skip
// using a local (which makes the generated code easier to read). In other cases, we want
// to cache the value in a local to avoid repeating any expensive conversion.
switch reader.(type) {
case *dst.Ident, *dst.SelectorExpr:
// reading a local variable or a field
cacheOriginal = nil
actualReader = reader
default:
// Something else, so we cache the original
local := knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "", "AsRead")
cacheOriginal = astbuilder.ShortDeclaration(local, reader)
actualReader = dst.NewIdent(local)
}
checkForNil := astbuilder.AreNotEqual(actualReader, astbuilder.Nil())
// If we have a value, need to convert it to our destination type
// We use a cloned knownLocals as the Write is within our if statement, and we don't want locals to leak
writeActualValue, err := conversion(
astbuilder.Dereference(actualReader),
writer,
knownLocals.Clone(),
generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"unable to convert %s to %s",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
writeZeroValue := writer(
destinationEndpoint.Type().AsZero(conversionContext.Types(), generationContext))
stmt := astbuilder.SimpleIfElse(
checkForNil,
writeActualValue,
writeZeroValue)
return astbuilder.Statements(cacheOriginal, stmt), nil
}, nil
}
// assignToEnumeration will generate a conversion where the destination is an enumeration if
// the source is type compatible with the base type of the enumeration
//
// <destination> = <enumeration-cast>(<source>)
func assignToEnumeration(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require destination to not be a bag item
if destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require destination to be non-optional
if destinationEndpoint.IsOptional() {
return nil, nil
}
// Require destination to be an enumeration
dstName, dstType, ok := conversionContext.ResolveType(destinationEndpoint.Type())
if !ok {
return nil, nil
}
dstEnum, ok := astmodel.AsEnumType(dstType)
if !ok {
return nil, nil
}
// Require a conversion between the base type of the enumeration and our source
dstEp := destinationEndpoint.WithType(dstEnum.BaseType())
conversion, err := CreateTypeConversion(sourceEndpoint, dstEp, conversionContext)
if err != nil {
return nil, err
}
if conversion == nil {
return nil, nil
}
conversionContext.AddPackageReference(astmodel.StringsReference)
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// If the enum is NOT based on a string, we can just do a direct cast and keep things simple
if dstEnum.BaseType() != astmodel.StringType {
return writer(astbuilder.CallFunc(dstName.Name(), reader)), nil
}
var cacheOriginal dst.Stmt
var actualReader dst.Expr
// If the value we're reading is a local or a field, it's cheap to read and we can skip
// using a local (which makes the generated code easier to read). In other cases, we want
// to cache the value in a local to avoid repeating any expensive conversion.
switch reader.(type) {
case *dst.Ident, *dst.SelectorExpr:
// reading a local variable or a field
cacheOriginal = nil
actualReader = reader
default:
// Something else, so we cache the original
local := knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "", "Value", "Cache")
cacheOriginal = astbuilder.ShortDeclaration(local, reader)
actualReader = dst.NewIdent(local)
}
dstNameExpr, err := dstName.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"unable to convert %s to %s",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
if dstEnum.NeedsMappingConversion(dstName) {
// We need to use the values mapping to convert the value in a case-insensitive way
mapperID := dstEnum.MapperVariableName(dstName)
genruntimePkg := generationContext.MustGetImportedPackageName(astmodel.GenRuntimeReference)
// genruntime.ToEnum(<actualReader>, <mapperId>)
toEnum := astbuilder.CallQualifiedFunc(
genruntimePkg,
"ToEnum",
actualReader,
dst.NewIdent(mapperID))
convert, err := conversion(
toEnum,
writer,
knownLocals,
generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"unable to convert %s to %s",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
return astbuilder.Statements(cacheOriginal, convert), nil
}
// Otherwise we just do a direct cast
castingWriter := func(expr dst.Expr) []dst.Stmt {
cast := &dst.CallExpr{
Fun: dstNameExpr,
Args: []dst.Expr{expr},
}
return writer(cast)
}
return conversion(
reader,
castingWriter,
knownLocals,
generationContext)
}, nil
}
// assignPrimitiveFromPrimitive will generate a direct assignment if both definitions have the
// same primitive type and are not optional
//
// <destination> = <source>
func assignPrimitiveFromPrimitive(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
_ *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be a primitive type
sourcePrimitive, sourceIsPrimitive := astmodel.AsPrimitiveType(sourceEndpoint.Type())
if !sourceIsPrimitive {
return nil, nil
}
// Require destination to be a primitive type
destinationPrimitive, destinationIsPrimitive := astmodel.AsPrimitiveType(destinationEndpoint.Type())
if !destinationIsPrimitive {
return nil, nil
}
// Require both properties to have the same primitive type
if !astmodel.TypeEquals(sourcePrimitive, destinationPrimitive) {
return nil, nil
}
return directAssignmentPropertyConversion, nil
}
// assignAliasedPrimitiveFromAliasedPrimitive will generate a direct assignment if both
// definitions have the same underlying primitive type and are not optional
//
// <destination> = <cast>(<source>)
func assignAliasedPrimitiveFromAliasedPrimitive(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be a name that resolves to a primitive type
_, sourceType, ok := conversionContext.ResolveType(sourceEndpoint.Type())
if !ok {
return nil, nil
}
sourcePrimitive, sourceIsPrimitive := astmodel.AsPrimitiveType(sourceType)
if !sourceIsPrimitive {
return nil, nil
}
// Require destination to be a name the resolves to a primitive type
destinationName, destinationType, ok := conversionContext.ResolveType(destinationEndpoint.Type())
if !ok {
return nil, nil
}
destinationPrimitive, destinationIsPrimitive := astmodel.AsPrimitiveType(destinationType)
if !destinationIsPrimitive {
return nil, nil
}
// Require both properties to have the same primitive type
if !astmodel.TypeEquals(sourcePrimitive, destinationPrimitive) {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
destinationNameExpr, err := destinationName.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(err, "creating destination expression")
}
return writer(&dst.CallExpr{
Fun: destinationNameExpr,
Args: []dst.Expr{reader},
}), nil
}, nil
}
// assignFromAliasedType will convert an alias of a type into that type
// type as long as it is not optional and is not an alias to an object type.
func assignFromAliasedType(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require source to not be a bag item
if sourceEndpoint.IsBagItem() {
return nil, nil
}
// Require source to be non-optional
if sourceEndpoint.IsOptional() {
return nil, nil
}
// Require source to be a name that resolves to a non-object type
_, sourceType, ok := conversionContext.ResolveType(sourceEndpoint.Type())
if !ok {
return nil, nil
}
if _, ok = astmodel.AsObjectType(sourceType); ok {
// Don't match objects, only other aliases - no objects aliases exist because they are removed by
// the RemoveTypeAliases pipeline stage, so if ResolveType results in an object then we have a normal
// TypeName -> Object, which is not an alias at all.
return nil, nil
}
// Require a conversion for the underlying type
sourceType = astmodel.Unwrap(sourceType) // Unwrap to avoid any validations
updatedSourceEndpoint := sourceEndpoint.WithType(sourceType)
conversion, err := CreateTypeConversion(updatedSourceEndpoint, destinationEndpoint, conversionContext)
if err != nil {
return nil, err
}
if conversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
sourceTypeExpr, err := sourceType.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(err, "creating source expression")
}
actualReader := &dst.CallExpr{
Fun: sourceTypeExpr,
Args: []dst.Expr{reader},
}
return conversion(actualReader, writer, knownLocals, generationContext)
}, nil
}
// assignToAliasedType will convert a value into the aliased type as long as it
// is not optional and the alias is not to an object type.
//
// <destination> = <cast>(<source>)
func assignToAliasedType(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require destination to not be a bag item
if destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require destination to be non-optional
if destinationEndpoint.IsOptional() {
return nil, nil
}
// Require destination to be a name the resolves to a non-object type
destinationName, destinationType, ok := conversionContext.ResolveType(destinationEndpoint.Type())
if !ok {
return nil, nil
}
if _, ok = astmodel.AsObjectType(destinationType); ok {
// Don't match objects, only other aliases - no objects aliases exist because they are removed by
// the RemoveTypeAliases pipeline stage, so if ResolveType results in an object then we have a normal
// TypeName -> Object, which is not an alias at all.
return nil, nil
}
// Require a conversion for the underlying type
destinationType = astmodel.Unwrap(destinationType) // Unwrap to avoid any validations
updatedDestinationEndpoint := destinationEndpoint.WithType(destinationType)
conversion, err := CreateTypeConversion(sourceEndpoint, updatedDestinationEndpoint, conversionContext)
if err != nil {
return nil, err
}
if conversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
destinationNameExpr, err := destinationName.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(err, "creating destination expression")
}
actualWriter := func(expr dst.Expr) []dst.Stmt {
castToAlias := &dst.CallExpr{
Fun: destinationNameExpr,
Args: []dst.Expr{expr},
}
return writer(castToAlias)
}
return conversion(reader, actualWriter, knownLocals, generationContext)
}, nil
}
// handCraftedConversion represents a hand-coded conversion
// this can be used to share code for common conversions (e.g. []string → []string)
type handCraftedConversion struct {
fromType astmodel.Type
toType astmodel.Type
implPackage astmodel.PackageReference
implFunc string
}
var handCraftedConversions = []handCraftedConversion{
{
fromType: astmodel.NewMapType(astmodel.StringType, astmodel.StringType),
toType: astmodel.NewMapType(astmodel.StringType, astmodel.StringType),
implPackage: astmodel.GenRuntimeReference,
implFunc: "CloneMapOfStringToString",
},
{
fromType: astmodel.NewArrayType(astmodel.StringType),
toType: astmodel.NewArrayType(astmodel.StringType),
implPackage: astmodel.GenRuntimeReference,
implFunc: "CloneSliceOfString",
},
{
fromType: astmodel.NewArrayType(astmodel.ConditionType),
toType: astmodel.NewArrayType(astmodel.ConditionType),
implPackage: astmodel.GenRuntimeReference,
implFunc: "CloneSliceOfCondition",
},
{
fromType: astmodel.OptionalIntType,
toType: astmodel.OptionalIntType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "ClonePointerToInt",
},
{
fromType: astmodel.OptionalStringType,
toType: astmodel.OptionalStringType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "ClonePointerToString",
},
{
fromType: astmodel.OptionalStringType,
toType: astmodel.StringType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "GetOptionalStringValue",
},
{
fromType: astmodel.OptionalIntType,
toType: astmodel.IntType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "GetOptionalIntValue",
},
{
fromType: astmodel.StringType,
toType: astmodel.ResourceReferenceType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "CreateResourceReferenceFromARMID",
},
{
fromType: astmodel.FloatType,
toType: astmodel.IntType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "GetIntFromFloat",
},
{
fromType: astmodel.JSONType,
toType: astmodel.StringType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "ConvertJSONToString",
},
{
fromType: astmodel.StringType,
toType: astmodel.JSONType,
implPackage: astmodel.GenRuntimeReference,
implFunc: "ConvertStringToJSON",
},
}
func assignHandcraftedImplementations(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Search for a handcrafted conversion to use
conversionFound := false
var conversion handCraftedConversion
for _, impl := range handCraftedConversions {
sourceType := sourceEndpoint.Type()
if vt, ok := astmodel.AsValidatedType(sourceType); ok {
// If the source is a validated type, we need to use the underlying type
sourceType = vt.ElementType()
}
destinationType := destinationEndpoint.Type()
if vt, ok := astmodel.AsValidatedType(destinationType); ok {
// If the destination is a validated type, we need to use the underlying type
destinationType = vt.ElementType()
}
if astmodel.TypeEquals(sourceType, impl.fromType) &&
astmodel.TypeEquals(destinationType, impl.toType) {
conversion = impl
conversionFound = true
break
}
}
if !conversionFound {
// No handcrafted conversion found
return nil, nil
}
// Make sure all the necessary packages are referenced
if ftn, ok := astmodel.AsTypeName(conversion.fromType); ok {
// Include a reference to the package our from type is found in
conversionContext.AddPackageReference(ftn.PackageReference())
}
if ttn, ok := astmodel.AsTypeName(conversion.toType); ok {
// Include a reference to the package our to type is found in
conversionContext.AddPackageReference(ttn.PackageReference())
}
// Include a reference to the package our implementation is found in
conversionContext.AddPackageReference(conversion.implPackage)
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
pkg := generationContext.MustGetImportedPackageName(conversion.implPackage)
return writer(astbuilder.CallQualifiedFunc(pkg, conversion.implFunc, reader)), nil
}, nil
}
// forbiddenConversion represents a conversion that we know we shouldn't even attempt to do
// Encountering one isn't a fatal error, but it does mean we can't generate a conversion
type forbiddenConversion struct {
fromType astmodel.Type
toType astmodel.Type
}
var forbiddenConversions = []forbiddenConversion{
{
// Can't use a string (the value of a secret) to initialize a secret reference (pointing to the value source)
// We encounter this when initializing the spec of a resource from its status
fromType: astmodel.StringType,
toType: astmodel.SecretReferenceType,
},
{
// Can't use a map[string]string (the value of a secret) to initialize a secret reference (pointing to the value source)
// We encounter this when initializing the spec of a resource from its status
fromType: astmodel.MapOfStringStringType,
toType: astmodel.SecretMapReferenceType,
},
}
// neuterForbiddenConversions is a conversion factory that will return a conversion that does nothing if we encounter a
// forbidden conversion
func neuterForbiddenConversions(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
_ *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
for _, forbidden := range forbiddenConversions {
if astmodel.TypeEquals(sourceEndpoint.Type(), forbidden.fromType) &&
astmodel.TypeEquals(destinationEndpoint.Type(), forbidden.toType) {
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
return nil, nil
}, nil
}
}
return nil, nil
}
// assignArrayFromArray will generate a code fragment to populate an array, assuming the
// underlying definitions of the two arrays are compatible
//
// <arr> := make([]<type>, len(<reader>))
//
// for <index>, <value> := range <reader> {
// // Shadow the loop variable to avoid aliasing
// <value> := <value>
// <arr>[<index>] := <value> // Or other conversion as required
// }
//
// <writer> = <arr>
func assignArrayFromArray(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require source to be an array type
sourceArray, sourceIsArray := astmodel.AsArrayType(sourceEndpoint.Type())
if !sourceIsArray {
return nil, nil
}
// Require destination to be an array type
destinationArray, destinationIsArray := astmodel.AsArrayType(destinationEndpoint.Type())
if !destinationIsArray {
return nil, nil
}
// Require a conversion between the array definitions
unwrappedSourceEndpoint := sourceEndpoint.WithType(sourceArray.Element())
unwrappedDestinationEndpoint := destinationEndpoint.WithType(destinationArray.Element())
conversion, err := CreateTypeConversion(
unwrappedSourceEndpoint,
unwrappedDestinationEndpoint,
conversionContext)
if err != nil {
return nil, eris.Wrapf(
err,
"finding array conversion from %s to %s",
astmodel.DebugDescription(sourceEndpoint.Type()),
astmodel.DebugDescription(destinationEndpoint.Type()))
}
if conversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
var cacheOriginal dst.Stmt
var actualReader dst.Expr
// If the value we're reading is a local or a field, it's cheap to read and we can skip
// using a local (which makes the generated code easier to read). In other cases, we want
// to cache the value in a local to avoid repeating any expensive conversion.
switch reader.(type) {
case *dst.Ident, *dst.SelectorExpr:
// reading a local variable or a field
cacheOriginal = nil
actualReader = reader
default:
// Something else, so we cache the original
local := knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "", "Cache")
cacheOriginal = astbuilder.ShortDeclaration(local, reader)
actualReader = dst.NewIdent(local)
}
checkForNil := astbuilder.AreNotEqual(actualReader, astbuilder.Nil())
// We create three obviously related identifiers to use for the array conversion
// The List is created in the current knownLocals scope because we need it after the loop completes.
// The other two are created in a nested knownLocals scope because they're only needed within the loop; this
// ensures any other locals needed for the conversion don't leak out into our main scope.
// These suffixes must not overlap with those used for map conversion. (If these suffixes overlap, the naming
// becomes difficult to read when converting maps containing slices or vice versa.)
branchLocals := knownLocals.Clone()
tempID := branchLocals.CreateSingularLocal(sourceEndpoint.Name(), "List")
loopLocals := branchLocals.Clone() // Clone after tempId is created so that it's visible within the loop
itemID := loopLocals.CreateSingularLocal(sourceEndpoint.Name(), "Item")
indexID := loopLocals.CreateSingularLocal(sourceEndpoint.Name(), "Index")
destinationArrayExpr, err := destinationArray.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"converting %s to %s",
sourceEndpoint.Name(),
destinationEndpoint.Name())
}
declaration := astbuilder.ShortDeclaration(
tempID,
astbuilder.MakeSlice(destinationArrayExpr, astbuilder.CallFunc("len", actualReader)))
writeToElement := func(expr dst.Expr) []dst.Stmt {
return astbuilder.Statements(
astbuilder.SimpleAssignment(
&dst.IndexExpr{
X: dst.NewIdent(tempID),
Index: dst.NewIdent(indexID),
},
expr),
)
}
avoidAliasing := astbuilder.ShortDeclaration(itemID, dst.NewIdent(itemID))
avoidAliasing.Decs.Start.Append("// Shadow the loop variable to avoid aliasing")
avoidAliasing.Decs.Before = dst.NewLine
elemConv, err := conversion(dst.NewIdent(itemID), writeToElement, loopLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating conversion for array element from %s to %s",
astmodel.DebugDescription(sourceEndpoint.Type()),
astmodel.DebugDescription(destinationEndpoint.Type()))
}
loopBody := astbuilder.Statements(avoidAliasing, elemConv)
assignValue := writer(dst.NewIdent(tempID))
loop := astbuilder.IterateOverSliceWithIndex(indexID, itemID, reader, loopBody...)
trueBranch := astbuilder.Statements(declaration, loop, assignValue)
assignZero := writer(astbuilder.Nil())
return astbuilder.Statements(
cacheOriginal,
astbuilder.SimpleIfElse(checkForNil, trueBranch, assignZero)), nil
}, nil
}
// assignMapFromMap will generate a code fragment to populate an array, assuming the
// underlying definitions of the two arrays are compatible
//
// if <reader> != nil {
// <map> := make(map[<key>]<type>)
// for key, <item> := range <reader> {
// <map>[<key>] := <item>
// }
// <writer> = <map>
// } else {
//
// <writer> = <zero>
// }
func assignMapFromMap(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require source to be a map
sourceMap, sourceIsMap := astmodel.AsMapType(sourceEndpoint.Type())
if !sourceIsMap {
// Source is not a map
return nil, nil
}
// Require destination to be a map
destinationMap, destinationIsMap := astmodel.AsMapType(destinationEndpoint.Type())
if !destinationIsMap {
// Destination is not a map
return nil, nil
}
// Require map keys to be identical
if !astmodel.TypeEquals(sourceMap.KeyType(), destinationMap.KeyType()) {
// Keys are different definitions
return nil, nil
}
// Require a conversion between the map items
unwrappedSourceEndpoint := sourceEndpoint.WithType(sourceMap.ValueType())
unwrappedDestinationEndpoint := destinationEndpoint.WithType(destinationMap.ValueType())
conversion, err := CreateTypeConversion(
unwrappedSourceEndpoint,
unwrappedDestinationEndpoint,
conversionContext)
if err != nil {
return nil, eris.Wrapf(
err,
"finding map conversion from %s to %s",
astmodel.DebugDescription(sourceEndpoint.Type()),
astmodel.DebugDescription(destinationEndpoint.Type()))
}
if conversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
var cacheOriginal dst.Stmt
var actualReader dst.Expr
// If the value we're reading is a local or a field, it's cheap to read and we can skip
// using a local (which makes the generated code easier to read). In other cases, we want
// to cache the value in a local to avoid repeating any expensive conversion.
switch reader.(type) {
case *dst.Ident, *dst.SelectorExpr:
// reading a local variable or a field
cacheOriginal = nil
actualReader = reader
default:
// Something else, so we cache the original
local := knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "", "Cache")
cacheOriginal = astbuilder.ShortDeclaration(local, reader)
actualReader = dst.NewIdent(local)
}
checkForNil := astbuilder.AreNotEqual(actualReader, astbuilder.Nil())
// We create three obviously related identifiers to use for the conversion.
// These are all within the scope of the true branch of our if statement
// The Map is created in the current knownLocals scope because we need it after the loop completes.
// The other two are created in a nested knownLocals scope because they're only needed within the loop; this
// ensures any other locals needed for the conversion don't leak out into our main scope.
// These suffixes must not overlap with those used for array conversion. (If these suffixes overlap, the naming
// becomes difficult to read when converting maps containing slices or vice versa.)
branchLocals := knownLocals.Clone()
tempID := branchLocals.CreateSingularLocal(sourceEndpoint.Name(), "Map")
loopLocals := branchLocals.Clone() // Clone after tempId is created so that it's visible within the loop
itemID := loopLocals.CreateSingularLocal(sourceEndpoint.Name(), "Value")
keyID := loopLocals.CreateSingularLocal(sourceEndpoint.Name(), "Key")
keyTypeExpr, err := destinationMap.KeyType().AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating map key type expression for %s",
astmodel.DebugDescription(destinationMap.KeyType()))
}
valueTypeExpr, err := destinationMap.ValueType().AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating map value type expression for %s",
astmodel.DebugDescription(destinationMap.ValueType()))
}
declaration := astbuilder.ShortDeclaration(
tempID,
astbuilder.MakeMapWithCapacity(
keyTypeExpr,
valueTypeExpr,
astbuilder.CallFunc("len", actualReader)))
assignToItem := func(expr dst.Expr) []dst.Stmt {
return astbuilder.Statements(
astbuilder.SimpleAssignment(
&dst.IndexExpr{
X: dst.NewIdent(tempID),
Index: dst.NewIdent(keyID),
},
expr),
)
}
avoidAliasing := astbuilder.ShortDeclaration(itemID, dst.NewIdent(itemID))
avoidAliasing.Decs.Start.Append("// Shadow the loop variable to avoid aliasing")
avoidAliasing.Decs.Before = dst.NewLine
elemConv, err := conversion(dst.NewIdent(itemID), assignToItem, loopLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating map item conversion from %s to %s",
astmodel.DebugDescription(sourceMap.ValueType()),
astmodel.DebugDescription(destinationMap.ValueType()))
}
loopBody := astbuilder.Statements(avoidAliasing, elemConv)
loop := astbuilder.IterateOverMapWithValue(keyID, itemID, actualReader, loopBody...)
assignMap := writer(dst.NewIdent(tempID))
trueBranch := astbuilder.Statements(declaration, loop, assignMap)
assignNil := writer(astbuilder.Nil())
return astbuilder.Statements(
cacheOriginal,
astbuilder.SimpleIfElse(checkForNil, trueBranch, assignNil)), nil
}, nil
}
// assignUserAssignedIdentityMapFromArray will generate a code fragment to populate a userAssignedIdentity array from
// a map whose key is the ARM ID of the userAssignedIdentity
//
// if source.UserAssignedIdentities != nil {
// <arr> := make([]<arrType>, 0, len(source.UserAssignedIdentities))
// for key, _ := range source.UserAssignedIdentities {
// ref := genruntime.CreateResourceReferenceFromARMID(key)
// <arr> = append(<arr>, UserAssignedIdentityDetails{Reference: ref})
// }
// <writer> = <arr>
// } else {
// <writer> = nil
// }
func assignUserAssignedIdentityMapFromArray(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// There's no conversion in the other direction (array -> map) for this because property_conversions only deals with:
// 1. Conversions between storage types, where UserAssignedIdentity's are arrays on both sides and don't need
// special handling.
// 2. Conversions from Status -> Spec, which is the direction that this conversion method deals with.
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require source to be a map type
sourceMapType, sourceIsMap := astmodel.AsMapType(sourceEndpoint.Type())
if !sourceIsMap {
return nil, nil
}
// Require destination to be an array type
destinationArray, destinationIsArray := astmodel.AsArrayType(destinationEndpoint.Type())
if !destinationIsArray {
return nil, nil
}
// Require the source endpoint to have the expected property name
if sourceEndpoint.Name() != astmodel.UserAssignedIdentitiesProperty {
return nil, nil
}
// Require the destination endpoint to have the expected property name
if destinationEndpoint.Name() != astmodel.UserAssignedIdentitiesProperty {
return nil, nil
}
// The destination should be a typeName
destinationElement := destinationArray.Element()
_, ok := astmodel.AsTypeName(destinationElement)
if !ok {
return nil, nil
}
// The source map should be map[string]TypeName
_, ok = astmodel.AsTypeName(sourceMapType.ValueType())
if sourceMapType.KeyType() != astmodel.StringType || !ok {
return nil, nil
}
conversion, err := CreateTypeConversion(
sourceEndpoint.WithType(astmodel.StringType),
destinationEndpoint.WithType(astmodel.ResourceReferenceType),
conversionContext)
if err != nil {
return nil, eris.Wrapf(
err,
"finding UserAssignedIdentities conversion from %s to %s",
astmodel.DebugDescription(astmodel.StringType),
astmodel.DebugDescription(astmodel.ResourceReferenceType))
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// <source>List := make([]<type>, 0, len(<source>)
tempID := knownLocals.CreateSingularLocal(sourceEndpoint.Name(), "List")
destinationArrayExpr, err := destinationArray.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating UserAssignedIdentities conversion from %s to %s",
astmodel.DebugDescription(astmodel.StringType),
astmodel.DebugDescription(astmodel.ResourceReferenceType))
}
declaration := astbuilder.ShortDeclaration(
tempID,
astbuilder.MakeEmptySlice(destinationArrayExpr, astbuilder.CallFunc("len", reader)))
loopLocals := knownLocals.Clone()
keyID := loopLocals.CreateLocal(sourceEndpoint.Name(), "Key")
intermediateDestination := loopLocals.CreateLocal(destinationEndpoint.Name(), "Ref")
destinationTypeExpr, err := destinationElement.AsTypeExpr(generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating UserAssignedIdentities conversion from %s to %s",
astmodel.DebugDescription(astmodel.StringType),
astmodel.DebugDescription(astmodel.ResourceReferenceType))
}
uaiBuilder := astbuilder.NewCompositeLiteralBuilder(destinationTypeExpr)
uaiBuilder.AddField("Reference", dst.NewIdent(intermediateDestination))
writeToElement := func(expr dst.Expr) []dst.Stmt {
return astbuilder.Statements(
astbuilder.ShortDeclaration(intermediateDestination, expr))
}
// for key, _ := range source.UserAssignedIdentities {
// ref := genruntime.CreateResourceReferenceFromARMID(key)
// <arr> = append(<arr>, UserAssignedIdentityDetails{Reference: ref})
// }
elemConv, err := conversion(dst.NewIdent(keyID), writeToElement, loopLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"creating UserAssignedIdentities conversion from %s to %s",
astmodel.DebugDescription(astmodel.StringType),
astmodel.DebugDescription(astmodel.ResourceReferenceType))
}
loopBody := astbuilder.Statements(
elemConv,
astbuilder.AppendItemToSlice(dst.NewIdent(tempID), uaiBuilder.Build()),
)
loop := astbuilder.IterateOverMapWithKey(
keyID,
reader,
loopBody...,
)
// if source.UserAssignedIdentities != nil
checkForNil := astbuilder.AreNotEqual(reader, astbuilder.Nil())
// <writer> = nil
assignNil := writer(astbuilder.Nil())
// <writer> = <arr>
assignValue := writer(dst.NewIdent(tempID))
// if source.UserAssignedIdentities != nil {
// <loop>
// } else {
// <writer> = nil
// }
trueBranch := astbuilder.Statements(
declaration,
loop,
assignValue)
return astbuilder.Statements(
astbuilder.SimpleIfElse(checkForNil, trueBranch, assignNil)), nil
}, nil
}
// assignEnumFromEnum will generate a conversion if both definitions have the same underlying
// primitive type and neither source nor destination is optional
//
// <local> = <baseType>(<source>)
// <destination> = <enum>(<local>)
//
// We don't technically need this one, but it generates nicer code because it bypasses an unnecessary cast.
func assignEnumFromEnum(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be an enumeration
_, sourceType, sourceFound := conversionContext.ResolveType(sourceEndpoint.Type())
if !sourceFound {
return nil, nil
}
sourceEnum, sourceIsEnum := astmodel.AsEnumType(sourceType)
if !sourceIsEnum {
return nil, nil
}
// Require destination to be an enumeration
destinationName, destinationType, destinationFound := conversionContext.ResolveType(destinationEndpoint.Type())
if !destinationFound {
return nil, nil
}
destinationEnum, destinationIsEnum := astmodel.AsEnumType(destinationType)
if !destinationIsEnum {
return nil, nil
}
// Require enumerations to have the same base definitions
if !astmodel.TypeEquals(sourceEnum.BaseType(), destinationEnum.BaseType()) {
return nil, eris.Errorf(
"no conversion from %s to %s",
astmodel.DebugDescription(sourceEnum.BaseType()),
astmodel.DebugDescription(destinationEnum.BaseType()))
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
local := knownLocals.CreateSingularLocal(destinationEndpoint.Name(), "", "As"+destinationName.Name(), "Value")
var declare dst.Stmt
if destinationEnum.NeedsMappingConversion(destinationName) {
// We need to use the values mapping to convert the value in a case-insensitive manner
mapperID := destinationEnum.MapperVariableName(destinationName)
genruntimePkg := generationContext.MustGetImportedPackageName(astmodel.GenRuntimeReference)
// genruntime.ToEnum(<actualReader>, <mapperId>)
toEnum := astbuilder.CallQualifiedFunc(
genruntimePkg,
"ToEnum",
astbuilder.CallFunc("string", reader),
dst.NewIdent(mapperID))
declare = astbuilder.ShortDeclaration(local, toEnum)
} else {
// No conversion required
declare = astbuilder.ShortDeclaration(local, astbuilder.CallFunc(destinationName.Name(), reader))
}
write := writer(dst.NewIdent(local))
return astbuilder.Statements(
declare,
write,
), nil
}, nil
}
// assignPrimitiveFromEnum will generate a conversion from an enumeration if the
// destination has the underlying base type of the enumeration and neither source nor destination
// is optional
//
// <local> = <baseType>(<source>)
// <destination> = <enum>(<local>)
func assignPrimitiveFromEnum(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be an enumeration
_, srcType, ok := conversionContext.ResolveType(sourceEndpoint.Type())
if !ok {
return nil, nil
}
srcEnum, srcIsEnum := astmodel.AsEnumType(srcType)
if !srcIsEnum {
return nil, nil
}
// Require destination to be a primitive type
dstPrimitive, ok := astmodel.AsPrimitiveType(destinationEndpoint.Type())
if !ok {
return nil, nil
}
// Require enumeration to have the destination as base type
if !astmodel.TypeEquals(srcEnum.BaseType(), dstPrimitive) {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
return writer(astbuilder.CallFunc(dstPrimitive.Name(), reader)), nil
}, nil
}
// assignObjectDirectlyFromObject will generate a conversion if both properties are TypeNames referencing ObjectType
// definitions, neither property is optional, and the types are adjacent in our storage conversion graph.
//
// var <local> <destinationType>
// err := <local>.AssignPropertiesFrom(<source>)
//
// if err != nil {
// return errors.Wrap(err, "while calling <local>.AssignPropertiesFrom(<source>)")
// }
//
// <destination> = <local>
func assignObjectDirectlyFromObject(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require expected direction
if conversionContext.direction != ConvertFrom {
return nil, nil
}
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be the name of an object
sourceName, sourceType, sourceFound := conversionContext.ResolveType(sourceEndpoint.Type())
if !sourceFound {
return nil, nil
}
if _, sourceIsObject := astmodel.AsObjectType(sourceType); !sourceIsObject {
return nil, nil
}
// Require destination to be the name of an object
destinationName, destinationType, destinationFound := conversionContext.ResolveType(destinationEndpoint.Type())
if !destinationFound {
return nil, nil
}
_, destinationIsObject := astmodel.AsObjectType(destinationType)
if !destinationIsObject {
return nil, nil
}
// If the source and destination types are in different packages, we must consult the conversion graph to make sure
// this is an expected conversion.
if !sourceName.PackageReference().Equals(destinationName.PackageReference()) {
// If our two types are not adjacent in our conversion graph, this is not the conversion you're looking for
nextType, err := conversionContext.FindNextType(destinationName)
if err != nil {
return nil, eris.Wrapf(
err,
"looking up next type for %s",
astmodel.DebugDescription(destinationEndpoint.Type()))
}
if !nextType.IsEmpty() && !astmodel.TypeEquals(nextType, sourceName) {
return nil, nil
}
// If the two definitions have different names, require an explicit rename from one to the other
//
// Challenge: If we can detect incorrect renaming configuration here, why do we need that configuration at all?
// Answer: Because we need to use that configuration other places (such as ConversionGraph) where we don't have
// the right information to infer correctly.
//
if sourceName.Name() != destinationName.Name() {
err := conversionContext.validateTypeRename(sourceName, destinationName)
if err != nil {
return nil, err
}
}
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
copyVar := knownLocals.CreateSingularLocal(destinationEndpoint.Name(), "", "Local", "Copy", "Temp")
// We have to do this at render time in order to ensure the first conversion generated
// declares 'err', not a later one
tok := token.ASSIGN
if knownLocals.TryCreateLocal("err") {
tok = token.DEFINE
}
localID := dst.NewIdent(copyVar)
errLocal := dst.NewIdent("err")
errorsPackageName := generationContext.MustGetImportedPackageName(astmodel.ErisReference)
declaration := astbuilder.LocalVariableDeclaration(copyVar, createTypeDeclaration(destinationName, generationContext), "")
functionName := NameOfPropertyAssignmentFunction(
conversionContext.FunctionBaseName(), sourceName, ConvertFrom, conversionContext.idFactory)
conversion := astbuilder.AssignmentStatement(
errLocal,
tok,
astbuilder.CallExpr(localID, functionName, astbuilder.AsReference(reader)))
checkForError := astbuilder.ReturnIfNotNil(
errLocal,
astbuilder.WrappedErrorf(
errorsPackageName,
"calling %s() to %s",
functionName,
describeAssignment(sourceEndpoint, destinationEndpoint)))
assignment := writer(localID)
return astbuilder.Statements(declaration, conversion, checkForError, assignment), nil
}, nil
}
// assignObjectDirectlyToObject will generate a conversion if both properties are TypeNames referencing ObjectType
// definitions, neither property is optional, and the types are adjacent in our storage conversion graph.
//
// var <local> <destinationType>
// err := <source>.AssignPropertiesTo(&<local>)
//
// if err != nil {
// return errors.Wrap(err, "while calling <local>.AssignPropertiesTo(<source>)")
// }
//
// <destination> = <local>
func assignObjectDirectlyToObject(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require expected direction
if conversionContext.direction != ConvertTo {
return nil, nil
}
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be the name of an object
sourceName, sourceType, sourceFound := conversionContext.ResolveType(sourceEndpoint.Type())
if !sourceFound {
return nil, nil
}
if _, sourceIsObject := astmodel.AsObjectType(sourceType); !sourceIsObject {
return nil, nil
}
// Require destination to be the name of an object
destinationName, destinationType, destinationFound := conversionContext.ResolveType(destinationEndpoint.Type())
if !destinationFound {
return nil, nil
}
_, destinationIsObject := astmodel.AsObjectType(destinationType)
if !destinationIsObject {
return nil, nil
}
// If the source and destination types are in different packages, we must consult the conversion graph to make sure
// this is an expected conversion.
if !sourceName.PackageReference().Equals(destinationName.PackageReference()) {
// If our two types are not adjacent in our conversion graph, this is not the conversion you're looking for
// Check that
nextType, err := conversionContext.FindNextType(sourceName)
if err != nil {
return nil, eris.Wrapf(
err,
"looking up next type for %s",
astmodel.DebugDescription(sourceEndpoint.Type()))
}
if !nextType.IsEmpty() && !astmodel.TypeEquals(nextType, destinationName) {
return nil, nil
}
// If the two definitions have different names, require an explicit rename from one to the other
//
// Challenge: If we can detect incorrect renaming configuration here, why do we need that configuration at all?
// Answer: Because we need to use that configuration other places (such as ConversionGraph) where we don't have
// the right information to infer correctly.
//
if sourceName.Name() != destinationName.Name() {
err := conversionContext.validateTypeRename(sourceName, destinationName)
if err != nil {
return nil, err
}
}
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
copyVar := knownLocals.CreateSingularLocal(destinationEndpoint.Name(), "", "Local", "Copy", "Temp")
// We have to do this at render time in order to ensure the first conversion generated
// declares 'err', not a later one
tok := token.ASSIGN
if knownLocals.TryCreateLocal("err") {
tok = token.DEFINE
}
localID := dst.NewIdent(copyVar)
errLocal := dst.NewIdent("err")
errorsPackageName := generationContext.MustGetImportedPackageName(astmodel.ErisReference)
declaration := astbuilder.LocalVariableDeclaration(copyVar, createTypeDeclaration(destinationName, generationContext), "")
functionName := NameOfPropertyAssignmentFunction(
conversionContext.FunctionBaseName(), destinationName, ConvertTo, conversionContext.idFactory)
conversion := astbuilder.AssignmentStatement(
errLocal,
tok,
astbuilder.CallExpr(reader, functionName, astbuilder.AddrOf(localID)))
checkForError := astbuilder.ReturnIfNotNil(
errLocal,
astbuilder.WrappedErrorf(
errorsPackageName,
"calling %s() to %s",
functionName,
describeAssignment(sourceEndpoint, destinationEndpoint)))
assignment := writer(dst.NewIdent(copyVar))
return astbuilder.Statements(declaration, conversion, checkForError, assignment), nil
}, nil
}
// assignInlineObjectsViaIntermediateObject will generate a conversion if both properties are TypeNames referencing ObjectType
// definitions, neither property is optional, the types are NOT adjacent in our storage conversion graph, and they are
// inline with each other. The conversion is implemented by assigning properties to an intermediate instance before
// assigning those to our actual destination instance.
//
// For ConvertFrom the generated code will be:
//
// var <local> <intermediateType>
// err := <local>.AssignPropertiesFrom(<source>)
//
// if err != nil {
// return errors.Wrap(err, "while calling <local>.AssignPropertiesFrom(<source>)")
// }
//
// var <otherlocal> <destinationType>
// err := <otherlocal>.AssignPropertiesFrom(<local>)
//
// if err != nil {
// return errors.Wrap(err, "while calling <otherlocal>.AssignPropertiesFrom(<local>)")
// }
//
// Note the actual steps are generated by nested conversions; this handler works by finding the two conversions needed
// given our intermediate type and chaining them together.
func assignInlineObjectsViaIntermediateObject(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be the name of an object
sourceName, sourceType, sourceFound := conversionContext.ResolveType(sourceEndpoint.Type())
if !sourceFound {
return nil, nil
}
if _, sourceIsObject := astmodel.AsObjectType(sourceType); !sourceIsObject {
return nil, nil
}
// Require destination to be the name of an object
destinationName, destinationType, destinationFound := conversionContext.ResolveType(destinationEndpoint.Type())
if !destinationFound {
return nil, nil
}
_, destinationIsObject := astmodel.AsObjectType(destinationType)
if !destinationIsObject {
return nil, nil
}
// Require a path from one name to the next, and work out an intermediate step to break down the conversion
var intermediateName astmodel.InternalTypeName
if conversionContext.PathExists(sourceName, destinationName) {
var err error
intermediateName, err = conversionContext.FindNextType(sourceName)
if err != nil {
return nil, eris.Wrapf(
err,
"looking up next type for %s",
astmodel.DebugDescription(destinationEndpoint.Type()))
}
} else if conversionContext.PathExists(destinationName, sourceName) {
var err error
intermediateName, err = conversionContext.FindNextType(destinationName)
if err != nil {
return nil, eris.Wrapf(
err,
"looking up next type for %s",
astmodel.DebugDescription(destinationEndpoint.Type()))
}
} else {
// No path between the two types, we can't handle the required conversion
return nil, nil
}
// If intermediateName is empty, we didn't find an intermediate to use, so this conversion step doesn't apply.
// If we found either our source or destination as the intermediate type, then the two types are directly
// convertible and (again), this conversion step doesn't apply.
if intermediateName.IsEmpty() ||
astmodel.TypeEquals(intermediateName, sourceName) ||
astmodel.TypeEquals(intermediateName, destinationName) {
return nil, nil
}
// Make sure we can reference our intermediate type when needed
conversionContext.AddPackageReference(intermediateName.PackageReference())
// Need a pair of conversions, using our intermediate type
intermediateEndpoint := NewTypedConversionEndpoint(
intermediateName,
intermediateName.Name()+"Stash")
firstConversion, err := CreateTypeConversion(sourceEndpoint, intermediateEndpoint, conversionContext)
if err != nil {
return nil, eris.Wrapf(
err,
"finding first intermediate conversion, from %s to %s",
astmodel.DebugDescription(sourceName),
astmodel.DebugDescription(intermediateName))
}
if firstConversion == nil {
return nil, nil
}
secondConversion, err := CreateTypeConversion(intermediateEndpoint, destinationEndpoint, conversionContext)
if err != nil {
return nil, eris.Wrapf(
err,
"finding second intermediate conversion, from %s to %s",
astmodel.DebugDescription(intermediateName),
astmodel.DebugDescription(destinationType))
}
if secondConversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// We capture the expression written by the first step pass it to the second step,
// allowing us to avoid extra local variable (this is a bit sneaky, as we rely on assignObjectDirectlyFromObject
// and assignObjectDirectlyToObject using a local variable themselves.)
var capture dst.Expr = nil
capturingWriter := func(expr dst.Expr) []dst.Stmt {
capture = expr
return []dst.Stmt{}
}
// Capture the first step
firstStep, err := firstConversion(reader, capturingWriter, knownLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"converting from %s to %s",
astmodel.DebugDescription(sourceName),
astmodel.DebugDescription(intermediateName))
}
secondStep, err := secondConversion(capture, writer, knownLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"converting from %s to %s",
astmodel.DebugDescription(intermediateName),
astmodel.DebugDescription(destinationName))
}
return astbuilder.Statements(
firstStep,
secondStep), nil
}, nil
}
// assignNonInlineObjectsViaPivotObject will generate a conversion if both properties are TypeNames referencing Object
// Type definitions, neither property is optional, the types are NOT adjacent in our storage conversion graph, and they
// are NOT inline with each other. The conversion is implemented by assigning properties to an intermediate pivot
// instance before assigning those to our actual destination instance.
//
// A key difference between this and assignInlineObjectsViaIntermediateObject is that this handles the case where
// the types are on different branches of the conversion graph, necessitating discovery of the closest shared type
// to use as a pivot. Using this kind of pivot requires reversing the direction of the second half of the conversion!
//
// For ConvertFrom the generated code will be:
//
// var <pivot> <pivotType>
// err := <pivot>.AssignPropertiesFrom(<source>)
//
// if err != nil {
// return errors.Wrap(err, "while calling <pivot>.AssignPropertiesFrom(<source>)")
// }
//
// var <otherlocal> <destinationType>
// err := <pviot>.AssignPropertiesTo(<otherLocakl>)
//
// if err != nil {
// return errors.Wrap(err, "while calling <otherlocal>.AssignPropertiesFrom(<local>)")
// }
//
// Note the actual steps are generated by nested conversions; this handler works by finding the two conversions needed
// given our intermediate type and chaining them together.
func assignNonInlineObjectsViaPivotObject(
sourceEndpoint *TypedConversionEndpoint,
destinationEndpoint *TypedConversionEndpoint,
conversionContext *PropertyConversionContext,
) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be the name of an object
sourceName, sourceType, sourceFound := conversionContext.ResolveType(sourceEndpoint.Type())
if !sourceFound {
return nil, nil
}
if _, sourceIsObject := astmodel.AsObjectType(sourceType); !sourceIsObject {
return nil, nil
}
// Require destination to be the name of an object
destinationName, destinationType, destinationFound := conversionContext.ResolveType(destinationEndpoint.Type())
if !destinationFound {
return nil, nil
}
_, destinationIsObject := astmodel.AsObjectType(destinationType)
if !destinationIsObject {
return nil, nil
}
// Require that there's no direct conversion path between our source and destination types
if conversionContext.PathExists(sourceName, destinationName) ||
conversionContext.PathExists(destinationName, sourceName) {
return nil, nil
}
// Find the pivot type; if we can't find one, this conversion doesn't apply
pivotName, found := conversionContext.FindPivotType(sourceName, destinationName)
if !found {
// No pivot found, do nothing
return nil, nil
}
// Make sure we can reference our intermediate type when needed
conversionContext.AddPackageReference(pivotName.PackageReference())
// Need a pair of conversions, using our intermediate type.
// First convert forward to the pivot.
// We can't call any methods on the pivot type (as it's later in the conversion graph), so we
// have to use ConvertTo to write to it
pivotEndpoint := NewTypedConversionEndpoint(
pivotName,
pivotName.Name()+"Pivot")
firstConversion, err := CreateTypeConversion(
sourceEndpoint,
pivotEndpoint,
conversionContext.WithDirection(ConvertTo))
if err != nil {
return nil, eris.Wrapf(
err,
"finding first intermediate conversion, from %s to %s",
astmodel.DebugDescription(sourceName),
astmodel.DebugDescription(pivotName))
}
if firstConversion == nil {
return nil, nil
}
// Second conversion needs to run in the opposite direction, using ConvertFrom to read from the pivot
secondConversion, err := CreateTypeConversion(
pivotEndpoint,
destinationEndpoint,
conversionContext.WithDirection(ConvertFrom))
if err != nil {
return nil, eris.Wrapf(
err,
"finding second intermediate conversion, from %s to %s",
astmodel.DebugDescription(pivotName),
astmodel.DebugDescription(destinationType))
}
if secondConversion == nil {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// We capture the expression written by the first step pass it to the second step,
// allowing us to avoid extra local variable (this is a bit sneaky, as we rely on assignObjectDirectlyFromObject
// and assignObjectDirectlyToObject using a local variable themselves.)
var capture dst.Expr = nil
capturingWriter := func(expr dst.Expr) []dst.Stmt {
capture = expr
return []dst.Stmt{}
}
// Capture the first step
firstStep, err := firstConversion(reader, capturingWriter, knownLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"converting from %s to %s",
astmodel.DebugDescription(sourceName),
astmodel.DebugDescription(pivotName))
}
secondStep, err := secondConversion(capture, writer, knownLocals, generationContext)
if err != nil {
return nil, eris.Wrapf(
err,
"converting from %s to %s",
astmodel.DebugDescription(pivotName),
astmodel.DebugDescription(destinationName))
}
return astbuilder.Statements(
firstStep,
secondStep), nil
}, nil
}
// assignKnownType will generate an assignment if both definitions have the specified TypeName
//
// <destination> = <source>
//
//nolint:deadcode,unused
func assignKnownType(name astmodel.TypeName) func(*TypedConversionEndpoint, *TypedConversionEndpoint, *PropertyConversionContext) (PropertyConversion, error) {
return func(sourceEndpoint *TypedConversionEndpoint, destinationEndpoint *TypedConversionEndpoint, _ *PropertyConversionContext) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be a named type
sourceName, sourceIsName := astmodel.AsTypeName(sourceEndpoint.Type())
if !sourceIsName {
return nil, nil
}
// Require destination to be a named type
destinationName, destinationIsName := astmodel.AsTypeName(destinationEndpoint.Type())
if !destinationIsName {
return nil, nil
}
// Require source to be our specific type
if !astmodel.TypeEquals(sourceName, name) {
return nil, nil
}
// Require destination to be our specific type
if !astmodel.TypeEquals(destinationName, name) {
return nil, nil
}
return directAssignmentPropertyConversion, nil
}
}
type knownTypeMethodReturn int
const (
returnsReference = 0
returnsValue = 1
)
// copyKnownType will generate an assignment with the results of a call on the specified TypeName
//
// <destination> = <source>.<methodName>()
func copyKnownType(name astmodel.TypeName, methodName string, returnKind knownTypeMethodReturn) func(*TypedConversionEndpoint, *TypedConversionEndpoint, *PropertyConversionContext) (PropertyConversion, error) {
return func(sourceEndpoint *TypedConversionEndpoint, destinationEndpoint *TypedConversionEndpoint, _ *PropertyConversionContext) (PropertyConversion, error) {
// Require both source and destination to not be bag items
if sourceEndpoint.IsBagItem() || destinationEndpoint.IsBagItem() {
return nil, nil
}
// Require both source and destination to be non-optional
if sourceEndpoint.IsOptional() || destinationEndpoint.IsOptional() {
return nil, nil
}
// Require source to be a named type
sourceName, sourceIsName := astmodel.AsTypeName(sourceEndpoint.Type())
if !sourceIsName {
return nil, nil
}
// Require destination to be a named type
destinationName, destinationIsName := astmodel.AsTypeName(destinationEndpoint.Type())
if !destinationIsName {
return nil, nil
}
// Require source to be our specific type
if !astmodel.TypeEquals(sourceName, name) {
return nil, nil
}
// Require destination to be our specific type
if !astmodel.TypeEquals(destinationName, name) {
return nil, nil
}
return func(
reader dst.Expr,
writer func(dst.Expr) []dst.Stmt,
knownLocals *astmodel.KnownLocalsSet,
generationContext *astmodel.CodeGenerationContext,
) ([]dst.Stmt, error) {
// If our writer is dereferencing a value, skip that as we don't need to dereference before a method call
if star, ok := reader.(*dst.StarExpr); ok {
reader = star.X
}
if returnKind == returnsReference {
// If the copy method returns a ptr, we need to dereference
// This dereference is always safe because we ensured that both source and destination are always
// non-optional. The handler assignToOptional() should do the right thing when this happens.
return writer(astbuilder.Dereference(astbuilder.CallExpr(reader, methodName))), nil
}
return writer(astbuilder.CallExpr(reader, methodName)), nil
}, nil
}
}
func createTypeDeclaration(
name astmodel.InternalTypeName,
generationContext *astmodel.CodeGenerationContext,
) dst.Expr {
if name.InternalPackageReference().Equals(generationContext.CurrentPackage()) {
return dst.NewIdent(name.Name())
}
packageName := generationContext.MustGetImportedPackageName(name.InternalPackageReference())
return astbuilder.Selector(dst.NewIdent(packageName), name.Name())
}
func describeAssignment(sourceEndpoint *TypedConversionEndpoint, destinationEndpoint *TypedConversionEndpoint) string {
if sourceEndpoint.Name() != destinationEndpoint.Name() {
return fmt.Sprintf("populate field %s from %s", destinationEndpoint.Name(), sourceEndpoint.Name())
}
return fmt.Sprintf("populate field %s", destinationEndpoint.Name())
}