v2/tools/generator/internal/functions/property_assignment_function.go (241 lines of code) (raw):

/* * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. */ package functions import ( "fmt" "go/token" "sort" "github.com/dave/dst" "github.com/rotisserie/eris" "golang.org/x/exp/maps" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/astbuilder" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/conversions" ) // PropertyAssignmentFunction represents a function that assigns all the properties from one resource or object to // another. Used to perform a single step of the conversions required to/from the hub version, or to convert from // status to spec. type PropertyAssignmentFunction struct { // Name is the unique name of this function name string // receiverDefinition is the type on which this function will be hosted receiverDefinition astmodel.TypeDefinition // otherDefinition is the type we are converting to (or from). This will be a type which is "closer" // to the hub storage type, making this a building block of the final conversion. otherDefinition astmodel.TypeDefinition // conversions is a map of all property conversions we are going to use, keyed by name of the // receiver endpoint (which may be a property, function, or property bag item) conversions map[string]StoragePropertyConversion // idFactory is a reference to an identifier factory used for creating Go identifiers idFactory astmodel.IdentifierFactory // direction indicates the kind of conversion we are generating direction conversions.Direction // conversionContext is additional information about the context in which this conversion was made conversionContext *conversions.PropertyConversionContext // identifier to use for our receiver in generated code receiverName string // identifier to use for our parameter in generated code parameterName string // readsFromPropertyBag keeps track of whether we will be reading property values from a property bag readsFromPropertyBag bool // writesToPropertyBag keeps track of whether we will be writing property values into a property bag writesToPropertyBag bool // augmentationInterface is the conversion augmentation interface associated with this conversion. // If this is nil, there is no augmented conversion associated with this conversion augmentationInterface astmodel.TypeName // packageReferences is our set of referenced packages packageReferences *astmodel.PackageReferenceSet // knownLocals is the set of local variables in the function knownLocals *astmodel.KnownLocalsSet // sourcePropertyBag is the (optional) property bag property on the source type sourcePropertyBag *astmodel.PropertyDefinition // destinationPropertyBag is the (optional) property bag property on the destination type destinationPropertyBag *astmodel.PropertyDefinition } // StoragePropertyConversion represents a function that generates the correct AST to convert a single property value // Different functions will be used, depending on the types of the properties to be converted. // source is an expression that returns the source we are converting from (a Resource or other Object) // destination is an expression that returns the destination we are converting to (again, a Resource or other Object) // The function returns a sequence of statements to carry out the stated conversion/copy type StoragePropertyConversion func( source dst.Expr, destination dst.Expr, knownLocals *astmodel.KnownLocalsSet, generationContext *astmodel.CodeGenerationContext) ([]dst.Stmt, error) // Ensure that PropertyAssignmentFunction implements Function var _ astmodel.Function = &PropertyAssignmentFunction{} // Name returns the name of this function func (fn *PropertyAssignmentFunction) Name() string { return fn.name } // RequiredPackageReferences returns the set of package references required by this function func (fn *PropertyAssignmentFunction) RequiredPackageReferences() *astmodel.PackageReferenceSet { return fn.packageReferences } // References returns the set of types referenced by this function func (fn *PropertyAssignmentFunction) References() astmodel.TypeNameSet { result := astmodel.NewTypeNameSet(fn.ParameterType()) if fn.augmentationInterface != nil { result.Add(fn.augmentationInterface) } return result } // Equals checks to see if the supplied function is the same as this one func (fn *PropertyAssignmentFunction) Equals(f astmodel.Function, _ astmodel.EqualityOverrides) bool { if other, ok := f.(*PropertyAssignmentFunction); ok { if fn.Name() != other.Name() { // Different name means not-equal return false } if len(fn.conversions) != len(other.conversions) { // Different count of conversions means not-equal return false } for name := range fn.conversions { if _, found := other.conversions[name]; !found { // Missing conversion means not-equal return false } } return true } return false } // Direction returns this functions direction of conversion func (fn *PropertyAssignmentFunction) Direction() conversions.Direction { return fn.direction } // AsFunc renders this function as an AST for serialization to a Go source file func (fn *PropertyAssignmentFunction) AsFunc( codeGenerationContext *astmodel.CodeGenerationContext, receiver astmodel.InternalTypeName, ) (*dst.FuncDecl, error) { description := fn.direction.SelectString( fmt.Sprintf("populates our %s from the provided source %s", receiver.Name(), fn.ParameterType().Name()), fmt.Sprintf("populates the provided destination %s from our %s", fn.ParameterType().Name(), receiver.Name())) // We always use a pointer receiver, so we can modify it receiverType := astmodel.NewOptionalType(receiver) receiverTypeExpr, err := receiverType.AsTypeExpr(codeGenerationContext) if err != nil { return nil, eris.Wrap(err, "creating receiver type expression") } body, err := fn.generateBody(fn.receiverName, fn.parameterName, codeGenerationContext) if err != nil { return nil, eris.Wrapf(err, "unable to generate body for %s", fn.Name()) } funcDetails := &astbuilder.FuncDetails{ ReceiverIdent: fn.receiverName, ReceiverType: receiverTypeExpr, Name: fn.Name(), Body: body, } parameterTypeExpr, err := astmodel.NewOptionalType(fn.ParameterType()). AsTypeExpr(codeGenerationContext) if err != nil { return nil, eris.Wrap(err, "creating parameter type expression") } funcDetails.AddParameter(fn.parameterName, parameterTypeExpr) funcDetails.AddReturns("error") funcDetails.AddComments(description) return funcDetails.DefineFunc(), nil } func (fn *PropertyAssignmentFunction) ParameterType() astmodel.TypeName { return fn.otherDefinition.Name() } // generateBody returns the statements required for the conversion function // receiver is an expression for access our receiver type, used to qualify field access // parameter is an expression for access to our parameter passed to the function, also used for field access // generationContext is our code generation context, passed to allow resolving of identifiers in other packages func (fn *PropertyAssignmentFunction) generateBody( receiver string, parameter string, generationContext *astmodel.CodeGenerationContext, ) ([]dst.Stmt, error) { // source is the identifier from which we are reading values source := fn.direction.SelectString(parameter, receiver) // destination is the identifier onto which we write values destination := fn.direction.SelectString(receiver, parameter) knownLocals := fn.knownLocals.Clone() bagPrologue := fn.createPropertyBagPrologue(source, generationContext) assignments, err := fn.generateAssignments(knownLocals, dst.NewIdent(source), dst.NewIdent(destination), generationContext) if err != nil { return nil, eris.Wrapf(err, "unable to generate assignments for %s", fn.Name()) } bagEpilogue := fn.propertyBagEpilogue(destination) handleOverrideInterface, err := fn.handleAugmentationInterface(receiver, parameter, knownLocals, generationContext) if err != nil { return nil, eris.Wrapf(err, "generating augmentation interface handling for %s", fn.Name()) } return astbuilder.Statements( bagPrologue, assignments, bagEpilogue, handleOverrideInterface, astbuilder.ReturnNoError()), nil } // createPropertyBagPrologue creates any introductory statements needed to set up our property bag before we start doing // assignments. We need to handle three cases: // // o If our source has a property bag, we clone it. // o If our destination has a property bag (and our source does not), we create a new one. // o If neither source nor destination has a property bag, we don't need to do anything. // // source is the name of the source to read the property bag from func (fn *PropertyAssignmentFunction) createPropertyBagPrologue( source string, generationContext *astmodel.CodeGenerationContext, ) []dst.Stmt { // We don't need the prologue if we're not using a property bag at all. // So when are we using one? // - If we're reading from the property bag // - If we're writing to the property bag // - If our destination has a property bag that needs initialization if !fn.readsFromPropertyBag && !fn.writesToPropertyBag && fn.destinationPropertyBag == nil { return nil } // Don't refactor the local genruntimePkg out to this scope - calling MustGetImportedPackageName() flags the // package as referenced, so we must only call that if we are actually going to reference the genruntime package var createBag dst.Expr var comment string genruntimePkg := generationContext.MustGetImportedPackageName(astmodel.GenRuntimeReference) if fn.sourcePropertyBag != nil { createBag = astbuilder.CallQualifiedFunc( genruntimePkg, "NewPropertyBag", astbuilder.Selector(dst.NewIdent(source), string(fn.sourcePropertyBag.PropertyName()))) comment = "// Clone the existing property bag" } else { createBag = astbuilder.CallQualifiedFunc( genruntimePkg, "NewPropertyBag") comment = "// Create a new property bag" } initializeBag := astbuilder.ShortDeclaration( fn.conversionContext.PropertyBagName(), createBag) initializeBag.Decs.Before = dst.NewLine astbuilder.AddComment(&initializeBag.Decorations().Start, comment) return astbuilder.Statements(initializeBag) } // propertyBagEpilogue creates any concluding statements required to handle our property bag after assignments are // complete. // // o If the destination has a property bag // > If our bag is empty, we set the destination to nil // > Otherwise we need to store our current property bag there // o Otherwise we do nothing func (fn *PropertyAssignmentFunction) propertyBagEpilogue( destination string, ) []dst.Stmt { prop := fn.destinationPropertyBag found := prop != nil if found { bagID := dst.NewIdent(fn.conversionContext.PropertyBagName()) bagProperty := astbuilder.Selector(dst.NewIdent(destination), string(prop.PropertyName())) condition := astbuilder.BinaryExpr(astbuilder.CallFunc("len", bagID), token.GTR, astbuilder.IntLiteral(0)) storeBag := astbuilder.SimpleAssignment(bagProperty, bagID) storeNil := astbuilder.SimpleAssignment(bagProperty, astbuilder.Nil()) store := astbuilder.SimpleIfElse( condition, astbuilder.Statements(storeBag), astbuilder.Statements(storeNil)) store.Decs.Before = dst.EmptyLine astbuilder.AddComment(&store.Decorations().Start, "// Update the property bag") return astbuilder.Statements(store) } return nil } // handleAugmentationInterface handles dealing with the override interface if there is one // Generates code that looks like: // // var accountAsAny any = account // if augmented, ok := accountAsAny.(augmentConversionForBatchAccount); ok { // err := augmentedAccount.AssignPropertiesFrom(source) // if err != nil { // return errors.Wrap( // err, // "calling augmented AssignPropertiesFrom() for conversion from v20210101s.BatchAccount") // } // } func (fn *PropertyAssignmentFunction) handleAugmentationInterface( receiver string, parameter string, knownLocals *astmodel.KnownLocalsSet, generationContext *astmodel.CodeGenerationContext, ) ([]dst.Stmt, error) { if fn.augmentationInterface == nil { return nil, nil } augmentationInterfaceExpr, err := fn.augmentationInterface.AsTypeExpr(generationContext) if err != nil { return nil, eris.Wrap(err, "creating augmentation interface type expression") } receiverAsAnyIdent := knownLocals.CreateLocal(receiver + "AsAny") sourceAsAny := astbuilder.NewVariableAssignmentWithType(receiverAsAnyIdent, dst.NewIdent("any"), dst.NewIdent(receiver)) // Clone locals at this point as we're entering an if block knownLocals = knownLocals.Clone() augmentedReceiverIdent := knownLocals.CreateLocal("augmented" + fn.idFactory.CreateIdentifier(receiver, astmodel.Exported)) conversionFuncName := fn.Direction().SelectString("AssignPropertiesFrom", "AssignPropertiesTo") callAssignOverride := astbuilder.ShortDeclaration( "err", astbuilder.CallQualifiedFunc(augmentedReceiverIdent, conversionFuncName, dst.NewIdent(parameter))) returnIfNotNil := astbuilder.ReturnIfNotNil( dst.NewIdent("err"), astbuilder.WrappedError( generationContext.MustGetImportedPackageName(astmodel.ErisReference), fmt.Sprintf("calling augmented %s() for conversion", conversionFuncName))) ifStmt := astbuilder.IfType( dst.NewIdent(receiverAsAnyIdent), augmentationInterfaceExpr, augmentedReceiverIdent, callAssignOverride, returnIfNotNil) sourceAsAny.Decorations().Before = dst.EmptyLine sourceAsAny.Decorations().Start.Prepend(fmt.Sprintf("// Invoke the %s interface (if implemented) to customize the conversion", fn.augmentationInterface.Name())) return astbuilder.Statements( sourceAsAny, ifStmt), nil } // generateAssignments generates a sequence of statements to copy information between the two types func (fn *PropertyAssignmentFunction) generateAssignments( knownLocals *astmodel.KnownLocalsSet, source dst.Expr, destination dst.Expr, generationContext *astmodel.CodeGenerationContext, ) ([]dst.Stmt, error) { var result []dst.Stmt // Find all the properties for which we have a conversion properties := maps.Keys(fn.conversions) // Sort the properties into alphabetical order to ensure deterministic generation sort.Strings(properties) // Accumulate all the statements required for conversions, in alphabetical order for _, prop := range properties { conversion := fn.conversions[prop] block, err := conversion(source, destination, knownLocals, generationContext) if err != nil { return nil, eris.Wrapf(err, "property %s", prop) } if len(block) > 0 { firstStatement := block[0] firstStatement.Decorations().Before = dst.EmptyLine firstStatement.Decorations().Start.Prepend("// " + prop) result = append(result, block...) } } return result, nil }