diff --git a/hack/generator/pkg/codegen/storage/function_injector.go b/hack/generator/pkg/astmodel/function_injector.go similarity index 64% rename from hack/generator/pkg/codegen/storage/function_injector.go rename to hack/generator/pkg/astmodel/function_injector.go index 8b238c715c1..158b3e2ed12 100644 --- a/hack/generator/pkg/codegen/storage/function_injector.go +++ b/hack/generator/pkg/astmodel/function_injector.go @@ -3,21 +3,19 @@ * Licensed under the MIT license. */ -package storage - -import "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +package astmodel // FunctionInjector is a utility for injecting function definitions into resources and objects type FunctionInjector struct { // visitor is used to do the actual injection - visitor astmodel.TypeVisitor + visitor TypeVisitor } // NewFunctionInjector creates a new function injector for modifying resources and objects func NewFunctionInjector() *FunctionInjector { result := &FunctionInjector{} - result.visitor = astmodel.TypeVisitorBuilder{ + result.visitor = TypeVisitorBuilder{ VisitObjectType: result.injectFunctionIntoObject, VisitResourceType: result.injectFunctionIntoResource, }.Build() @@ -26,22 +24,32 @@ func NewFunctionInjector() *FunctionInjector { } // Inject modifies the passed type definition by injecting the passed function -func (fi *FunctionInjector) Inject(def astmodel.TypeDefinition, fn astmodel.Function) (astmodel.TypeDefinition, error) { - return fi.visitor.VisitDefinition(def, fn) +func (fi *FunctionInjector) Inject(def TypeDefinition, fns ...Function) (TypeDefinition, error) { + result := def + + for _, fn := range fns { + var err error + result, err = fi.visitor.VisitDefinition(result, fn) + if err != nil { + return TypeDefinition{}, err + } + } + + return result, nil } // injectFunctionIntoObject takes the function provided as a context and includes it on the // provided object type func (_ *FunctionInjector) injectFunctionIntoObject( - _ *astmodel.TypeVisitor, ot *astmodel.ObjectType, ctx interface{}) (astmodel.Type, error) { - fn := ctx.(astmodel.Function) + _ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) { + fn := ctx.(Function) return ot.WithFunction(fn), nil } // injectFunctionIntoResource takes the function provided as a context and includes it on the // provided resource type func (_ *FunctionInjector) injectFunctionIntoResource( - _ *astmodel.TypeVisitor, rt *astmodel.ResourceType, ctx interface{}) (astmodel.Type, error) { - fn := ctx.(astmodel.Function) + _ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) { + fn := ctx.(Function) return rt.WithFunction(fn), nil } diff --git a/hack/generator/pkg/astmodel/interface_implementation.go b/hack/generator/pkg/astmodel/interface_implementation.go index 3712ac56c39..bfb1a05049f 100644 --- a/hack/generator/pkg/astmodel/interface_implementation.go +++ b/hack/generator/pkg/astmodel/interface_implementation.go @@ -57,6 +57,11 @@ func (iface *InterfaceImplementation) References() TypeNameSet { return results } +// FunctionCount returns the number of included functions +func (iface *InterfaceImplementation) FunctionCount() int { + return len(iface.functions) +} + // Equals determines if this interface is equal to another interface func (iface *InterfaceImplementation) Equals(other *InterfaceImplementation) bool { if len(iface.functions) != len(other.functions) { diff --git a/hack/generator/pkg/astmodel/interface_injector.go b/hack/generator/pkg/astmodel/interface_injector.go new file mode 100644 index 00000000000..807b4b4831d --- /dev/null +++ b/hack/generator/pkg/astmodel/interface_injector.go @@ -0,0 +1,49 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package astmodel + +// InterfaceInjector is a utility for injecting interface implementations into resources and objects +type InterfaceInjector struct { + // visitor is used to do the actual injection + visitor TypeVisitor +} + +// NewInterfaceInjector creates a new interface injector for modifying resources and objects +func NewInterfaceInjector() *InterfaceInjector { + result := &InterfaceInjector{} + + result.visitor = TypeVisitorBuilder{ + VisitObjectType: result.injectInterfaceIntoObject, + VisitResourceType: result.injectInterfaceIntoResource, + }.Build() + + return result +} + +// Inject modifies the passed type definition by injecting the passed function +func (i *InterfaceInjector) Inject(def TypeDefinition, implementation *InterfaceImplementation) (TypeDefinition, error) { + result, err := i.visitor.VisitDefinition(def, implementation) + if err != nil { + return TypeDefinition{}, err + } + return result, nil +} + +// injectFunctionIntoObject takes the function provided as a context and includes it on the +// provided object type +func (i *InterfaceInjector) injectInterfaceIntoObject( + _ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) { + implementation := ctx.(*InterfaceImplementation) + return ot.WithInterface(implementation), nil +} + +// injectFunctionIntoResource takes the function provided as a context and includes it on the +// provided resource type +func (i *InterfaceInjector) injectInterfaceIntoResource( + _ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) { + fn := ctx.(*InterfaceImplementation) + return rt.WithInterface(fn), nil +} diff --git a/hack/generator/pkg/codegen/storage/property_injector.go b/hack/generator/pkg/astmodel/property_injector.go similarity index 65% rename from hack/generator/pkg/codegen/storage/property_injector.go rename to hack/generator/pkg/astmodel/property_injector.go index f933bc177d5..9b4ebe21aec 100644 --- a/hack/generator/pkg/codegen/storage/property_injector.go +++ b/hack/generator/pkg/astmodel/property_injector.go @@ -3,21 +3,19 @@ * Licensed under the MIT license. */ -package storage - -import "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +package astmodel // PropertyInjector is a utility for injecting property definitions into resources and objects type PropertyInjector struct { // visitor is used to do the actual injection - visitor astmodel.TypeVisitor + visitor TypeVisitor } // NewPropertyInjector creates a new property injector for modifying resources and objects func NewPropertyInjector() *PropertyInjector { result := &PropertyInjector{} - result.visitor = astmodel.TypeVisitorBuilder{ + result.visitor = TypeVisitorBuilder{ VisitObjectType: result.injectPropertyIntoObject, VisitResourceType: result.injectPropertyIntoResource, }.Build() @@ -26,20 +24,20 @@ func NewPropertyInjector() *PropertyInjector { } // Inject modifies the passed type definition by injecting the passed property -func (pi *PropertyInjector) Inject(def astmodel.TypeDefinition, prop *astmodel.PropertyDefinition) (astmodel.TypeDefinition, error) { +func (pi *PropertyInjector) Inject(def TypeDefinition, prop *PropertyDefinition) (TypeDefinition, error) { return pi.visitor.VisitDefinition(def, prop) } // injectPropertyIntoObject takes the property provided as a context and includes it on the provided object type func (pi *PropertyInjector) injectPropertyIntoObject( - _ *astmodel.TypeVisitor, ot *astmodel.ObjectType, ctx interface{}) (astmodel.Type, error) { - prop := ctx.(*astmodel.PropertyDefinition) + _ *TypeVisitor, ot *ObjectType, ctx interface{}) (Type, error) { + prop := ctx.(*PropertyDefinition) return ot.WithProperty(prop), nil } // injectPropertyIntoResource takes the property provided as a context and includes it on the provided resource type func (pi *PropertyInjector) injectPropertyIntoResource( - _ *astmodel.TypeVisitor, rt *astmodel.ResourceType, ctx interface{}) (astmodel.Type, error) { - prop := ctx.(*astmodel.PropertyDefinition) + _ *TypeVisitor, rt *ResourceType, ctx interface{}) (Type, error) { + prop := ctx.(*PropertyDefinition) return rt.WithProperty(prop), nil } diff --git a/hack/generator/pkg/astmodel/types.go b/hack/generator/pkg/astmodel/types.go index ac6d0678e0b..a1681acafdf 100644 --- a/hack/generator/pkg/astmodel/types.go +++ b/hack/generator/pkg/astmodel/types.go @@ -245,7 +245,7 @@ func (types Types) ResolveResourceStatusDefinition( } // Process applies a func to transform all members of this set of type definitions, returning a new set of type -// definitions containing the results of the transfomration, or possibly an error +// definitions containing the results of the transformation, or possibly an error // Only definitions returned by the func will be included in the results of the function. The func may return a nil // TypeDefinition if it doesn't want to include anything in the output set. func (types Types) Process(transformation func(definition TypeDefinition) (*TypeDefinition, error)) (Types, error) { diff --git a/hack/generator/pkg/codegen/code_generator.go b/hack/generator/pkg/codegen/code_generator.go index 024d4d18f03..e09a586059f 100644 --- a/hack/generator/pkg/codegen/code_generator.go +++ b/hack/generator/pkg/codegen/code_generator.go @@ -175,6 +175,7 @@ func createAllPipelineStages(idFactory astmodel.IdentifierFactory, configuration See https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/webhook/conversion/conversion.go#L310 pipeline.InjectHubFunction(idFactory).UsedFor(pipeline.ARMTarget), + pipeline.ImplementConvertibleInterface(idFactory), */ // Safety checks at the end: diff --git a/hack/generator/pkg/codegen/pipeline/implement_convertible_interface.go b/hack/generator/pkg/codegen/pipeline/implement_convertible_interface.go new file mode 100644 index 00000000000..48e826d2545 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/implement_convertible_interface.go @@ -0,0 +1,85 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" + "github.com/Azure/azure-service-operator/hack/generator/pkg/functions" +) + +// ImplementConvertibleInterfaceStageId is the unique identifier for this pipeline stage +const ImplementConvertibleInterfaceStageId = "implementConvertibleInterface" + +// ImplementConvertibleInterface injects the functions ConvertTo() and ConvertFrom() into each non-hub Resource +// Type, providing the required implementation of the Convertible interface needed by the controller +func ImplementConvertibleInterface(idFactory astmodel.IdentifierFactory) Stage { + + stage := MakeStage( + ImplementConvertibleInterfaceStageId, + "Implement the Convertible interface on each non-hub Resource type", + func(ctx context.Context, state *State) (*State, error) { + injector := astmodel.NewInterfaceInjector() + + modifiedTypes := make(astmodel.Types) + resources := storage.FindResourceTypes(state.Types()) + for name, def := range resources { + resource, ok := astmodel.AsResourceType(def.Type()) + if !ok { + // Skip non-resources (though, they should be filtered out, above) + continue + } + + if resource.IsStorageVersion() { + // The hub storage version doesn't implement Convertible + continue + } + + convertible := createConvertibleInterfaceImplementation( + name, resource, state.ConversionGraph(), idFactory) + if convertible.FunctionCount() > 0 { + modified, err := injector.Inject(def, convertible) + if err != nil { + return nil, errors.Wrapf(err, "injecting Convertible interface into %s", name) + } + + modifiedTypes.Add(modified) + } + } + + newTypes := state.Types().OverlayWith(modifiedTypes) + return state.WithTypes(newTypes), nil + }) + + stage.RequiresPrerequisiteStages(InjectPropertyAssignmentFunctionsStageId) + return stage +} + +// createConvertibleInterfaceImplementation creates the required implementation of conversion.Convertible, ready for +// injection onto the resource. The ConvertTo() and ConvertFrom() methods chain the required conversion between resource +// versions, but are dependent upon previously injected AssignPropertiesTo() and AssignPropertiesFrom() methods to +// actually copy information across. See resource_conversion_function.go for more information. +func createConvertibleInterfaceImplementation( + name astmodel.TypeName, + resource *astmodel.ResourceType, + conversionGraph *storage.ConversionGraph, + idFactory astmodel.IdentifierFactory) *astmodel.InterfaceImplementation { + var conversionFunctions []astmodel.Function + + for _, fn := range resource.Functions() { + if propertyAssignmentFn, ok := fn.(*functions.PropertyAssignmentFunction); ok { + hub := conversionGraph.FindHubTypeName(name) + conversionFn := functions.NewResourceConversionFunction(hub, propertyAssignmentFn, idFactory) + conversionFunctions = append(conversionFunctions, conversionFn) + } + } + + return astmodel.NewInterfaceImplementation(astmodel.ConvertibleInterface, conversionFunctions...) +} diff --git a/hack/generator/pkg/codegen/pipeline/implement_convertible_interface_test.go b/hack/generator/pkg/codegen/pipeline/implement_convertible_interface_test.go new file mode 100644 index 00000000000..35a17f94397 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/implement_convertible_interface_test.go @@ -0,0 +1,64 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package pipeline + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +// TestInjectConvertibleInterface checks that the pipeline stage does what we expect when run in relative isolation, +// with only a few expected (and closely reated) stages in operation +func TestInjectConvertibleInterface(t *testing.T) { + g := NewGomegaWithT(t) + + idFactory := astmodel.NewIdentifierFactory() + // Test Resource V1 + + specV1 := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty) + statusV1 := test.CreateStatus(test.Pkg2020, "Person") + resourceV1 := test.CreateResource(test.Pkg2020, "Person", specV1, statusV1) + + // Test Resource V2 + + specV2 := test.CreateSpec(test.Pkg2021, "Person", test.FullNameProperty) + statusV2 := test.CreateStatus(test.Pkg2021, "Person") + resourceV2 := test.CreateResource(test.Pkg2021, "Person", specV2, statusV2) + + types := make(astmodel.Types) + types.AddAll(resourceV1, specV1, statusV1, resourceV2, specV2, statusV2) + + state := NewState().WithTypes(types) + + // Run CreateConversionGraph first to populate the conversion graph + createConversionGraph := CreateConversionGraph() + state, err := createConversionGraph.Run(context.TODO(), state) + g.Expect(err).To(Succeed()) + + // Run CreateStorageTypes to create additional resources into which we will inject + createStorageTypes := CreateStorageTypes() + state, err = createStorageTypes.Run(context.TODO(), state) + g.Expect(err).To(Succeed()) + + // Run InjectPropertyAssignmentFunctions to create those functions + injectPropertyFns := InjectPropertyAssignmentFunctions(idFactory) + state, err = injectPropertyFns.Run(context.TODO(), state) + g.Expect(err).To(Succeed()) + + // Now run our stage + injectFunctions := ImplementConvertibleInterface(idFactory) + state, err = injectFunctions.Run(context.TODO(), state) + g.Expect(err).To(Succeed()) + + // When verifying the golden file, check that the implementations of ConvertTo() and ConvertFrom() are what you + // expect - if you don't have expectations, check that they do the right thing. + test.AssertPackagesGenerateExpectedCode(t, state.Types(), t.Name()) +} diff --git a/hack/generator/pkg/codegen/pipeline/inject_hub_function.go b/hack/generator/pkg/codegen/pipeline/inject_hub_function.go index d69d2d1899f..b3b8781ed5d 100644 --- a/hack/generator/pkg/codegen/pipeline/inject_hub_function.go +++ b/hack/generator/pkg/codegen/pipeline/inject_hub_function.go @@ -26,7 +26,7 @@ func InjectHubFunction(idFactory astmodel.IdentifierFactory) Stage { InjectHubFunctionStageId, "Inject the function Hub() into each hub resource", func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { - injector := storage.NewFunctionInjector() + injector := astmodel.NewFunctionInjector() result := types.Copy() resources := storage.FindResourceTypes(types) diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go b/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go index 78e698a14e5..05498126321 100644 --- a/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go +++ b/hack/generator/pkg/codegen/pipeline/inject_original_gvk_function.go @@ -27,7 +27,7 @@ func InjectOriginalGVKFunction(idFactory astmodel.IdentifierFactory) Stage { injectOriginalGVKFunctionId, "Inject the function OriginalGVK() into each Resource type", func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { - injector := storage.NewFunctionInjector() + injector := astmodel.NewFunctionInjector() result := types.Copy() resources := storage.FindResourceTypes(types) diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go index 41667b95638..fa46600f0ff 100644 --- a/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_function.go @@ -28,7 +28,7 @@ func InjectOriginalVersionFunction(idFactory astmodel.IdentifierFactory) Stage { InjectOriginalVersionFunctionStageId, "Inject the function OriginalVersion() into each Spec type", func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { - injector := storage.NewFunctionInjector() + injector := astmodel.NewFunctionInjector() result := types.Copy() specs := storage.FindSpecTypes(types) diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go index 817875930d7..dba03687246 100644 --- a/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_property.go @@ -27,7 +27,7 @@ func InjectOriginalVersionProperty() Stage { InjectOriginalVersionPropertyId, "Inject the property OriginalVersion into each Storage Spec type", func(ctx context.Context, types astmodel.Types) (astmodel.Types, error) { - injector := storage.NewPropertyInjector() + injector := astmodel.NewPropertyInjector() result := types.Copy() doesNotHaveOriginalVersionFunction := func(definition astmodel.TypeDefinition) bool { diff --git a/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go b/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go index 271698b9aa4..65d11e4fbd5 100644 --- a/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go +++ b/hack/generator/pkg/codegen/pipeline/inject_original_version_property_test.go @@ -12,7 +12,6 @@ import ( . "github.com/onsi/gomega" "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" - "github.com/Azure/azure-service-operator/hack/generator/pkg/codegen/storage" "github.com/Azure/azure-service-operator/hack/generator/pkg/functions" "github.com/Azure/azure-service-operator/hack/generator/pkg/test" ) @@ -43,7 +42,7 @@ func TestInjectOriginalVersionProperty_WhenOriginalVersionFunctionFound_DoesNotI g := NewGomegaWithT(t) idFactory := astmodel.NewIdentifierFactory() - fnInjector := storage.NewFunctionInjector() + fnInjector := astmodel.NewFunctionInjector() // Define a test resource spec := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty, test.FamilyNameProperty, test.KnownAsProperty) diff --git a/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go b/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go index 39369e6d91d..be6700bfa6c 100644 --- a/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go +++ b/hack/generator/pkg/codegen/pipeline/inject_property_assignment_functions.go @@ -80,7 +80,7 @@ type propertyAssignmentFunctionsFactory struct { graph *storage.ConversionGraph idFactory astmodel.IdentifierFactory types astmodel.Types - functionInjector *storage.FunctionInjector + functionInjector *astmodel.FunctionInjector } func NewPropertyAssignmentFunctionsFactory( @@ -91,7 +91,7 @@ func NewPropertyAssignmentFunctionsFactory( graph: graph, idFactory: idFactory, types: types, - functionInjector: storage.NewFunctionInjector(), + functionInjector: astmodel.NewFunctionInjector(), } } diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20200101.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20200101.golden new file mode 100644 index 00000000000..02a0eee4e69 --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20200101.golden @@ -0,0 +1,172 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20200101storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +var _ conversion.Convertible = &Person{} + +// ConvertFrom populates our Person from the provided hub Person +func (person *Person) ConvertFrom(hub conversion.Hub) error { + // intermediate variable for conversion + var source v20200101storage.Person + + err := source.ConvertFrom(hub) + if err != nil { + return errors.Wrap(err, "converting from hub to source") + } + + err = person.AssignPropertiesFromPerson(&source) + if err != nil { + return errors.Wrap(err, "converting from source to person") + } + + return nil +} + +// ConvertTo populates the provided hub Person from our Person +func (person *Person) ConvertTo(hub conversion.Hub) error { + // intermediate variable for conversion + var destination v20200101storage.Person + err := person.AssignPropertiesToPerson(&destination) + if err != nil { + return errors.Wrap(err, "converting to destination from person") + } + err = destination.ConvertTo(hub) + if err != nil { + return errors.Wrap(err, "converting from destination to hub") + } + + return nil +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *v20200101storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *v20200101storage.Person) error { + + // Spec + var spec v20200101storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20200101storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FullName: As would be used to address mail + FullName string `json:"fullName"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *Person_Spec) AssignPropertiesFromPersonSpec(source *v20200101storage.Person_Spec) error { + + // FullName + if source.FullName != nil { + personSpec.FullName = *source.FullName + } else { + personSpec.FullName = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *Person_Spec) AssignPropertiesToPersonSpec(destination *v20200101storage.Person_Spec) error { + + // FullName + fullName := personSpec.FullName + destination.FullName = &fullName + + // No error + return nil +} + +type Person_Status struct { + Status string `json:"status"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *Person_Status) AssignPropertiesFromPersonStatus(source *v20200101storage.Person_Status) error { + + // Status + if source.Status != nil { + personStatus.Status = *source.Status + } else { + personStatus.Status = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *Person_Status) AssignPropertiesToPersonStatus(destination *v20200101storage.Person_Status) error { + + // Status + status := personStatus.Status + destination.Status = &status + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20200101storage.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20200101storage.golden new file mode 100644 index 00000000000..ea345158ccc --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20200101storage.golden @@ -0,0 +1,171 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101storage + +import ( + "fmt" + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20200101storage" + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20200101.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20200101storage.Person_Spec `json:"spec,omitempty"` + Status v20200101storage.Person_Status `json:"status,omitempty"` +} + +var _ conversion.Convertible = &Person{} + +// ConvertFrom populates our Person from the provided hub Person +func (person *v20200101storage.Person) ConvertFrom(hub conversion.Hub) error { + source, ok := hub.(*v20211231storage.Person) + if !ok { + return fmt.Errorf("expected storage:microsoft.person/v20211231storage/Person but received %T instead", hub) + } + return person.AssignPropertiesFromPerson(source) +} + +// ConvertTo populates the provided hub Person from our Person +func (person *v20200101storage.Person) ConvertTo(hub conversion.Hub) error { + destination, ok := hub.(*v20211231storage.Person) + if !ok { + return fmt.Errorf("expected storage:microsoft.person/v20211231storage/Person but received %T instead", hub) + } + return person.AssignPropertiesToPerson(destination) +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *v20200101storage.Person) AssignPropertiesFromPerson(source *v20211231storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *v20200101storage.Person) AssignPropertiesToPerson(destination *v20211231storage.Person) error { + + // Spec + var spec v20211231storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20211231storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +//Storage version of v20200101.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20200101storage.Person `json:"items"` +} + +//Storage version of v20200101.Person_Spec +type Person_Spec struct { + FullName *string `json:"fullName,omitempty"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *v20200101storage.Person_Spec) AssignPropertiesFromPersonSpec(source *v20211231storage.Person_Spec) error { + + // FullName + if source.FullName != nil { + fullName := *source.FullName + personSpec.FullName = &fullName + } else { + personSpec.FullName = nil + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *v20200101storage.Person_Spec) AssignPropertiesToPersonSpec(destination *v20211231storage.Person_Spec) error { + + // FullName + if personSpec.FullName != nil { + fullName := *personSpec.FullName + destination.FullName = &fullName + } else { + destination.FullName = nil + } + + // No error + return nil +} + +//Storage version of v20200101.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *v20200101storage.Person_Status) AssignPropertiesFromPersonStatus(source *v20211231storage.Person_Status) error { + + // Status + if source.Status != nil { + status := *source.Status + personStatus.Status = &status + } else { + personStatus.Status = nil + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *v20200101storage.Person_Status) AssignPropertiesToPersonStatus(destination *v20211231storage.Person_Status) error { + + // Status + if personStatus.Status != nil { + status := *personStatus.Status + destination.Status = &status + } else { + destination.Status = nil + } + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&v20200101storage.Person{}, &v20200101storage.PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20211231.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20211231.golden new file mode 100644 index 00000000000..b8cf0ab8f4d --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20211231.golden @@ -0,0 +1,157 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231 + +import ( + "fmt" + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231storage" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +var _ conversion.Convertible = &Person{} + +// ConvertFrom populates our Person from the provided hub Person +func (person *Person) ConvertFrom(hub conversion.Hub) error { + source, ok := hub.(*v20211231storage.Person) + if !ok { + return fmt.Errorf("expected storage:microsoft.person/v20211231storage/Person but received %T instead", hub) + } + return person.AssignPropertiesFromPerson(source) +} + +// ConvertTo populates the provided hub Person from our Person +func (person *Person) ConvertTo(hub conversion.Hub) error { + destination, ok := hub.(*v20211231storage.Person) + if !ok { + return fmt.Errorf("expected storage:microsoft.person/v20211231storage/Person but received %T instead", hub) + } + return person.AssignPropertiesToPerson(destination) +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *v20211231storage.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *v20211231storage.Person) error { + + // Spec + var spec v20211231storage.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status v20211231storage.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +type Person_Spec struct { + //FullName: As would be used to address mail + FullName string `json:"fullName"` +} + +// AssignPropertiesFromPersonSpec populates our Person_Spec from the provided source Person_Spec +func (personSpec *Person_Spec) AssignPropertiesFromPersonSpec(source *v20211231storage.Person_Spec) error { + + // FullName + if source.FullName != nil { + personSpec.FullName = *source.FullName + } else { + personSpec.FullName = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonSpec populates the provided destination Person_Spec from our Person_Spec +func (personSpec *Person_Spec) AssignPropertiesToPersonSpec(destination *v20211231storage.Person_Spec) error { + + // FullName + fullName := personSpec.FullName + destination.FullName = &fullName + + // No error + return nil +} + +type Person_Status struct { + Status string `json:"status"` +} + +// AssignPropertiesFromPersonStatus populates our Person_Status from the provided source Person_Status +func (personStatus *Person_Status) AssignPropertiesFromPersonStatus(source *v20211231storage.Person_Status) error { + + // Status + if source.Status != nil { + personStatus.Status = *source.Status + } else { + personStatus.Status = "" + } + + // No error + return nil +} + +// AssignPropertiesToPersonStatus populates the provided destination Person_Status from our Person_Status +func (personStatus *Person_Status) AssignPropertiesToPersonStatus(destination *v20211231storage.Person_Status) error { + + // Status + status := personStatus.Status + destination.Status = &status + + // No error + return nil +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20211231storage.golden b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20211231storage.golden new file mode 100644 index 00000000000..4b6f3f1daaa --- /dev/null +++ b/hack/generator/pkg/codegen/pipeline/testdata/TestInjectConvertibleInterface-v20211231storage.golden @@ -0,0 +1,41 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20211231storage + +import ( + "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231storage" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +//Storage version of v20211231.Person +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec v20211231storage.Person_Spec `json:"spec,omitempty"` + Status v20211231storage.Person_Status `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +//Storage version of v20211231.Person +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []v20211231storage.Person `json:"items"` +} + +//Storage version of v20211231.Person_Spec +type Person_Spec struct { + FullName *string `json:"fullName,omitempty"` +} + +//Storage version of v20211231.Person_Status +type Person_Status struct { + Status *string `json:"status,omitempty"` +} + +func init() { + SchemeBuilder.Register(&v20211231storage.Person{}, &v20211231storage.PersonList{}) +} diff --git a/hack/generator/pkg/functions/resource_conversion_function.go b/hack/generator/pkg/functions/resource_conversion_function.go new file mode 100644 index 00000000000..3b10ddb5b41 --- /dev/null +++ b/hack/generator/pkg/functions/resource_conversion_function.go @@ -0,0 +1,314 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package functions + +import ( + "fmt" + "go/token" + + "github.com/dave/dst" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astbuilder" + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" +) + +// ResourceConversionFunction implements conversions to/from our hub type +// Existing PropertyAssignment functions are used to implement stepwise conversion +// +// Direct conversion from the hub type: +// +// func ( ) Convert(hub conversion.Hub) error { +// source, ok := hub.(*) +// if !ok { +// return fmt.Errorf("expected but received %T instead", hub) +// } +// return .AssignProperties(source) +// } +// +// Indirect conversion, multiple steps via an intermediate instance +// +// func (r ) Convert(hub conversion.Hub) error { +// var source +// err := source.Convert(hub) +// if err != nil { +// return errors.Wrap(err, "converting from hub to source") +// } +// +// err = .AssignProperties(&source) +// if err != nil { +// return errors.Wrap(err, "converting from source to ") +// } +// +// return nil +// } +// +type ResourceConversionFunction struct { + // hub is the TypeName of the canonical hub type, the final target or original source for conversion + hub astmodel.TypeName + // propertyFunction is a reference to the function we will call to copy properties across + propertyFunction *PropertyAssignmentFunction + // idFactory is a reference to an identifier factory used for creating Go identifiers + idFactory astmodel.IdentifierFactory +} + +// Ensure we properly implement the function interface +var _ astmodel.Function = &ResourceConversionFunction{} + +// NewResourceConversionFunction creates a conversion function that populates our hub type from the current instance +// hub is the TypeName of our hub type +// propertyFuntion is the function we use to copy properties across +func NewResourceConversionFunction( + hub astmodel.TypeName, + propertyFunction *PropertyAssignmentFunction, + idFactory astmodel.IdentifierFactory) *ResourceConversionFunction { + result := &ResourceConversionFunction{ + hub: hub, + propertyFunction: propertyFunction, + idFactory: idFactory, + } + + return result +} + +func (fn *ResourceConversionFunction) Name() string { + return fn.propertyFunction.direction.SelectString("ConvertFrom", "ConvertTo") +} + +func (fn *ResourceConversionFunction) RequiredPackageReferences() *astmodel.PackageReferenceSet { + result := astmodel.NewPackageReferenceSet( + astmodel.GitHubErrorsReference, + astmodel.ControllerRuntimeConversion, + astmodel.FmtReference, + fn.hub.PackageReference) + + // Include the package required by the parameter of the property assignment function + propertyFunctionParameterTypeName := fn.propertyFunction.otherDefinition.Name() + result.AddReference(propertyFunctionParameterTypeName.PackageReference) + + return result +} + +func (fn *ResourceConversionFunction) References() astmodel.TypeNameSet { + // Include the type of the parameter of the property assignment function + propertyFunctionParameterTypeName := fn.propertyFunction.otherDefinition.Name() + + result := astmodel.NewTypeNameSet(fn.hub, propertyFunctionParameterTypeName) + return result +} + +func (fn *ResourceConversionFunction) AsFunc( + generationContext *astmodel.CodeGenerationContext, receiver astmodel.TypeName) *dst.FuncDecl { + + // Create a sensible name for our receiver + receiverName := fn.idFactory.CreateIdentifier(receiver.Name(), astmodel.NotExported) + + // We always use a pointer receiver so we can modify it + receiverType := astmodel.NewOptionalType(receiver).AsType(generationContext) + + funcDetails := &astbuilder.FuncDetails{ + ReceiverIdent: receiverName, + ReceiverType: receiverType, + Name: fn.Name(), + } + + conversionPackage := generationContext.MustGetImportedPackageName(astmodel.ControllerRuntimeConversion) + + funcDetails.AddParameter("hub", astbuilder.QualifiedTypeName(conversionPackage, "Hub")) + funcDetails.AddReturns("error") + funcDetails.AddComments(fn.declarationDocComment(receiver)) + + if fn.hub.Equals(fn.propertyFunction.otherDefinition.Name()) { + // Not using an intermediate step + funcDetails.Body = fn.directConversion(receiverName, generationContext) + } else { + fn.propertyFunction.direction. + WhenFrom(func() { funcDetails.Body = fn.indirectConversionFromHub(receiverName, generationContext) }). + WhenTo(func() { funcDetails.Body = fn.indirectConversionToHub(receiverName, generationContext) }) + } + + return funcDetails.DefineFunc() +} + +// directConversion creates a simple direct conversion between the two types +// +// , ok := .() +// if !ok { +// return errors.Errorf("expected but received %T instead", ) +// } +// +// return .AssignProperties(To|From)() +// +func (fn *ResourceConversionFunction) directConversion( + receiverName string, generationContext *astmodel.CodeGenerationContext) []dst.Stmt { + fmtPackage := generationContext.MustGetImportedPackageName(astmodel.FmtReference) + + localId := fn.localVariableId() + localIdent := dst.NewIdent(localId) + hubIdent := dst.NewIdent("hub") + + assignLocal := astbuilder.TypeAssert( + localIdent, + hubIdent, + astbuilder.Dereference(fn.hub.AsType(generationContext))) + + checkAssert := astbuilder.ReturnIfNotOk( + astbuilder.FormatError( + fmtPackage, + fmt.Sprintf("expected %s but received %%T instead", fn.hub), + hubIdent)) + + copyAndReturn := astbuilder.Returns( + astbuilder.CallExpr(dst.NewIdent(receiverName), fn.propertyFunction.Name(), localIdent)) + + return astbuilder.Statements( + assignLocal, + checkAssert, + copyAndReturn) +} + +// indirectConversionFromHub generates a conversion when the type we know about isn't the hub type, but is closer to it +// in our conversion graph +// +// var source +// err := source.ConvertFrom() +// if err != nil { +// return errors.Wrap(err, "converting from hub to source") +// } +// +// err = .AssignPropertiesFrom(&source) +// if err != nil { +// return errors.Wrap(err, "converting from source to ") +// } +// +// return nil +// +func (fn *ResourceConversionFunction) indirectConversionFromHub( + receiverName string, generationContext *astmodel.CodeGenerationContext) []dst.Stmt { + errorsPackage := generationContext.MustGetImportedPackageName(astmodel.GitHubErrorsReference) + localId := fn.localVariableId() + errIdent := dst.NewIdent("err") + + intermediateType := fn.propertyFunction.otherDefinition.Name() + + declareLocal := astbuilder.LocalVariableDeclaration( + localId, intermediateType.AsType(generationContext), "// intermediate variable for conversion") + declareLocal.Decorations().Before = dst.NewLine + + populateLocalFromHub := astbuilder.SimpleAssignment( + errIdent, + token.DEFINE, + astbuilder.CallExpr(dst.NewIdent(localId), fn.Name(), dst.NewIdent("hub"))) + populateLocalFromHub.Decs.Before = dst.EmptyLine + + checkForErrorsPopulatingLocal := astbuilder.CheckErrorAndWrap( + errorsPackage, + fmt.Sprintf("converting from hub to %s", localId)) + + populateReceiverFromLocal := astbuilder.SimpleAssignment( + errIdent, + token.ASSIGN, + astbuilder.CallExpr(dst.NewIdent(receiverName), fn.propertyFunction.Name(), astbuilder.AddrOf(dst.NewIdent(localId)))) + populateReceiverFromLocal.Decs.Before = dst.EmptyLine + + checkForErrorsPopulatingReceiver := astbuilder.CheckErrorAndWrap( + errorsPackage, + fmt.Sprintf("converting from %s to %s", localId, receiverName)) + + returnNil := astbuilder.Returns(dst.NewIdent("nil")) + returnNil.Decorations().Before = dst.EmptyLine + + return astbuilder.Statements( + declareLocal, + populateLocalFromHub, + checkForErrorsPopulatingLocal, + populateReceiverFromLocal, + checkForErrorsPopulatingReceiver, + returnNil) +} + +// indirectConversionToHub generates a conversion when the type we know about isn't the hub type, but is closer to it in +// our conversion graph +// +// var destination +// err = .AssignPropertiesTo(&destination) +// if err != nil { +// return errors.Wrap(err, "converting to destination from ") +// } +// +// err := destination.ConvertTo() +// if err != nil { +// return errors.Wrap(err, "converting from destination to hub") +// } +// +// return nil +// +func (fn *ResourceConversionFunction) indirectConversionToHub( + receiverName string, generationContext *astmodel.CodeGenerationContext) []dst.Stmt { + errorsPackage := generationContext.MustGetImportedPackageName(astmodel.GitHubErrorsReference) + localId := fn.localVariableId() + errIdent := dst.NewIdent("err") + + intermediateType := fn.propertyFunction.otherDefinition.Name() + + declareLocal := astbuilder.LocalVariableDeclaration( + localId, intermediateType.AsType(generationContext), "// intermediate variable for conversion") + declareLocal.Decorations().Before = dst.NewLine + + populateLocalFromReceiver := astbuilder.SimpleAssignment( + errIdent, + token.DEFINE, + astbuilder.CallExpr(dst.NewIdent(receiverName), fn.propertyFunction.Name(), astbuilder.AddrOf(dst.NewIdent(localId)))) + + checkForErrorsPopulatingLocal := astbuilder.CheckErrorAndWrap( + errorsPackage, + fmt.Sprintf("converting to %s from %s", localId, receiverName)) + + populateHubFromLocal := astbuilder.SimpleAssignment( + errIdent, + token.ASSIGN, + astbuilder.CallExpr(dst.NewIdent(localId), fn.Name(), dst.NewIdent("hub"))) + + checkForErrorsPopulatingHub := astbuilder.CheckErrorAndWrap( + errorsPackage, + fmt.Sprintf("converting from %s to hub", localId)) + + returnNil := astbuilder.Returns(dst.NewIdent("nil")) + returnNil.Decorations().Before = dst.EmptyLine + + return astbuilder.Statements( + declareLocal, + populateLocalFromReceiver, + checkForErrorsPopulatingLocal, + populateHubFromLocal, + checkForErrorsPopulatingHub, + returnNil) +} + +// localVariableId returns a good identifier to use for a local variable in our function, +// based which direction we are converting +func (fn *ResourceConversionFunction) localVariableId() string { + return fn.propertyFunction.direction.SelectString("source", "destination") +} + +func (fn *ResourceConversionFunction) declarationDocComment(receiver astmodel.TypeName) string { + return fn.propertyFunction.direction.SelectString( + fmt.Sprintf("populates our %s from the provided hub %s", receiver.Name(), fn.hub.Name()), + fmt.Sprintf("populates the provided hub %s from our %s", fn.hub.Name(), receiver.Name())) +} + +func (fn *ResourceConversionFunction) Equals(otherFn astmodel.Function) bool { + rcf, ok := otherFn.(*ResourceConversionFunction) + if !ok { + return false + } + + if !fn.propertyFunction.Equals(rcf.propertyFunction) { + return false + } + + return fn.Name() == rcf.Name() && + fn.hub.Equals(rcf.hub) +} diff --git a/hack/generator/pkg/functions/resource_conversion_function_test.go b/hack/generator/pkg/functions/resource_conversion_function_test.go new file mode 100644 index 00000000000..ffd6594d229 --- /dev/null +++ b/hack/generator/pkg/functions/resource_conversion_function_test.go @@ -0,0 +1,107 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package functions + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/Azure/azure-service-operator/hack/generator/pkg/astmodel" + "github.com/Azure/azure-service-operator/hack/generator/pkg/conversions" + "github.com/Azure/azure-service-operator/hack/generator/pkg/test" +) + +// Test_ResourceConversionFunction_DirectConversion_GeneratesExpectedCode tests the code when the ConvertTo() and +// ConvertFrom() functions are directly converting to/from the Hub type, without any intermediate step. +func Test_ResourceConversionFunction_DirectConversion_GeneratesExpectedCode(t *testing.T) { + g := NewGomegaWithT(t) + idFactory := astmodel.NewIdentifierFactory() + + // Create our upstream type + personSpec2020 := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty, test.KnownAsProperty, test.FamilyNameProperty) + personStatus2020 := test.CreateStatus(test.Pkg2020, "Person") + person2020 := test.CreateResource(test.Pkg2020, "Person", personSpec2020, personStatus2020) + + // Create our downstream type + personSpec2021 := test.CreateSpec(test.Pkg2021, "Person", test.FullNameProperty, test.KnownAsProperty, test.FamilyNameProperty, test.CityProperty) + personStatus2021 := test.CreateStatus(test.Pkg2021, "Person") + person2021 := test.CreateResource(test.Pkg2021, "Person", personSpec2021, personStatus2021) + + // Create Property Assignment functions + types := make(astmodel.Types) + types.AddAll(person2020, personSpec2020, personStatus2020) + types.AddAll(person2021, personSpec2021, personStatus2021) + + conversionContext := conversions.NewPropertyConversionContext(types, idFactory) + propertyAssignTo, err := NewPropertyAssignmentToFunction(person2020, person2021, idFactory, conversionContext) + g.Expect(err).To(Succeed()) + + propertyAssignFrom, err := NewPropertyAssignmentFromFunction(person2020, person2021, idFactory, conversionContext) + g.Expect(err).To(Succeed()) + + // Create Resource Conversion Functions + convertTo := NewResourceConversionFunction(person2021.Name(), propertyAssignTo, idFactory) + convertFrom := NewResourceConversionFunction(person2021.Name(), propertyAssignFrom, idFactory) + + // Inject these methods into person2020 + injector := astmodel.NewFunctionInjector() + person2020, err = injector.Inject(person2020, propertyAssignTo, propertyAssignFrom, convertTo, convertFrom) + g.Expect(err).To(Succeed()) + + // Write to a file + fileDef := test.CreateFileDefinition(person2020) + test.AssertFileGeneratesExpectedCode(t, fileDef, t.Name()) +} + +// Test_ResourceConversionFunction_IndirectConversion_GeneratesExpectedCode tests the code when the ConvertTo() and +// ConvertFrom() functions can't convert directly to/from the hub type and are forced to stage the conversion on an +// intermediate type. +func Test_ResourceConversionFunction_IndirectConversion_GeneratesExpectedCode(t *testing.T) { + g := NewGomegaWithT(t) + idFactory := astmodel.NewIdentifierFactory() + + // Create our upstream type + personSpec2020 := test.CreateSpec(test.Pkg2020, "Person", test.FullNameProperty, test.KnownAsProperty, test.FamilyNameProperty) + personStatus2020 := test.CreateStatus(test.Pkg2020, "Person") + person2020 := test.CreateResource(test.Pkg2020, "Person", personSpec2020, personStatus2020) + + // Create our downstream type - directly convertible with the upstream type + personSpec2021 := test.CreateSpec(test.Pkg2021, "Person", test.FullNameProperty, test.KnownAsProperty, test.FamilyNameProperty, test.CityProperty) + personStatus2021 := test.CreateStatus(test.Pkg2021, "Person") + person2021 := test.CreateResource(test.Pkg2021, "Person", personSpec2021, personStatus2021) + + // Create our hub type - multiple conversion steps away from Pkg2020 + personSpec2022 := test.CreateSpec(test.Pkg2022, "Person", test.FullNameProperty, test.KnownAsProperty, test.FamilyNameProperty, test.CityProperty) + personStatus2022 := test.CreateStatus(test.Pkg2022, "Person") + person2022 := test.CreateResource(test.Pkg2022, "Person", personSpec2021, personStatus2021) + + // Create Property Assignment functions + types := make(astmodel.Types) + types.AddAll(person2020, personSpec2020, personStatus2020) + types.AddAll(person2021, personSpec2021, personStatus2021) + types.AddAll(person2022, personSpec2022, personStatus2022) + + conversionContext := conversions.NewPropertyConversionContext(types, idFactory) + propertyAssignTo, err := NewPropertyAssignmentToFunction(person2020, person2021, idFactory, conversionContext) + g.Expect(err).To(Succeed()) + + propertyAssignFrom, err := NewPropertyAssignmentFromFunction(person2020, person2021, idFactory, conversionContext) + g.Expect(err).To(Succeed()) + + // Create Resource Conversion Functions + convertTo := NewResourceConversionFunction(person2022.Name(), propertyAssignTo, idFactory) + convertFrom := NewResourceConversionFunction(person2022.Name(), propertyAssignFrom, idFactory) + + // Inject these methods into person2020 + injector := astmodel.NewFunctionInjector() + person2020, err = injector.Inject(person2020, propertyAssignTo, propertyAssignFrom, convertTo, convertFrom) + g.Expect(err).To(Succeed()) + + // Write to a file + fileDef := test.CreateFileDefinition(person2020) + test.AssertFileGeneratesExpectedCode(t, fileDef, t.Name()) +} diff --git a/hack/generator/pkg/functions/testdata/Test_ResourceConversionFunction_DirectConversion_GeneratesExpectedCode.golden b/hack/generator/pkg/functions/testdata/Test_ResourceConversionFunction_DirectConversion_GeneratesExpectedCode.golden new file mode 100644 index 00000000000..d77d7b81634 --- /dev/null +++ b/hack/generator/pkg/functions/testdata/Test_ResourceConversionFunction_DirectConversion_GeneratesExpectedCode.golden @@ -0,0 +1,96 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + "fmt" + person "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *person.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *person.Person) error { + + // Spec + var spec person.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status person.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// ConvertFrom populates our Person from the provided hub Person +func (person *Person) ConvertFrom(hub conversion.Hub) error { + source, ok := hub.(*person.Person) + if !ok { + return fmt.Errorf("expected github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231/Person but received %T instead", hub) + } + return person.AssignPropertiesFromPerson(source) +} + +// ConvertTo populates the provided hub Person from our Person +func (person *Person) ConvertTo(hub conversion.Hub) error { + destination, ok := hub.(*person.Person) + if !ok { + return fmt.Errorf("expected github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231/Person but received %T instead", hub) + } + return person.AssignPropertiesToPerson(destination) +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +} diff --git a/hack/generator/pkg/functions/testdata/Test_ResourceConversionFunction_IndirectConversion_GeneratesExpectedCode.golden b/hack/generator/pkg/functions/testdata/Test_ResourceConversionFunction_IndirectConversion_GeneratesExpectedCode.golden new file mode 100644 index 00000000000..9b0302cc37f --- /dev/null +++ b/hack/generator/pkg/functions/testdata/Test_ResourceConversionFunction_IndirectConversion_GeneratesExpectedCode.golden @@ -0,0 +1,111 @@ +// Code generated by azure-service-operator-codegen. DO NOT EDIT. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package v20200101 + +import ( + personv20211231 "github.com/Azure/azure-service-operator/testing/microsoft.person/v20211231" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Person struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Person_Spec `json:"spec,omitempty"` + Status Person_Status `json:"status,omitempty"` +} + +// AssignPropertiesFromPerson populates our Person from the provided source Person +func (person *Person) AssignPropertiesFromPerson(source *personv20211231.Person) error { + + // Spec + var spec Person_Spec + err := spec.AssignPropertiesFromPersonSpec(&source.Spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesFromPersonSpec()") + } + person.Spec = spec + + // Status + var status Person_Status + err = status.AssignPropertiesFromPersonStatus(&source.Status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesFromPersonStatus()") + } + person.Status = status + + // No error + return nil +} + +// AssignPropertiesToPerson populates the provided destination Person from our Person +func (person *Person) AssignPropertiesToPerson(destination *personv20211231.Person) error { + + // Spec + var spec personv20211231.Person_Spec + err := person.Spec.AssignPropertiesToPersonSpec(&spec) + if err != nil { + return errors.Wrap(err, "populating Spec from Spec, calling AssignPropertiesToPersonSpec()") + } + destination.Spec = spec + + // Status + var status personv20211231.Person_Status + err = person.Status.AssignPropertiesToPersonStatus(&status) + if err != nil { + return errors.Wrap(err, "populating Status from Status, calling AssignPropertiesToPersonStatus()") + } + destination.Status = status + + // No error + return nil +} + +// ConvertFrom populates our Person from the provided hub Person +func (person *Person) ConvertFrom(hub conversion.Hub) error { + // intermediate variable for conversion + var source personv20211231.Person + + err := source.ConvertFrom(hub) + if err != nil { + return errors.Wrap(err, "converting from hub to source") + } + + err = person.AssignPropertiesFromPerson(&source) + if err != nil { + return errors.Wrap(err, "converting from source to person") + } + + return nil +} + +// ConvertTo populates the provided hub Person from our Person +func (person *Person) ConvertTo(hub conversion.Hub) error { + // intermediate variable for conversion + var destination personv20211231.Person + err := person.AssignPropertiesToPerson(&destination) + if err != nil { + return errors.Wrap(err, "converting to destination from person") + } + err = destination.ConvertTo(hub) + if err != nil { + return errors.Wrap(err, "converting from destination to hub") + } + + return nil +} + +// +kubebuilder:object:root=true +type PersonList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Person `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Person{}, &PersonList{}) +}