v2/tools/generator/internal/config/group_configuration.go (136 lines of code) (raw):

/* * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. */ package config import ( "fmt" "strings" "github.com/rotisserie/eris" "gopkg.in/yaml.v3" kerrors "k8s.io/apimachinery/pkg/util/errors" "github.com/Azure/azure-service-operator/v2/internal/set" "github.com/Azure/azure-service-operator/v2/internal/util/typo" "github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel" ) // GroupConfiguration contains additional information about an entire group and forms the top of a hierarchy containing // information to supplement the schema and swagger sources consumed by the generator. // // ┌──────────────────────────┐ ╔════════════════════╗ ┌──────────────────────┐ ┌───────────────────┐ ┌───────────────────────┐ // │ │ ║ ║ │ │ │ │ │ │ // │ ObjectModelConfiguration │───────║ GroupConfiguration ║───────│ VersionConfiguration │───────│ TypeConfiguration │───────│ PropertyConfiguration │ // │ │1 1..n║ ║1 1..n│ │1 1..n│ │1 1..n│ │ // └──────────────────────────┘ ╚════════════════════╝ └──────────────────────┘ └───────────────────┘ └───────────────────────┘ type GroupConfiguration struct { name string versions map[string]*VersionConfiguration advisor *typo.Advisor // Configurable properties here (alphabetical, please) PayloadType configurable[PayloadType] // Specify how this property should be serialized for ARM } type PayloadType string const ( OmitEmptyProperties PayloadType = "omitempty" // Omit all empty properties even collections ExplicitCollections PayloadType = "explicitcollections" // Always include collections (as null), omit other empty properties ExplicitEmptyCollections PayloadType = "explicitemptycollections" // Always include collections (as empty map/array), omit other empty properties ExplicitProperties PayloadType = "explicitproperties" // Always include all properties ) const ( payloadTypeTag = "$payloadType" // Enumeration specifying what kind of payload to send to ARM. ) // NewGroupConfiguration returns a new (empty) GroupConfiguration func NewGroupConfiguration(name string) *GroupConfiguration { scope := "group " + name return &GroupConfiguration{ name: name, versions: make(map[string]*VersionConfiguration), advisor: typo.NewAdvisor(), // Initialize configurable properties here (alphabetical, please) PayloadType: makeConfigurable[PayloadType](payloadTypeTag, scope), } } // Add includes configuration for the specified version as a part of this group configuration // In addition to indexing by the name of the version, we also index by the local-package-name and storage-package-name // of the version, so we can do lookups via TypeName. All indexing is lower-case to allow case-insensitive lookups (this // makes our configuration more forgiving). func (gc *GroupConfiguration) addVersion(name string, version *VersionConfiguration) { // Convert version.name into a package version // We do this by constructing a local package reference because this avoids replicating the logic here and risking // inconsistency if things are changed in the future. local := astmodel.MakeLocalPackageReference("prefix", "group", astmodel.GeneratorVersion, name) gc.versions[strings.ToLower(name)] = version gc.versions[strings.ToLower(local.Version())] = version } // visitVersion invokes the provided visitor on the specified version if present. // Returns a NotConfiguredError if the version is not found; otherwise whatever error is returned by the visitor. func (gc *GroupConfiguration) visitVersion( ref astmodel.PackageReference, visitor *configurationVisitor, ) error { vc := gc.findVersion(ref) if vc == nil { return nil } err := visitor.visitVersion(vc) if err != nil { return eris.Wrapf(err, "configuration of group %s", gc.name) } return nil } // visitVersions invokes the provided visitor on all versions. func (gc *GroupConfiguration) visitVersions(visitor *configurationVisitor) error { // All our versions are listed under multiple keys, so we hedge against processing them multiple times versionsSeen := set.Make[string]() errs := make([]error, 0, len(gc.versions)) for _, v := range gc.versions { if versionsSeen.Contains(v.name) { continue } err := visitor.visitVersion(v) err = gc.advisor.Wrapf(err, v.name, "version %s not seen", v.name) errs = append(errs, err) versionsSeen.Add(v.name) } // Both errors.Wrapf() and kerrors.NewAggregate() return nil if nothing went wrong return eris.Wrapf( kerrors.NewAggregate(errs), "group %s", gc.name) } // findVersion uses the provided PackageReference to work out which nested VersionConfiguration should be used func (gc *GroupConfiguration) findVersion(ref astmodel.PackageReference) *VersionConfiguration { switch r := ref.(type) { case astmodel.DerivedPackageReference: return gc.findVersion(r.Base()) case astmodel.LocalPackageReference: return gc.findVersionForLocalPackageReference(r) } panic(fmt.Sprintf("didn't expect PackageReference of type %T", ref)) } // findVersion uses the provided LocalPackageReference to work out which nested VersionConfiguration should be used func (gc *GroupConfiguration) findVersionForLocalPackageReference(ref astmodel.LocalPackageReference) *VersionConfiguration { gc.advisor.AddTerm(ref.APIVersion()) gc.advisor.AddTerm(ref.PackageName()) // Check based on the ApiVersion alone apiKey := strings.ToLower(ref.APIVersion()) if version, ok := gc.versions[apiKey]; ok { // make sure there's an exact match on the actual version name, so we don't generate a recommendation gc.advisor.AddTerm(version.name) return version } // Also check the entire package name (allows config to specify just a particular generator version if needed) pkgKey := strings.ToLower(ref.PackageName()) if version, ok := gc.versions[pkgKey]; ok { // make sure there's an exact match on the actual version name, so we don't generate a recommendation gc.advisor.AddTerm(version.name) return version } return nil } // UnmarshalYAML populates our instance from the YAML. // The slice node.Content contains pairs of nodes, first one for an ID, then one for the value. func (gc *GroupConfiguration) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return eris.New("expected mapping") } gc.versions = make(map[string]*VersionConfiguration) var lastID string for i, c := range value.Content { // Grab identifiers and loop to handle the associated value if i%2 == 0 { lastID = c.Value continue } // Handle nested version metadata if c.Kind == yaml.MappingNode { v := NewVersionConfiguration(lastID) err := c.Decode(&v) if err != nil { return eris.Wrapf(err, "decoding yaml for %q", lastID) } gc.addVersion(lastID, v) continue } // $payloadType: <string> if strings.EqualFold(lastID, payloadTypeTag) && c.Kind == yaml.ScalarNode { switch strings.ToLower(c.Value) { case string(OmitEmptyProperties): gc.PayloadType.Set(OmitEmptyProperties) case string(ExplicitCollections): gc.PayloadType.Set(ExplicitCollections) case string(ExplicitEmptyCollections): gc.PayloadType.Set(ExplicitEmptyCollections) case string(ExplicitProperties): gc.PayloadType.Set(ExplicitProperties) default: return eris.Errorf("unknown %s value: %s.", payloadTypeTag, c.Value) } continue } // No handler for this value, return an error return eris.Errorf( "group configuration, unexpected yaml value %s: %s (line %d col %d)", lastID, c.Value, c.Line, c.Column) } return nil }