v2/tools/generator/internal/astmodel/conversion_function_builder.go (821 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package astmodel
import (
"fmt"
"go/token"
"strings"
"github.com/dave/dst"
"github.com/rotisserie/eris"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astbuilder"
)
// ConversionParameters are parameters for converting between a source type and a destination type.
type ConversionParameters struct {
Source dst.Expr
Destination dst.Expr
SourceType Type
DestinationType Type
NameHint string
ConversionContext []Type
AssignmentHandler func(destination, source dst.Expr) dst.Stmt
Locals *KnownLocalsSet
// SourceProperty is the source property for this conversion. Note that for recursive conversions this still refers
// to the source property that the conversion chain will ultimately be sourced from
SourceProperty *PropertyDefinition
// DestinationProperty is the destination property for this conversion. Note that for recursive conversions this still refers
// to the destination property that the conversion chain will ultimately be assigned to
DestinationProperty *PropertyDefinition
}
// GetSource gets the Source field.
func (params ConversionParameters) GetSource() dst.Expr {
return dst.Clone(params.Source).(dst.Expr)
}
// GetDestination gets the Destination field.
func (params ConversionParameters) GetDestination() dst.Expr {
return dst.Clone(params.Destination).(dst.Expr)
}
// WithSource returns a new ConversionParameters with the updated Source.
func (params ConversionParameters) WithSource(source dst.Expr) ConversionParameters {
result := params.copy()
result.Source = source
return result
}
// WithSourceType returns a new ConversionParameters with the updated SourceType.
func (params ConversionParameters) WithSourceType(t Type) ConversionParameters {
result := params.copy()
result.SourceType = t
return result
}
// WithDestination returns a new ConversionParameters with the updated Destination.
func (params ConversionParameters) WithDestination(destination dst.Expr) ConversionParameters {
result := params.copy()
result.Destination = destination
return result
}
// WithDestinationType returns a new ConversionParameters with the updated DestinationType.
func (params ConversionParameters) WithDestinationType(t Type) ConversionParameters {
result := params.copy()
result.DestinationType = t
return result
}
// WithAssignmentHandler returns a new ConversionParameters with the updated AssignmentHandler.
func (params ConversionParameters) WithAssignmentHandler(
assignmentHandler func(result dst.Expr, destination dst.Expr) dst.Stmt,
) ConversionParameters {
result := params.copy()
result.AssignmentHandler = assignmentHandler
return result
}
// AssignmentHandlerOrDefault returns the AssignmentHandler or a default assignment handler if AssignmentHandler was nil.
func (params ConversionParameters) AssignmentHandlerOrDefault() func(destination, source dst.Expr) dst.Stmt {
if params.AssignmentHandler == nil {
return AssignmentHandlerAssign
}
return params.AssignmentHandler
}
// CountArraysAndMapsInConversionContext returns the number of arrays/maps which are in the conversion context.
// This is to aid in situations where there are deeply nested conversions (i.e. array of map of maps). In these contexts,
// temporary variables need to be declared to store intermediate conversion results.
func (params ConversionParameters) CountArraysAndMapsInConversionContext() int {
result := 0
for _, t := range params.ConversionContext {
switch t.(type) {
case *MapType:
result += 1
case *ArrayType:
result += 1
}
}
return result
}
func (params ConversionParameters) copy() ConversionParameters {
result := params
result.ConversionContext = append([]Type(nil), params.ConversionContext...)
result.Locals = result.Locals.Clone()
return result
}
type ConversionHandler func(builder *ConversionFunctionBuilder, params ConversionParameters) ([]dst.Stmt, error)
// TODO: There feels like overlap between this and the Storage Conversion Factories? Need further thinking to combine them?
// ConversionFunctionBuilder is used to build a function converting between two similar types.
// It has a set of built-in conversions and can be configured with additional conversions.
type ConversionFunctionBuilder struct {
// TODO: Better way to let you fuss with this? How can you pick out what I've already put in here to overwrite it?
conversions []ConversionHandler
IDFactory IdentifierFactory
CodeGenerationContext *CodeGenerationContext
// SupportExplicitEmptyCollectionsSerializationMode signals that collections can be initialized to a size 0 collection, rather than an empty collection
SupportExplicitEmptyCollectionsSerializationMode bool
}
// NewConversionFunctionBuilder creates a new ConversionFunctionBuilder with the default conversions already added.
func NewConversionFunctionBuilder(
idFactory IdentifierFactory,
codeGenerationContext *CodeGenerationContext,
) *ConversionFunctionBuilder {
return &ConversionFunctionBuilder{
IDFactory: idFactory,
CodeGenerationContext: codeGenerationContext,
conversions: []ConversionHandler{
// Complex wrapper types checked first
IdentityConvertComplexOptionalProperty,
IdentityConvertComplexArrayProperty,
IdentityConvertComplexMapProperty,
// TODO: a flip function of some kind would be kinda nice (for source vs dest)
IdentityAssignValidatedTypeDestination,
IdentityAssignValidatedTypeSource,
IdentityAssignPrimitiveType,
AssignToOptional,
AssignFromOptional,
AssignToAliasOfPrimitive,
AssignFromAliasOfPrimitive,
AssignToAliasOfCollection,
AssignFromAliasOfCollection,
AssignToEnum,
AssignFromEnum,
IdentityDeepCopyJSON,
IdentityAssignTypeName,
},
}
}
func (builder *ConversionFunctionBuilder) WithSupportExplicitEmptyCollectionsSerializationMode() *ConversionFunctionBuilder {
builder.SupportExplicitEmptyCollectionsSerializationMode = true
return builder
}
func (builder *ConversionFunctionBuilder) ShouldInitializeCollectionToEmpty(prop *PropertyDefinition) bool {
return prop.HasTagValue(SerializationType, SerializationTypeExplicitEmptyCollection)
}
// AddConversionHandlers adds the specified conversion handlers to the end of the conversion list.
func (builder *ConversionFunctionBuilder) AddConversionHandlers(conversionHandlers ...ConversionHandler) {
builder.conversions = append(builder.conversions, conversionHandlers...)
}
// PrependConversionHandlers adds the specified conversion handlers to the beginning of the conversion list.
func (builder *ConversionFunctionBuilder) PrependConversionHandlers(conversionHandlers ...ConversionHandler) {
builder.conversions = append(conversionHandlers, builder.conversions...)
}
// BuildConversion creates a conversion between the source and destination defined by params.
func (builder *ConversionFunctionBuilder) BuildConversion(params ConversionParameters) ([]dst.Stmt, error) {
for _, conversion := range builder.conversions {
result, err := conversion(builder, params)
if err != nil {
return nil, err
}
if len(result) > 0 {
return result, nil
}
}
msg := fmt.Sprintf(
"don't know how to perform conversion for %s -> %s",
DebugDescription(params.SourceType),
DebugDescription(params.DestinationType))
panic(msg)
}
// IdentityConvertComplexOptionalProperty handles conversion for optional properties with complex elements
// This function generates code that looks like this:
//
// if <source> != nil {
// <code for producing result from destinationType.Element()>
// <destination> = &<result>
// }
func IdentityConvertComplexOptionalProperty(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
destinationType, ok := params.DestinationType.(*OptionalType)
if !ok {
return nil, nil
}
sourceType, ok := params.SourceType.(*OptionalType)
if !ok {
return nil, nil
}
locals := params.Locals.Clone()
tempVarIdent := locals.CreateLocal(params.NameHint)
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: astbuilder.Dereference(params.GetSource()),
SourceType: sourceType.Element(),
Destination: dst.NewIdent(tempVarIdent),
DestinationType: destinationType.Element(),
NameHint: params.NameHint,
ConversionContext: append(params.ConversionContext, destinationType),
AssignmentHandler: AssignmentHandlerDefine,
Locals: locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build conversion for optional element")
}
// Tack on the final assignment
conversion = append(
conversion,
astbuilder.SimpleAssignment(
params.GetDestination(),
astbuilder.AddrOf(dst.NewIdent(tempVarIdent))))
result := astbuilder.IfNotNil(
params.GetSource(),
conversion...)
return astbuilder.Statements(result), nil
}
// IdentityConvertComplexArrayProperty handles conversion for array properties with complex elements
// This function generates code that looks like this:
//
// for _, item := range <source> {
// <code for producing result from destinationType.Element()>
// <destination> = append(<destination>, <result>)
// }
func IdentityConvertComplexArrayProperty(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
destinationType, ok := params.DestinationType.(*ArrayType)
if !ok {
return nil, nil
}
sourceType, ok := params.SourceType.(*ArrayType)
if !ok {
return nil, nil
}
var results []dst.Stmt
locals := params.Locals.Clone() // Loop variables are scoped inside the loop
itemIdent := builder.CreateLocal(locals, "item", params.NameHint)
depth := params.CountArraysAndMapsInConversionContext()
destination := params.GetDestination()
// Check what depth we're at to determine if we need to define an intermediate variable to hold the result
// or if we'll be able to use the final destination directly.
if depth > 0 {
// TODO: The suffix here should maybe be configurable on the function builder?
innerDestinationIdent := locals.CreateLocal(params.NameHint, "Temp")
destination = dst.NewIdent(innerDestinationIdent)
destinationTypeExpr, err := destinationType.AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating destination type expression")
}
results = append(
results,
astbuilder.LocalVariableDeclaration(
innerDestinationIdent,
destinationTypeExpr,
""))
}
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: dst.NewIdent(itemIdent),
SourceType: sourceType.Element(),
Destination: dst.Clone(destination).(dst.Expr),
DestinationType: destinationType.Element(),
NameHint: itemIdent,
ConversionContext: append(params.ConversionContext, destinationType),
AssignmentHandler: astbuilder.AppendItemToSlice,
Locals: locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build conversion for array element")
}
result := &dst.RangeStmt{
Key: dst.NewIdent("_"),
Value: dst.NewIdent(itemIdent),
X: params.GetSource(),
Tok: token.DEFINE,
Body: astbuilder.StatementBlock(conversion...),
}
results = append(results, result)
// If we have an assignment handler, we need to make sure to call it. This only happens in the case of nested
// maps/arrays, where we need to make sure we generate the map assignment/array append before returning (otherwise
// the "actual" assignment will just end up being to an empty array/map).
if params.AssignmentHandler != nil {
results = append(results, params.AssignmentHandler(params.GetDestination(), dst.Clone(destination).(dst.Expr)))
}
// If we must forcibly construct empty collections, check if the destination is nil and if so, construct an empty collection
// This only applies for top-level collections (we don't forcibly construct nested collections)
if depth == 0 && builder.ShouldInitializeCollectionToEmpty(params.SourceProperty) {
destinationTypeExpr, err := destinationType.Element().AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating destination type expression")
}
emptySlice := astbuilder.SliceLiteral(destinationTypeExpr)
assignEmpty := astbuilder.SimpleAssignment(params.GetDestination(), emptySlice)
astbuilder.AddComment(
&assignEmpty.Decs.Start,
"// Set property to empty map, as this resource is set to serialize all collections explicitly")
assignEmpty.Decs.Before = dst.NewLine
ifNil := astbuilder.IfNil(
params.GetDestination(),
assignEmpty)
results = append(results, ifNil)
}
return results, nil
}
// IdentityConvertComplexMapProperty handles conversion for map properties with complex values.
// This function panics if the map keys are not primitive types.
// This function generates code that looks like this:
//
// if <source> != nil {
// <destination> = make(map[<destinationType.KeyType()]<destinationType.ValueType()>, len(<source>))
// for key, value := range <source> {
// <code for producing result from destinationType.ValueType()>
// <destination>[key] = <result>
// }
// }
func IdentityConvertComplexMapProperty(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
destinationType, ok := params.DestinationType.(*MapType)
if !ok {
return nil, nil
}
sourceType, ok := params.SourceType.(*MapType)
if !ok {
return nil, nil
}
if _, ok := destinationType.KeyType().(*PrimitiveType); !ok {
msg := fmt.Sprintf(
"map had non-primitive key type: %s",
DebugDescription(destinationType.KeyType()))
panic(msg)
}
depth := params.CountArraysAndMapsInConversionContext()
locals := params.Locals.Clone() // Loop variables are scoped inside the loop
keyIdent := builder.CreateLocal(locals, "key", params.NameHint)
valueIdent := builder.CreateLocal(locals, "value", params.NameHint)
nameHint := valueIdent
destination := params.GetDestination()
makeMapToken := token.ASSIGN
// Check what depth we're at to determine if we need to define an intermediate variable to hold the result
// or if we'll be able to use the final destination directly.
if depth > 0 {
innerDestinationIdent := locals.CreateLocal(params.NameHint, "Temp")
destination = dst.NewIdent(innerDestinationIdent)
makeMapToken = token.DEFINE
}
handler := func(lhs dst.Expr, rhs dst.Expr) dst.Stmt {
return astbuilder.InsertMap(lhs, dst.NewIdent(keyIdent), rhs)
}
keyTypeExpr, err := destinationType.KeyType().AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating key type expression")
}
valueTypeExpr, err := destinationType.ValueType().AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating value type expression")
}
makeMapStatement := astbuilder.AssignmentStatement(
destination,
makeMapToken,
astbuilder.MakeMapWithCapacity(keyTypeExpr, valueTypeExpr,
astbuilder.CallFunc("len", params.GetSource())))
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: dst.NewIdent(valueIdent),
SourceType: sourceType.ValueType(),
Destination: dst.Clone(destination).(dst.Expr),
DestinationType: destinationType.ValueType(),
NameHint: nameHint,
ConversionContext: append(params.ConversionContext, destinationType),
AssignmentHandler: handler,
Locals: locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build conversion for map value")
}
rangeStatement := &dst.RangeStmt{
Key: dst.NewIdent(keyIdent),
Value: dst.NewIdent(valueIdent),
X: params.GetSource(),
Tok: token.DEFINE,
Body: astbuilder.StatementBlock(conversion...),
}
// If we must forcibly construct empty collections, check if the destination is nil and if so, construct an empty collection
// This only applies for top-level collections (we don't forcibly construct nested collections)
var result *dst.IfStmt
if depth == 0 && builder.ShouldInitializeCollectionToEmpty(params.SourceProperty) {
emptyMap := astbuilder.MakeMap(keyTypeExpr, valueTypeExpr)
assignEmpty := astbuilder.SimpleAssignment(params.GetDestination(), emptyMap)
astbuilder.AddComment(
&assignEmpty.Decs.Start,
"// Set property to empty map, as this resource is set to serialize all collections explicitly")
assignEmpty.Decs.Before = dst.NewLine
result = astbuilder.SimpleIfElse(
astbuilder.NotNil(params.GetSource()),
astbuilder.Statements(
makeMapStatement,
rangeStatement,
),
astbuilder.Statements(
assignEmpty,
),
)
} else {
result = astbuilder.IfNotNil(
params.GetSource(),
makeMapStatement,
rangeStatement)
}
// If we have an assignment handler, we need to make sure to call it. This only happens in the case of nested
// maps/arrays, where we need to make sure we generate the map assignment/array append before returning (otherwise
// the "actual" assignment will just end up being to an empty array/map).
if params.AssignmentHandler != nil {
result.Body.List = append(result.Body.List, params.AssignmentHandler(params.GetDestination(), dst.Clone(destination).(dst.Expr)))
}
return astbuilder.Statements(result), nil
}
// IdentityAssignTypeName handles conversion for TypeName's that are the same
// Note that because this handler is dealing with TypeName's and not Optional<TypeName>, it is safe to
// perform a simple assignment rather than a copy.
// This function generates code that looks like this:
//
// <destination> <assignmentHandler> <source>
func IdentityAssignTypeName(
_ *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
destinationType, ok := params.DestinationType.(TypeName)
if !ok {
return nil, nil
}
sourceType, ok := params.SourceType.(TypeName)
if !ok {
return nil, nil
}
// Can only apply basic assignment for typeNames that are the same
if !TypeEquals(sourceType, destinationType) {
return nil, nil
}
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
params.GetSource())), nil
}
// IdentityAssignPrimitiveType just assigns source to destination directly, no conversion needed.
// This function generates code that looks like this:
// <destination> <assignmentHandler> <source>
func IdentityAssignPrimitiveType(
_ *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
if _, ok := params.DestinationType.(*PrimitiveType); !ok {
return nil, nil
}
if _, ok := params.SourceType.(*PrimitiveType); !ok {
return nil, nil
}
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
params.GetSource())), nil
}
// AssignToOptional assigns address of source to destination.
// This function generates code that looks like this, for simple conversions:
// <destination> <assignmentHandler> &<source>
//
// or:
// <destination>Temp := convert(<source>)
// <destination> <assignmentHandler> &<destination>Temp
func AssignToOptional(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
optDest, ok := params.DestinationType.(*OptionalType)
if !ok {
return nil, nil
}
if TypeEquals(optDest.Element(), params.SourceType) {
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
astbuilder.AddrOf(params.GetSource()))), nil
}
// a more complex conversion is needed
dstType := optDest.Element()
tmpLocal := builder.CreateLocal(params.Locals, "temp", params.NameHint)
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: params.Source,
SourceType: params.SourceType,
Destination: dst.NewIdent(tmpLocal),
DestinationType: dstType,
NameHint: tmpLocal,
ConversionContext: nil,
AssignmentHandler: nil,
Locals: params.Locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build inner conversion to optional")
}
if len(conversion) == 0 {
return nil, nil // unable to build inner conversion
}
destinationTypeExpr, err := dstType.AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating destination type expression")
}
return astbuilder.Statements(
astbuilder.LocalVariableDeclaration(tmpLocal, destinationTypeExpr, ""),
conversion,
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
astbuilder.AddrOf(dst.NewIdent(tmpLocal)))), nil
}
// AssignFromOptional assigns address of source to destination.
// This function generates code that looks like this, for simple conversions:
//
// if (<source> != nil) {
// <destination> <assignmentHandler> *<source>
// }
//
// or:
//
// if (<source> != nil) {
// <destination>Temp := convert(*<source>)
// <destination> <assignmentHandler> <destination>Temp
// }
func AssignFromOptional(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
optSrc, ok := params.SourceType.(*OptionalType)
if !ok {
return nil, nil
}
if TypeEquals(optSrc.Element(), params.DestinationType) {
return astbuilder.Statements(
astbuilder.IfNotNil(params.GetSource(),
params.AssignmentHandlerOrDefault()(params.GetDestination(), astbuilder.Dereference(params.GetSource())),
)), nil
}
// a more complex conversion is needed
srcType := optSrc.Element()
locals := params.Locals.Clone()
tmpLocal := builder.CreateLocal(locals, "temp", params.NameHint)
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: astbuilder.Dereference(params.GetSource()),
SourceType: srcType,
Destination: dst.NewIdent(tmpLocal),
DestinationType: params.DestinationType,
NameHint: tmpLocal,
ConversionContext: nil,
AssignmentHandler: nil,
Locals: locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build inner conversion from optional")
}
if len(conversion) == 0 {
return nil, nil // unable to build inner conversion
}
var result []dst.Stmt
destinationTypeExpr, err := params.DestinationType.AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating destination type expression")
}
result = append(result, astbuilder.LocalVariableDeclaration(tmpLocal, destinationTypeExpr, ""))
result = append(result, conversion...)
result = append(result, params.AssignmentHandlerOrDefault()(params.GetDestination(), dst.NewIdent(tmpLocal)))
return astbuilder.Statements(
astbuilder.IfNotNil(
params.GetSource(),
result...)), nil
}
// AssignToAliasOfPrimitive assigns a primitive value to an alias of that same type.
// This function generates code that looks like this:
//
// <destination> <assignmentHandler> <destinationType>(<source>)
func AssignToAliasOfPrimitive(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
// Source must be a primitive type
srcPrim, ok := params.SourceType.(*PrimitiveType)
if !ok {
return nil, nil
}
// Destination must be an internal type name
dstName, ok := params.DestinationType.(InternalTypeName)
if !ok {
return nil, nil
}
// ... who's definition we know ...
dstDef, err := builder.CodeGenerationContext.GetDefinition(dstName)
if err != nil {
//nolint:nilerr // err is not nil, we defer to a different conversion
return nil, nil
}
// ... and it's not optional (hedging against oddities) ...
if _, ok = AsOptionalType(dstDef.Type()); ok {
return nil, nil
}
// ... and it is a primitive type ...
dstPrim, ok := AsPrimitiveType(dstDef.Type())
if !ok {
return nil, nil
}
// ... and is an alias of the source type ...
if !TypeEquals(srcPrim, dstPrim) {
return nil, nil
}
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
astbuilder.CallFunc(
dstName.Name(),
params.GetSource()))),
nil
}
// AssignFromAliasOfPrimitive assigns a primitive value from an alias of that same type.
// This function generates code that looks like this:
//
// <destination> <assignmentHandler> <destinationType>(<source>)
func AssignFromAliasOfPrimitive(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
// Source must be an internal type name
srcName, ok := params.SourceType.(InternalTypeName)
if !ok {
return nil, nil
}
// ... who's definition we know ...
srcDef, err := builder.CodeGenerationContext.GetDefinition(srcName)
if err != nil {
//nolint:nilerr // err is not nil, we defer to a different conversion
return nil, nil
}
// ... and it's not optional (hedging against oddities) ...
if _, ok = AsOptionalType(srcDef.Type()); ok {
return nil, nil
}
// ... and it is a primitive type ...
srcPrim, ok := AsPrimitiveType(srcDef.Type())
if !ok {
return nil, nil
}
// Destination must be a primitive type
dstPrim, ok := params.DestinationType.(*PrimitiveType)
if !ok {
return nil, nil
}
// ... and is an alias of the source type ...
if !TypeEquals(srcPrim, dstPrim) {
return nil, nil
}
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
astbuilder.CallFunc(
dstPrim.String(),
params.GetSource()))),
nil
}
// AssignToAliasOfCollection assigns an array of values to an alias of that same type.
// This function generates code that looks like this:
//
// <destination> <assignmentHandler> <destinationType>(<source>)
func AssignToAliasOfCollection(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
// Source must be an array type or a map type
_, srcIsArray := AsArrayType(params.SourceType)
_, srcIsMap := AsMapType(params.SourceType)
if !srcIsArray && !srcIsMap {
return nil, nil
}
// Destination must be an internal type name
dstName, ok := params.DestinationType.(InternalTypeName)
if !ok {
return nil, nil
}
// ... who's definition we know ...
dstDef, err := builder.CodeGenerationContext.GetDefinition(dstName)
if err != nil {
//nolint:nilerr // err is not nil, we defer to a different conversion
return nil, nil
}
// ... and it's not optional (hedging against oddities) ...
if _, ok = AsOptionalType(dstDef.Type()); ok {
return nil, nil
}
// ... and it must also be an array type or a map type (and the same kind as source)
dstArray, dstIsArray := AsArrayType(dstDef.Type())
dstMap, dstIsMap := AsMapType(dstDef.Type())
if dstIsArray != srcIsArray || dstIsMap != srcIsMap {
return nil, nil
}
var dstType Type
if dstIsArray {
dstType = dstArray
} else if dstIsMap {
dstType = dstMap
}
// ... and we can convert between those array types ...
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: params.Source,
SourceType: params.SourceType,
Destination: params.Destination,
DestinationType: dstDef.Type(),
NameHint: params.NameHint,
ConversionContext: append(params.ConversionContext, dstType),
AssignmentHandler: func(lhs dst.Expr, rhs dst.Expr) dst.Stmt {
// Use the existing assignment handler, but make sure we cast the rhs to the right type
return params.AssignmentHandlerOrDefault()(
lhs,
astbuilder.CallFunc(
dstName.Name(),
rhs))
},
Locals: params.Locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build conversion for array element")
}
return astbuilder.Statements(
conversion,
),
nil
}
// AssignFromAliasOfCollection assigns an array of values from an alias of that same type.
// This function generates code that looks like this:
//
// <destination> <assignmentHandler> <destinationType>(<source>)
func AssignFromAliasOfCollection(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
// Source must be an internal type name
srcName, ok := params.SourceType.(InternalTypeName)
if !ok {
return nil, nil
}
// ... who's definition we know ...
srcDef, err := builder.CodeGenerationContext.GetDefinition(srcName)
if err != nil {
//nolint:nilerr // err is not nil, we defer to a different conversion
return nil, nil
}
// ... and it's not optional (hedging against oddities) ...
if _, ok = AsOptionalType(srcDef.Type()); ok {
return nil, nil
}
// ... and it is an array type or a map type ...
_, srcIsArray := AsArrayType(srcDef.Type())
_, srcIsMap := AsMapType(srcDef.Type())
if !srcIsArray && !srcIsMap {
return nil, nil
}
// Destination must also be an array type or a map type (and the same kind as source)
_, dstIsArray := AsArrayType(params.DestinationType)
_, dstIsMap := AsMapType(params.DestinationType)
if dstIsArray != srcIsArray || dstIsMap != srcIsMap {
return nil, nil
}
// ... and we can convert between those collection types ...
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: params.Source,
SourceType: srcDef.Type(),
Destination: params.Destination,
DestinationType: params.DestinationType,
NameHint: params.NameHint,
ConversionContext: append(params.ConversionContext, params.DestinationType),
AssignmentHandler: func(lhs dst.Expr, rhs dst.Expr) dst.Stmt {
// Use the existing assignment handler if there is one, but make sure we always assign
return params.AssignmentHandlerOrDefault()(
lhs,
rhs)
},
Locals: params.Locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrap(err, "unable to build conversion for array element")
}
return astbuilder.Statements(
conversion,
),
nil
}
// AssignToEnum stores a value into an enum type.
// This function generates code that looks like this:
// <destination> = <enumType>(<source>)
func AssignToEnum(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
// Is the destination a typename of an enum?
itn, ok := params.DestinationType.(InternalTypeName)
if !ok {
// Not a typename
return nil, nil
}
def, err := builder.CodeGenerationContext.GetDefinition(itn)
if err != nil {
// Couldn't the definition, this handler isn't the one we want
// (not actually an error)
return nil, nil //nolint:nilerr // err is not nil, we defer to a different conversion
}
et, ok := AsEnumType(def.Type())
if !ok {
// Definition isn't for an enum type,
return nil, nil
}
if TypeEquals(et.BaseType(), params.SourceType) {
// We can directly cast the source to the destination
var cast dst.Expr
if builder.CodeGenerationContext.CurrentPackage() == itn.PackageReference() {
cast = astbuilder.CallFunc(itn.Name(), params.GetSource())
} else {
alias := builder.CodeGenerationContext.MustGetImportedPackageName(itn.PackageReference())
cast = astbuilder.CallQualifiedFunc(alias, itn.Name(), params.GetSource())
}
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
cast)), nil
}
// a more complex conversion is needed
dstType := et.BaseType()
tmpLocal := builder.CreateLocal(params.Locals, "temp", params.NameHint)
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: params.Source,
SourceType: params.SourceType,
Destination: dst.NewIdent(tmpLocal),
DestinationType: dstType,
NameHint: tmpLocal,
ConversionContext: nil,
AssignmentHandler: nil,
Locals: params.Locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrapf(err, "unable to build inner conversion to enum %s", itn.name)
}
if len(conversion) == 0 {
// unable to build inner conversion
return nil, nil
}
destinationTypeExpr, err := dstType.AsTypeExpr(builder.CodeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating destination type expression")
}
var cast dst.Expr
if builder.CodeGenerationContext.CurrentPackage() == itn.PackageReference() {
cast = astbuilder.CallFunc(itn.Name(), dst.NewIdent(tmpLocal))
} else {
alias := builder.CodeGenerationContext.MustGetImportedPackageName(itn.PackageReference())
cast = astbuilder.CallQualifiedFunc(alias, itn.Name(), dst.NewIdent(tmpLocal))
}
return astbuilder.Statements(
astbuilder.LocalVariableDeclaration(tmpLocal, destinationTypeExpr, ""),
conversion,
params.AssignmentHandlerOrDefault()(
params.GetDestination(),
cast)), nil
}
// AssignFromEnum reads a value from an enum type.
// This function generates code that looks like this:
// <destination> = <primitiveType>(<source>)
func AssignFromEnum(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
// Is the source a typename of an enum?
itn, ok := params.SourceType.(InternalTypeName)
if !ok {
// Not a typename
return nil, nil
}
def, err := builder.CodeGenerationContext.GetDefinition(itn)
if err != nil {
return nil, nil //nolint:nilerr // err is not nil, we defer to a different conversion
}
et, ok := AsEnumType(def.Type())
if !ok {
// Definition isn't for an enum type,
return nil, nil
}
srcType := et.BaseType()
cast := astbuilder.CallFunc(srcType.Name(), params.GetSource())
conversion, err := builder.BuildConversion(
ConversionParameters{
Source: cast,
SourceType: srcType,
Destination: params.Destination,
DestinationType: params.DestinationType,
NameHint: "",
ConversionContext: nil,
AssignmentHandler: nil,
Locals: params.Locals,
SourceProperty: params.SourceProperty,
DestinationProperty: params.DestinationProperty,
})
if err != nil {
return nil, eris.Wrapf(err, "unable to build inner conversion to enum %s", itn.name)
}
if len(conversion) == 0 {
// unable to build inner conversion
return nil, nil
}
return conversion, nil
}
// IdentityAssignValidatedTypeDestination generates an assignment to the underlying validated type Element
func IdentityAssignValidatedTypeDestination(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
validatedType, ok := params.DestinationType.(*ValidatedType)
if !ok {
return nil, nil
}
// pass through to underlying type
params = params.WithDestinationType(validatedType.ElementType())
return builder.BuildConversion(params)
}
// IdentityAssignValidatedTypeSource generates an assignment to the underlying validated type Element
func IdentityAssignValidatedTypeSource(
builder *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
validatedType, ok := params.SourceType.(*ValidatedType)
if !ok {
return nil, nil
}
// pass through to underlying type
params = params.WithSourceType(validatedType.ElementType())
return builder.BuildConversion(params)
}
// IdentityDeepCopyJSON special cases copying JSON-type fields to call the DeepCopy method.
// It generates code that looks like:
//
// <destination> = *<source>.DeepCopy()
func IdentityDeepCopyJSON(
_ *ConversionFunctionBuilder,
params ConversionParameters,
) ([]dst.Stmt, error) {
if !TypeEquals(params.DestinationType, JSONType) {
return nil, nil
}
newSource := astbuilder.Dereference(
&dst.CallExpr{
Fun: astbuilder.Selector(params.GetSource(), "DeepCopy"),
Args: []dst.Expr{},
})
return astbuilder.Statements(
params.AssignmentHandlerOrDefault()(params.GetDestination(), newSource)), nil
}
// AssignmentHandlerDefine is an assignment handler for definitions, using :=
func AssignmentHandlerDefine(lhs dst.Expr, rhs dst.Expr) dst.Stmt {
return astbuilder.AssignmentStatement(lhs, token.DEFINE, rhs)
}
// AssignmentHandlerAssign is an assignment handler for standard assignments to existing variables, using =
func AssignmentHandlerAssign(lhs dst.Expr, rhs dst.Expr) dst.Stmt {
return astbuilder.SimpleAssignment(lhs, rhs)
}
// CreateLocal creates an unused local variable name.
// Names are chosen according to the following rules:
// 1. If there is no local variable with the <suffix> name, use that.
// 2. If there is a local variable with the <suffix> name, create a variable name <nameHint><suffix>.
//
// In the case that <nameHint><suffix> is also taken append numbers to the end in standard KnownLocalsSet fashion.
// Note that this function trims numbers on the right hand side of nameHint, so a nameHint of "item1" will get a local
// variable named item<suffix>.
func (builder *ConversionFunctionBuilder) CreateLocal(locals *KnownLocalsSet, suffix string, nameHint string) string {
ident := suffix
if locals.HasName(ident) || ident == "" {
// Trim any trailing numbers so that we don't end up with ident1111
// Note that this can end up trimming trailing digits on fields that have them (i.e. "loop1" might become "loop").
// This is ok as it doesn't hurt anything and helps avoid "loop12" (for loop1 number 2).
trimmedNameHint := strings.TrimRight(nameHint, "0123456789")
ident = locals.CreateLocal(trimmedNameHint, suffix)
} else {
locals.Add(ident)
}
return ident
}