v2/tools/generator/internal/config/object_model_configuration.go (288 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package config
import (
"regexp"
"strings"
"github.com/rotisserie/eris"
"gopkg.in/yaml.v3"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/Azure/azure-service-operator/v2/internal/util/typo"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel"
)
// ObjectModelConfiguration contains additional information about entire object model, allowing fine-tuning of the
// information loaded from JSON schema and Swagger specs. There is a hierarchy of types involved, as follows:
//
// ╔══════════════════════════╗ ┌────────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
// ║ ║ │ │ │ │ │ │ │ │
// ║ ObjectModelConfiguration ║───────│ GroupConfiguration │───────│ VersionConfiguration │───────│ TypeConfiguration │───────│ PropertyConfiguration │
// ║ ║1 1..n│ │1 1..n│ │1 1..n│ │1 1..n│ │
// ╚══════════════════════════╝ └────────────────────┘ └──────────────────────┘ └───────────────────┘ └───────────────────────┘
type ObjectModelConfiguration struct {
groups map[string]*GroupConfiguration // nested configuration for individual groups
typoAdvisor *typo.Advisor
// Group access fields here (alphabetical, please)
PayloadType propertyAccess[PayloadType]
// Type access fields here (alphabetical, please)
AzureGeneratedSecrets typeAccess[[]string]
DefaultAzureName typeAccess[bool]
Export typeAccess[bool]
ExportAs typeAccess[string]
GeneratedConfigs typeAccess[map[string]string]
Importable typeAccess[bool]
IsResource typeAccess[bool]
ManualConfigs typeAccess[[]string]
RenameTo typeAccess[string]
ResourceEmbeddedInParent typeAccess[string]
OperatorSpecProperties typeAccess[[]OperatorSpecPropertyConfiguration]
StripDocumentation typeAccess[bool]
SupportedFrom typeAccess[string]
TypeNameInNextVersion typeAccess[string]
// Property access fields here (alphabetical, please)
Description propertyAccess[string]
ImportConfigMapMode propertyAccess[ImportConfigMapMode]
IsSecret propertyAccess[bool]
PropertyNameInNextVersion propertyAccess[string]
ReferenceType propertyAccess[ReferenceType]
RenamePropertyTo propertyAccess[string]
ResourceLifecycleOwnedByParent propertyAccess[string]
}
// NewObjectModelConfiguration returns a new (empty) ObjectModelConfiguration
func NewObjectModelConfiguration() *ObjectModelConfiguration {
result := &ObjectModelConfiguration{
groups: make(map[string]*GroupConfiguration),
typoAdvisor: typo.NewAdvisor(),
}
// Initialize group access fields here (alphabetical, please)
// Initialize multi-level access fields here (alphabetical, please)
result.PayloadType = makeGroupAccess[PayloadType](
result,
func(c *GroupConfiguration) *configurable[PayloadType] { return &c.PayloadType },
).withTypeOverride(
func(c *TypeConfiguration) *configurable[PayloadType] { return &c.PayloadType },
).withPropertyOverride(
func(c *PropertyConfiguration) *configurable[PayloadType] { return &c.PayloadType },
)
// Initialize type access fields here (alphabetical, please)
result.AzureGeneratedSecrets = makeTypeAccess[[]string](
result, func(c *TypeConfiguration) *configurable[[]string] { return &c.AzureGeneratedSecrets })
result.DefaultAzureName = makeTypeAccess[bool](
result, func(c *TypeConfiguration) *configurable[bool] { return &c.DefaultAzureName })
result.Export = makeTypeAccess[bool](
result, func(c *TypeConfiguration) *configurable[bool] { return &c.Export })
result.ExportAs = makeTypeAccess[string](
result, func(c *TypeConfiguration) *configurable[string] { return &c.ExportAs })
result.GeneratedConfigs = makeTypeAccess[map[string]string](
result, func(c *TypeConfiguration) *configurable[map[string]string] { return &c.GeneratedConfigs })
result.Importable = makeTypeAccess[bool](
result, func(c *TypeConfiguration) *configurable[bool] { return &c.Importable })
result.IsResource = makeTypeAccess[bool](
result, func(c *TypeConfiguration) *configurable[bool] { return &c.IsResource })
result.ManualConfigs = makeTypeAccess[[]string](
result, func(c *TypeConfiguration) *configurable[[]string] { return &c.ManualConfigs })
result.OperatorSpecProperties = makeTypeAccess[[]OperatorSpecPropertyConfiguration](
result, func(c *TypeConfiguration) *configurable[[]OperatorSpecPropertyConfiguration] {
return &c.OperatorSpecProperties
})
result.RenameTo = makeTypeAccess[string](
result, func(c *TypeConfiguration) *configurable[string] { return &c.RenameTo })
result.ResourceEmbeddedInParent = makeTypeAccess[string](
result, func(c *TypeConfiguration) *configurable[string] { return &c.ResourceEmbeddedInParent })
result.StripDocumentation = makeTypeAccess[bool](
result, func(c *TypeConfiguration) *configurable[bool] { return &c.StripDocumentation })
result.SupportedFrom = makeTypeAccess[string](
result, func(c *TypeConfiguration) *configurable[string] { return &c.SupportedFrom })
result.TypeNameInNextVersion = makeTypeAccess[string](
result, func(c *TypeConfiguration) *configurable[string] { return &c.NameInNextVersion })
// Initialize property access fields here (alphabetical, please)
result.Description = makePropertyAccess[string](
result, func(c *PropertyConfiguration) *configurable[string] { return &c.Description })
result.ImportConfigMapMode = makePropertyAccess[ImportConfigMapMode](
result, func(c *PropertyConfiguration) *configurable[ImportConfigMapMode] { return &c.ImportConfigMapMode })
result.IsSecret = makePropertyAccess[bool](
result, func(c *PropertyConfiguration) *configurable[bool] { return &c.IsSecret })
result.PropertyNameInNextVersion = makePropertyAccess[string](
result, func(c *PropertyConfiguration) *configurable[string] { return &c.NameInNextVersion })
result.ReferenceType = makePropertyAccess[ReferenceType](
result, func(c *PropertyConfiguration) *configurable[ReferenceType] { return &c.ReferenceType })
result.RenamePropertyTo = makePropertyAccess[string](
result, func(c *PropertyConfiguration) *configurable[string] { return &c.RenameTo })
result.ResourceLifecycleOwnedByParent = makePropertyAccess[string](
result, func(c *PropertyConfiguration) *configurable[string] { return &c.ResourceLifecycleOwnedByParent })
return result
}
// IsEmpty returns true if we have no configuration at all, false if we have some groups configured.
func (omc *ObjectModelConfiguration) IsEmpty() bool {
return len(omc.groups) == 0
}
// IsGroupConfigured returns true if we have any configuration for the specified group, false otherwise.
func (omc *ObjectModelConfiguration) IsGroupConfigured(pkg astmodel.InternalPackageReference) bool {
var result bool
visitor := newSingleGroupConfigurationVisitor(pkg, func(configuration *GroupConfiguration) error {
result = true
return nil
})
err := visitor.visit(omc)
if err != nil {
// For any error, we'll assume we're expecting the group
return true
}
return result
}
// IsTypeConfigured returns true if we have any configuration for the specified type, false otherwise.
func (omc *ObjectModelConfiguration) IsTypeConfigured(name astmodel.InternalTypeName) bool {
var result bool
visitor := newSingleTypeConfigurationVisitor(name, func(configuration *TypeConfiguration) error {
result = true
return nil
})
err := visitor.visit(omc)
if err != nil {
// For any error, we'll assume we're expecting the type
return true
}
return result
}
// AddTypeAlias adds a type alias for the specified type name,
// allowing configuration related to the type to be accessed via the new name.
func (omc *ObjectModelConfiguration) AddTypeAlias(name astmodel.InternalTypeName, alias string) {
versionVisitor := newSingleVersionConfigurationVisitor(
name.InternalPackageReference(),
func(configuration *VersionConfiguration) error {
return configuration.addTypeAlias(name.Name(), alias)
})
err := versionVisitor.visit(omc)
if err != nil {
// Should never have an error in this case, but if we do make sure we know
panic(err)
}
}
var VersionRegex = regexp.MustCompile(`^v\d\d?$`)
// FindHandCraftedTypeNames returns the set of type-names that are hand-crafted.
// These are identified by having `v<n>` as their version.
func (omc *ObjectModelConfiguration) FindHandCraftedTypeNames(localPath string) (astmodel.InternalTypeNameSet, error) {
result := astmodel.NewInternalTypeNameSet()
var currentGroup string
var currentPackage astmodel.InternalPackageReference
// Collect the names of hand-crafted types
typeVisitor := newEveryTypeConfigurationVisitor(
func(typeConfig *TypeConfiguration) error {
name := astmodel.MakeInternalTypeName(currentPackage, typeConfig.name)
result.Add(name)
return nil
})
// Collect hand-crafted versions as we see them.
// They look like v<n> where n is a small number.
versionVisitor := newEveryVersionConfigurationVisitor(
func(verConfig *VersionConfiguration) error {
if VersionRegex.MatchString(verConfig.name) {
currentPackage = astmodel.MakeLocalPackageReference(
localPath,
currentGroup,
"", // no prefix needed (or wanted!) for v1
verConfig.name)
return verConfig.visitTypes(typeVisitor)
}
return nil
})
// Look inside each group for hand-crafted versions
groupVisitor := newEveryGroupConfigurationVisitor(
func(groupConfig *GroupConfiguration) error {
currentGroup = groupConfig.name
return groupConfig.visitVersions(versionVisitor)
})
err := groupVisitor.visit(omc)
if err != nil {
return nil, eris.Wrapf(err, "failed to find hand-crafted packages")
}
return result, nil
}
// addGroup includes the provided GroupConfiguration in this model configuration
func (omc *ObjectModelConfiguration) addGroup(name string, group *GroupConfiguration) {
if omc.groups == nil {
// Initialize the map just-in-time
omc.groups = make(map[string]*GroupConfiguration)
}
// store the group name using lowercase,
// so we can do case-insensitive lookups later
omc.groups[strings.ToLower(name)] = group
}
// visitGroup invokes the provided visitor on the specified group if present.
// Returns a NotConfiguredError if the group is not found; otherwise whatever error is returned by the visitor.
func (omc *ObjectModelConfiguration) visitGroup(
ref astmodel.InternalPackageReference,
visitor *configurationVisitor,
) error {
group := omc.findGroup(ref)
if group == nil {
return nil
}
return visitor.visitGroup(group)
}
// visitGroups invokes the provided visitor on all nested groups.
func (omc *ObjectModelConfiguration) visitGroups(visitor *configurationVisitor) error {
errs := make([]error, 0, len(omc.groups))
for _, gc := range omc.groups {
err := visitor.visitGroup(gc)
err = omc.typoAdvisor.Wrapf(err, gc.name, "group %s not seen", gc.name)
errs = append(errs, err)
}
// kerrors.NewAggregate() returns nil if nothing went wrong
return kerrors.NewAggregate(errs)
}
// findGroup uses the provided TypeName to work out which nested GroupConfiguration should be used
func (omc *ObjectModelConfiguration) findGroup(ref astmodel.InternalPackageReference) *GroupConfiguration {
group := ref.Group()
if omc == nil || omc.groups == nil {
return nil
}
omc.typoAdvisor.AddTerm(group)
if g, ok := omc.groups[group]; ok {
return g
}
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 (omc *ObjectModelConfiguration) UnmarshalYAML(value *yaml.Node) error {
if value.Kind != yaml.MappingNode {
return eris.New("expected mapping")
}
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 name metadata
if c.Kind == yaml.MappingNode && lastID != "" {
g := NewGroupConfiguration(lastID)
err := c.Decode(&g)
if err != nil {
return eris.Wrapf(err, "decoding yaml for %q", lastID)
}
omc.addGroup(lastID, g)
continue
}
// No handler for this value, return an error
return eris.Errorf(
"object model configuration, unexpected yaml value %s: %s (line %d col %d)", lastID, c.Value, c.Line, c.Column)
}
return nil
}
// ModifyGroup allows the configuration of a specific group to be modified.
// If configuration for that group doesn't exist, it will be created.
// While intended for test use, this isn't in a _test.go file as we want to use it from tests in multiple packages.
func (omc *ObjectModelConfiguration) ModifyGroup(
ref astmodel.InternalPackageReference,
action func(configuration *GroupConfiguration) error,
) error {
groupName := ref.Group()
grp := omc.findGroup(ref)
if grp == nil {
grp = NewGroupConfiguration(groupName)
omc.addGroup(groupName, grp)
}
return action(grp)
}
// ModifyVersion allows the configuration of a specific version to be modified.
// If configuration for that version doesn't exist, it will be created.
// While intended for test use, this isn't in a _test.go file as we want to use it from tests in multiple packages.
func (omc *ObjectModelConfiguration) ModifyVersion(
ref astmodel.InternalPackageReference,
action func(configuration *VersionConfiguration) error,
) error {
_, version := ref.GroupVersion()
return omc.ModifyGroup(
ref,
func(configuration *GroupConfiguration) error {
ver := configuration.findVersion(ref)
if ver == nil {
ver = NewVersionConfiguration(version)
configuration.addVersion(version, ver)
}
return action(ver)
})
}
// ModifyType allows the configuration of a specific type to be modified.
// If configuration for that type doesn't exist, it will be created.
// While intended for test use, this isn't in a _test.go file as we want to use it from tests in multiple packages.
func (omc *ObjectModelConfiguration) ModifyType(
name astmodel.InternalTypeName,
action func(typeConfiguration *TypeConfiguration) error,
) error {
return omc.ModifyVersion(
name.InternalPackageReference(),
func(versionConfiguration *VersionConfiguration) error {
typeName := name.Name()
typ := versionConfiguration.findType(typeName)
if typ == nil {
typ = NewTypeConfiguration(typeName)
versionConfiguration.addType(typeName, typ)
}
return action(typ)
})
}
// ModifyProperty allows the configuration of a specific property to be modified.
// If configuration for that property doesn't exist, it will be created.
// While intended for test use, this isn't in a _test.go file as we want to use it from tests in multiple packages.
func (omc *ObjectModelConfiguration) ModifyProperty(
typeName astmodel.InternalTypeName,
property astmodel.PropertyName,
action func(propertyConfiguration *PropertyConfiguration) error,
) error {
return omc.ModifyType(
typeName,
func(typeConfiguration *TypeConfiguration) error {
prop := typeConfiguration.findProperty(property)
if prop == nil {
name := property.String()
prop = NewPropertyConfiguration(name)
typeConfiguration.addProperty(name, prop)
}
return action(prop)
})
}