v2/tools/generator/internal/jsonast/jsonast.go (665 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package jsonast
import (
"context"
"fmt"
"math"
"math/big"
"net/url"
"regexp"
"strings"
"github.com/devigned/tab"
"github.com/go-logr/logr"
"github.com/rotisserie/eris"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"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/config"
)
type (
// TypeHandler is a standard delegate used for walking the schema tree.
// Note that it is permissible for a TypeHandler to return `nil, nil`, which indicates that
// there is no type to be included in the output.
TypeHandler func(ctx context.Context, scanner *SchemaScanner, schema Schema, log logr.Logger) (astmodel.Type, error)
// UnknownSchemaError is used when we find a JSON schema node that we don't know how to handle
UnknownSchemaError struct {
Schema Schema
Filters []string
}
// A SchemaScanner is used to scan a JSON Schema extracting and collecting type definitions
SchemaScanner struct {
definitions map[astmodel.TypeName]*astmodel.TypeDefinition
TypeHandlers map[SchemaType]TypeHandler
configuration *config.Configuration
idFactory astmodel.IdentifierFactory
log logr.Logger
}
)
// findTypeDefinition looks to see if we have seen the specified definition before, returning its definition if we have.
func (scanner *SchemaScanner) findTypeDefinition(name astmodel.TypeName) (*astmodel.TypeDefinition, bool) {
result, ok := scanner.definitions[name]
return result, ok
}
// addTypeDefinition adds a type definition to emit later
func (scanner *SchemaScanner) addTypeDefinition(def astmodel.TypeDefinition) {
if existing, ok := scanner.definitions[def.Name()]; ok && existing != nil {
panic(fmt.Sprintf("overwriting existing definition for %s", def.Name()))
}
scanner.definitions[def.Name()] = &def
}
// addEmptyTypeDefinition adds a placeholder definition; it should always be replaced later
func (scanner *SchemaScanner) addEmptyTypeDefinition(name astmodel.TypeName) {
scanner.definitions[name] = nil
}
// removeTypeDefinition removes a type definition
func (scanner *SchemaScanner) removeTypeDefinition(name astmodel.TypeName) {
delete(scanner.definitions, name)
}
func (use *UnknownSchemaError) Error() string {
if use.Schema == nil || use.Schema.url() == nil {
return "unable to determine schema type for nil schema or one without a URL"
}
return fmt.Sprintf("unable to determine the schema type for %s", use.Schema.url().String())
}
// NewSchemaScanner constructs a new scanner, ready for use
func NewSchemaScanner(
idFactory astmodel.IdentifierFactory,
configuration *config.Configuration,
log logr.Logger,
) *SchemaScanner {
return &SchemaScanner{
definitions: make(map[astmodel.TypeName]*astmodel.TypeDefinition),
TypeHandlers: defaultTypeHandlers(),
configuration: configuration,
idFactory: idFactory,
log: log,
}
}
// AddTypeHandler will override a default type handler for a given SchemaType. This allows for a consumer to customize
// AST generation.
func (scanner *SchemaScanner) AddTypeHandler(schemaType SchemaType, handler TypeHandler) {
scanner.TypeHandlers[schemaType] = handler
}
// RunHandler triggers the appropriate handler for the specified schemaType
func (scanner *SchemaScanner) RunHandler(ctx context.Context, schemaType SchemaType, schema Schema) (astmodel.Type, error) {
if ctx.Err() != nil { // check for cancellation
return nil, ctx.Err()
}
handler := scanner.TypeHandlers[schemaType]
return handler(ctx, scanner, schema, scanner.log)
}
// RunHandlerForSchema inspects the passed schema to identify what kind it is, then runs the appropriate handler
func (scanner *SchemaScanner) RunHandlerForSchema(ctx context.Context, schema Schema) (astmodel.Type, error) {
schemaType, err := getSubSchemaType(schema)
if err != nil {
return nil, err
}
return scanner.RunHandler(ctx, schemaType, schema)
}
// RunHandlersForSchemas inspects each passed schema and runs the appropriate handler
func (scanner *SchemaScanner) RunHandlersForSchemas(ctx context.Context, schemas []Schema) ([]astmodel.Type, error) {
var results []astmodel.Type
var errs []error
for _, schema := range schemas {
t, err := scanner.RunHandlerForSchema(ctx, schema)
if err != nil {
var unknownSchema *UnknownSchemaError
if eris.As(err, &unknownSchema) {
if unknownSchema.Schema.description() != nil {
// some Swagger types (e.g. ServiceFabric Cluster) use allOf with a description-only schema
scanner.log.V(2).Info(
"skipping description-only schema type",
"schema", unknownSchema.Schema.url(),
"description", *unknownSchema.Schema.description())
continue
}
}
errs = append(errs, eris.Wrapf(err, "unable to handle schema %s", schema.ID()))
}
if t != nil {
results = append(results, t)
}
}
err := kerrors.NewAggregate(errs)
if err != nil {
return nil, err
}
return results, nil
}
func (scanner *SchemaScanner) GenerateAllDefinitions(ctx context.Context, schema Schema) (astmodel.TypeDefinitionSet, error) {
title := schema.title()
if title == nil {
return nil, eris.New("given schema has no title")
}
rootName := *title
rootURL := schema.url()
rootGroup, err := groupOf(rootURL)
if err != nil {
return nil, eris.Wrapf(err, "unable to extract group for schema")
}
rootVersion := versionOf(rootURL)
rootPackage := scanner.configuration.MakeLocalPackageReference(
scanner.idFactory.CreateGroupName(rootGroup),
rootVersion)
rootTypeName := astmodel.MakeInternalTypeName(rootPackage, rootName)
_, err = generateDefinitionsFor(ctx, scanner, rootTypeName, schema)
if err != nil {
return nil, err
}
return scanner.Definitions(), nil
}
// Definitions produces a set of all the types defined so far
func (scanner *SchemaScanner) Definitions() astmodel.TypeDefinitionSet {
defs := make(astmodel.TypeDefinitionSet)
for defName, def := range scanner.definitions {
if def == nil {
// safety check/assert:
panic(fmt.Sprintf("%s was nil", defName))
}
if defName != def.Name() {
// this indicates a serious programming error
panic(fmt.Sprintf("mismatched typenames: %s != %s", defName, def.Name()))
}
defs.Add(*def)
}
return defs
}
// defaultTypeHandlers will create a default map of JSONType to AST transformers
func defaultTypeHandlers() map[SchemaType]TypeHandler {
return map[SchemaType]TypeHandler{
Array: arrayHandler,
OneOf: oneOfHandler,
AnyOf: anyOfHandler,
AllOf: allOfHandler,
Ref: refHandler,
Object: objectHandler,
Enum: enumHandler,
String: stringHandler,
Int: intHandler,
Number: numberHandler,
Bool: boolHandler,
}
}
func stringHandler(_ context.Context, _ *SchemaScanner, schema Schema, log logr.Logger) (astmodel.Type, error) {
t := astmodel.StringType
maxLength := schema.maxLength()
minLength := schema.minLength()
pattern := schema.pattern()
format := schema.format()
if maxLength != nil || minLength != nil || pattern != nil || format != "" {
patterns := make([]*regexp.Regexp, 0, 2)
if pattern != nil {
patterns = append(patterns, pattern)
}
if format != "" {
formatPattern := formatToPattern(format, log)
if formatPattern != nil {
patterns = append(patterns, formatPattern)
}
}
if format == "arm-id" {
t = astmodel.ARMIDType
}
// If there are no validations except format, and that format didn't result in any patterns,
// then there's no need for a validated type so just return t
if len(patterns) == 0 && maxLength == nil && minLength == nil {
return t, nil
}
validations := astmodel.StringValidations{
MaxLength: maxLength,
MinLength: minLength,
Patterns: patterns,
}
return astmodel.NewValidatedType(t, validations), nil
}
return t, nil
}
// copied from ARM implementation
var uuidRegex = regexp.MustCompile("^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$")
func formatToPattern(format string, log logr.Logger) *regexp.Regexp {
switch format {
case "uuid":
return uuidRegex
case "date-time", "date", "duration", "date-time-rfc1123", "arm-id":
// TODO: don’t bother validating for now
return nil
case "password":
// This is handled later in the status_augment phase of processing, so just
// ignore it for now
return nil
default:
log.V(1).Info(
"Unknown property format",
"format", format)
return nil
}
}
func numberHandler(_ context.Context, _ *SchemaScanner, schema Schema, _ logr.Logger) (astmodel.Type, error) {
t := astmodel.FloatType
v := getNumberValidations(schema)
if v != nil {
// for controller-gen anything with min/max/multipleof must be based on int
// double-check that all of these are integral
var errs []string
if v.Minimum != nil {
if !v.Minimum.IsInt() {
errs = append(errs, "'minimum' validation must be an integer")
}
}
if v.Maximum != nil {
if !v.Maximum.IsInt() {
errs = append(errs, "'maximum' validation must be an integer")
}
}
if v.MultipleOf != nil {
if !v.MultipleOf.IsInt() {
errs = append(errs, "'multipleOf' validation must be an integer")
}
}
result := astmodel.NewValidatedType(t, *v)
if len(errs) > 0 {
return astmodel.NewErroredType(result, errs, nil), nil
}
// we have checked they are all integers:
return result, nil
}
return t, nil
}
var (
zero *big.Rat = big.NewRat(0, 1)
maxUint32 *big.Rat = big.NewRat(1, 1).SetUint64(math.MaxUint32)
)
func intHandler(_ context.Context, _ *SchemaScanner, schema Schema, _ logr.Logger) (astmodel.Type, error) {
t := astmodel.IntType
v := getNumberValidations(schema)
if v != nil {
// special-case some things to return different types
if !v.ExclusiveMaximum && v.Maximum != nil &&
v.MultipleOf == nil &&
!v.ExclusiveMinimum && v.Minimum != nil && v.Minimum.Cmp(zero) == 0 {
if v.Maximum.Cmp(maxUint32) == 0 {
return astmodel.UInt32Type, nil
}
}
return astmodel.NewValidatedType(t, *v), nil
}
return t, nil
}
func getNumberValidations(schema Schema) *astmodel.NumberValidations {
minValue := schema.minValue()
minExclusive := schema.minValueExclusive()
maxValue := schema.maxValue()
maxExclusive := schema.maxValueExclusive()
multipleOf := schema.multipleOf()
if minValue != nil || maxValue != nil || multipleOf != nil {
return &astmodel.NumberValidations{
Maximum: maxValue,
Minimum: minValue,
ExclusiveMaximum: maxExclusive,
ExclusiveMinimum: minExclusive,
MultipleOf: multipleOf,
}
}
return nil
}
func boolHandler(_ context.Context, _ *SchemaScanner, _ Schema, _ logr.Logger) (astmodel.Type, error) {
return astmodel.BoolType, nil
}
func enumHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, _ logr.Logger) (astmodel.Type, error) {
_, span := tab.StartSpan(ctx, "enumHandler")
defer span.End()
// Default to a string base type
baseType := astmodel.StringType
for _, t := range []SchemaType{Bool, Int, Number, String} {
if schema.hasType(t) {
bt, err := GetPrimitiveType(t)
if err != nil {
return nil, err
}
baseType = bt
}
}
enumValues := schema.enumValues()
values := make([]astmodel.EnumValue, 0, len(enumValues))
for _, v := range enumValues {
vTrimmed := strings.Trim(v, "\"")
// Some specs include boolean (or float, int) enums with quotes around the literals.
// Trim quotes from anything that's not a string
if baseType != astmodel.StringType {
v = vTrimmed
}
// TODO: This is a bit of a hack as we don't have a way to handle this generically right now
// TODO: for an arbitrary non-renderable character
// use vTrimmed as seed for identifier as it doesn't have quotes surrounding it
id := scanner.idFactory.CreateIdentifier(vTrimmed, astmodel.Exported)
values = append(values, astmodel.MakeEnumValue(id, v))
}
enumType := astmodel.NewEnumType(baseType, values...)
return enumType, nil
}
func objectHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, log logr.Logger) (astmodel.Type, error) {
ctx, span := tab.StartSpan(ctx, "objectHandler")
defer span.End()
properties, err := getProperties(ctx, scanner, schema, log)
if err != nil {
return nil, err
}
// if we _only_ have an 'additionalProperties' property, then we are making
// a dictionary-like type, and we won't generate an object type; instead, we
// will just use the 'additionalProperties' type directly
if len(properties) == 1 && properties[0].PropertyName() == astmodel.AdditionalPropertiesPropertyName {
return properties[0].PropertyType(), nil
}
isResource := schema.extensionAsBool("x-ms-azure-resource")
// If we're a resource, our 'Id' property needs to have a special type
if isResource {
for i, prop := range properties {
if prop.HasName("Id") || prop.HasName("ID") {
properties[i] = prop.WithType(astmodel.NewOptionalType(astmodel.ARMIDType))
}
}
}
objectType := astmodel.NewObjectType().WithProperties(properties...).WithIsResource(isResource)
return objectType, nil
}
func generatePropertyDefinition(ctx context.Context, scanner *SchemaScanner, rawPropName string, prop Schema) (*astmodel.PropertyDefinition, error) {
propertyName := scanner.idFactory.CreatePropertyName(rawPropName, astmodel.Exported)
schemaType, err := getSubSchemaType(prop)
var use *UnknownSchemaError
if eris.As(err, &use) {
// if we don't know the type, we still need to provide the property, we will just provide open interface
property := astmodel.NewPropertyDefinition(propertyName, rawPropName, astmodel.AnyType)
return property, nil
}
if err != nil {
return nil, err
}
propType, err := scanner.RunHandler(ctx, schemaType, prop)
if eris.As(err, &use) {
// if we don't know the type, we still need to provide the property, we will just provide open interface
property := astmodel.NewPropertyDefinition(propertyName, rawPropName, astmodel.AnyType)
return property, nil
}
if err != nil {
return nil, err
}
// This can happen if the property type was pruned away by a type filter.
if propType == nil {
// returning nil here is a signal to the caller that this property cannot be constructed.
return nil, nil
}
property := astmodel.NewPropertyDefinition(propertyName, rawPropName, propType).WithReadOnly(prop.readOnly())
return property, nil
}
func getProperties(
ctx context.Context,
scanner *SchemaScanner,
schema Schema,
log logr.Logger,
) ([]*astmodel.PropertyDefinition, error) {
ctx, span := tab.StartSpan(ctx, "getProperties")
defer span.End()
props := schema.properties()
properties := make([]*astmodel.PropertyDefinition, 0, len(props))
for propName, propSchema := range props {
property, err := generatePropertyDefinition(ctx, scanner, propName, propSchema)
if err != nil {
return nil, err
}
// This can happen if the property type was pruned away by a type filter.
// There are a few options here: We can skip this property entirely, we can emit it
// with no type (won't compile), or we can emit with with interface{}.
// Currently emitting a warning and skipping
if property == nil {
// TODO: This log shouldn't happen in cases where the type in question is later excluded, see:
// TODO: https://github.com/Azure/azure-service-operator/issues/1517
log.V(2).Info(
"Property omitted due to nil propType (probably due to type filter)",
"property", propName)
continue
}
// add documentation
if propSchema.description() != nil {
property = property.WithDescription(*propSchema.description())
}
// add flattening
property = property.SetFlatten(propSchema.extensionAsBool("x-ms-client-flatten"))
// add secret flag
hasSecretExtension := propSchema.extensionAsBool("x-ms-secret")
hasFormatPassword := propSchema.format() == "password"
if hasSecretExtension || hasFormatPassword {
property = property.WithIsSecret(true)
}
// add validations
isRequired := false
for _, required := range schema.requiredProperties() {
if propName == required {
isRequired = true
break
}
}
// All types are optional (regardless of if the property is required or not) because of
// non-optional types (int, string, MyType, etc) interaction with omitempty.
// If a field is json:omitempty but its type is not optional (not a ptr) then the default value
// for that type will be omitted from the JSON payload. For example 0 would be omitted for ints.
// On the other hand if a field is NOT json:omitempty then the type is always serialized in the payload
// which causes issues for kubebuilder:validation:Required (how can we tell the user didn't specify that value?)
// See https://github.com/Azure/azure-service-operator/issues/1999 for more details.
property = property.MakeTypeOptional()
if isRequired {
property = property.MakeRequired()
} else {
property = property.MakeOptional()
}
properties = append(properties, property)
}
// see: https://json-schema.org/understanding-json-schema/reference/object.html#properties
if schema.additionalPropertiesAllowed() {
additionalPropSchema := schema.additionalPropertiesSchema()
if additionalPropSchema == nil {
// if not specified, any additional properties are allowed
// (TODO: tell all Azure teams this fact and get them to update their API definitions!)
// for now we aren't following the spec 100% as it pollutes the generated code
// only generate this field if there are no other fields:
// Note: we use props here rather than properties as ref types may be pruned
// and that can result in a type that looks like it had no properties (so classified as a map below)
// when in fact it had properties, just they were pruned. This usually happens when
// the whole type is going to be pruned.
if len(props) == 0 {
// TODO: for JSON serialization this needs to be unpacked into "parent"
additionalProperties := astmodel.NewPropertyDefinition(
astmodel.AdditionalPropertiesPropertyName,
astmodel.AdditionalPropertiesJSONName,
astmodel.NewStringMapType(astmodel.AnyType))
properties = append(properties, additionalProperties)
}
} else {
// otherwise, it is a type for all additional fields
// TODO: for JSON serialization this needs to be unpacked into "parent"
additionalPropsType, err := scanner.RunHandlerForSchema(ctx, additionalPropSchema)
if err != nil {
// If the error is an UnknownSchemaError AND we have properties already, we skip generating
// the additional properties. As mentioned above, this isn't 100% following the spec, but
// it seems to do the right thing
var use *UnknownSchemaError
if eris.As(err, &use) && len(properties) > 0 {
return properties, nil
}
return nil, err
}
// This can happen if the property type was pruned away by a type filter.
// There are a few options here: We can skip this property entirely, we can emit it
// with no type (won't compile), or we can emit with with interface{}.
// TODO: Currently setting this to anyType as that's easiest to deal with and will generate
// TODO: a warning during controller-gen
if additionalPropsType == nil {
additionalPropsType = astmodel.AnyType
}
additionalProperties := astmodel.NewPropertyDefinition(
astmodel.AdditionalPropertiesPropertyName,
astmodel.AdditionalPropertiesJSONName,
astmodel.NewStringMapType(additionalPropsType))
properties = append(properties, additionalProperties)
}
}
return properties, nil
}
func refHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, log logr.Logger) (astmodel.Type, error) {
ctx, span := tab.StartSpan(ctx, "refHandler")
defer span.End()
// Allow for inclusions of other full-fledged files, such as the autogenerated list in the 2019-04-01
// deploymentTemplate.json
refSchema := schema.refSchema()
// TODO: How hacky is this?
if refSchema.url().Host != "" && refSchema.url().Fragment == "" {
return scanner.RunHandlerForSchema(ctx, refSchema)
}
typeName, err := schema.refTypeName()
if err != nil {
return nil, err
}
// Prune the graph according to the configuration
shouldPrune, because := scanner.configuration.ShouldPrune(typeName)
if shouldPrune == config.Prune {
log.V(2).Info(
"Skipping type",
"type", typeName,
"because", because)
return nil, nil // Skip entirely
}
return generateDefinitionsFor(ctx, scanner, typeName, schema.refSchema())
}
func generateDefinitionsFor(
ctx context.Context,
scanner *SchemaScanner,
typeName astmodel.InternalTypeName,
schema Schema,
) (astmodel.TypeName, error) {
schemaType, err := getSubSchemaType(schema)
if err != nil {
return nil, err
}
schemaURL := schema.url()
// see if we already generated something for this ref
if _, ok := scanner.findTypeDefinition(typeName); ok {
return typeName, nil
}
// Add a placeholder to avoid recursive calls
// we will overwrite this later (this is checked below)
scanner.addEmptyTypeDefinition(typeName)
result, err := scanner.RunHandler(ctx, schemaType, schema)
if err != nil {
scanner.removeTypeDefinition(typeName) // we weren't able to generate it, remove placeholder
return nil, err
}
// TODO: This code and below does nothing in the Swagger path as schema.url() is always empty.
// TODO: It's still used in the JSON schema path for golden tests and should be removed once those
// TODO: are retired.
resourceType := categorizeResourceType(schemaURL)
if resourceType != nil {
result = astmodel.NewAzureResourceType(result, nil, typeName, *resourceType, nil)
}
definition := astmodel.MakeTypeDefinition(typeName, result)
// Add a description of the type
var description []string
if desc := schema.description(); desc != nil {
description = astbuilder.WordWrap(*desc, 120)
}
// Add URL reference if we have one
if schema.url().String() != "" {
description = append(description, fmt.Sprintf("Generated from: %s", schema.url().String()))
}
if len(description) > 0 {
definition = definition.WithDescription(description...)
}
scanner.addTypeDefinition(definition)
if def, ok := scanner.findTypeDefinition(typeName); !ok || def == nil {
// safety check in case of breaking changes
panic(fmt.Sprintf("didn't set type definition for %s", typeName))
}
return definition.Name(), nil
}
func allOfHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, _ logr.Logger) (astmodel.Type, error) {
ctx, span := tab.StartSpan(ctx, "allOfHandler")
defer span.End()
types, err := scanner.RunHandlersForSchemas(ctx, schema.allOf())
if err != nil {
return nil, err
}
// if the node that contains the allOf defines other properties, create an object type with them inside to merge
if len(schema.properties()) > 0 {
objectType, err := scanner.RunHandler(ctx, Object, schema)
if err != nil {
return nil, err
}
types = append(types, objectType)
}
return astmodel.BuildAllOfType(types...), nil
}
func oneOfHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, _ logr.Logger) (astmodel.Type, error) {
ctx, span := tab.StartSpan(ctx, "oneOfHandler")
defer span.End()
result := astmodel.NewOneOfType(schema.ID())
// Capture the discriminator property name, if we have one
if schema.discriminator() != "" {
result = result.WithDiscriminatorProperty(schema.discriminator())
}
// Capture the discriminator value, if we have one
if discriminatorValue, ok := schema.extensionAsString("x-ms-discriminator-value"); ok {
result = result.WithDiscriminatorValue(discriminatorValue)
}
// Handle any nested OneOf options
// These will each be either a TypeName or an Object
types, err := scanner.RunHandlersForSchemas(ctx, schema.oneOf())
if err != nil {
return nil, eris.Wrapf(err, "unable to generate oneOf types for %s", schema.ID())
}
result = result.WithTypes(types)
// Also need to pick up any nested AllOf options
// If an object, these are properties that are required for this option
// Otherwise, add as an option
allOfTypes, err := scanner.RunHandlersForSchemas(ctx, schema.allOf())
if err != nil {
return nil, eris.Wrapf(err, "unable to generate allOf types for %s", schema.ID())
}
// Our AllOf contains either properties to add to our OneOf, or references to parent types
for _, t := range allOfTypes {
if obj, ok := asCommonProperties(t, scanner.definitions); ok {
// If we have an object, add its properties
result = result.WithAdditionalPropertyObject(obj)
continue
}
// Treat it as an option
result = result.WithType(t)
}
// If there are any properties, we need to create an object type to wrap them
if len(schema.properties()) > 0 {
t, err := scanner.RunHandler(ctx, Object, schema)
if err != nil {
return nil, eris.Wrapf(err, "unable to generate object for properties of %s", schema.ID())
}
obj, ok := t.(*astmodel.ObjectType)
if !ok {
return nil, eris.Errorf(
"expected object type for properties of %s, got %T", schema.ID(), t)
}
result = result.WithAdditionalPropertyObject(obj)
}
return result, nil
}
// attempts to resolve the passed type to an object type that represents properties
func asCommonProperties(
t astmodel.Type,
defs map[astmodel.TypeName]*astmodel.TypeDefinition,
) (*astmodel.ObjectType, bool) {
if obj, isObject := astmodel.AsObjectType(t); isObject {
return obj, true
}
if tn, isTypeName := astmodel.AsTypeName(t); isTypeName {
if def, found := defs[tn]; found {
return asCommonProperties(def.Type(), defs)
}
}
return nil, false
}
func anyOfHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, log logr.Logger) (astmodel.Type, error) {
ctx, span := tab.StartSpan(ctx, "anyOfHandler")
defer span.End()
// See https://github.com/Azure/azure-service-operator/issues/1518 for details about why this is treated as oneOf
log.V(1).Info(
"Handling anyOf type as if it were oneOf",
"url", schema.url())
return oneOfHandler(ctx, scanner, schema, log)
}
func arrayHandler(ctx context.Context, scanner *SchemaScanner, schema Schema, log logr.Logger) (astmodel.Type, error) {
ctx, span := tab.StartSpan(ctx, "arrayHandler")
defer span.End()
items := schema.items()
if len(items) > 1 {
return nil, eris.Errorf("item contains more children than expected: %s", schema.items())
}
if len(items) == 0 {
// there is no type to the elements, so we must assume interface{}
log.V(1).Info(
"Interface assumption unproven",
"url", schema.url())
result := astmodel.NewArrayType(astmodel.AnyType)
return withArrayValidations(schema, result), nil
}
// get the only child type and wrap it up as an array type:
onlyChild := items[0]
astType, err := scanner.RunHandlerForSchema(ctx, onlyChild)
if err != nil {
return nil, err
}
// astType can be nil if it was pruned from the tree
if astType == nil {
return nil, nil
}
result := astmodel.NewArrayType(astType)
return withArrayValidations(schema, result), nil
}
func withArrayValidations(schema Schema, t *astmodel.ArrayType) astmodel.Type {
maxItems := schema.maxItems()
minItems := schema.minItems()
if maxItems != nil || minItems != nil {
return astmodel.NewValidatedType(t, astmodel.ArrayValidations{
MaxItems: maxItems,
MinItems: minItems,
})
}
return t
}
func getSubSchemaType(schema Schema) (SchemaType, error) {
// handle special nodes:
switch {
case len(schema.enumValues()) > 0: // this should come before the primitive checks below
return Enum, nil
// Handle the three cases of oneOf: complete/root/leaf
case schema.hasOneOf() || schema.discriminator() != "" || schema.hasExtension("x-ms-discriminator-value"):
return OneOf, nil
case schema.hasAllOf():
return AllOf, nil
case schema.hasAnyOf():
return AnyOf, nil
case schema.isRef():
return Ref, nil
}
for _, t := range []SchemaType{Object, String, Number, Int, Bool, Array} {
if schema.hasType(t) {
return t, nil
}
}
// TODO: this whole switch is a bit wrong because type: 'object' can
// be combined with OneOf/AnyOf/etc. still, it works okay for now...
if len(schema.properties()) > 0 || schema.additionalPropertiesSchema() != nil {
// haven't figured out a type but it has properties, treat it as an object
return Object, nil
}
return Unknown, &UnknownSchemaError{Schema: schema}
}
// GetPrimitiveType returns the primtive type for this schema type
func GetPrimitiveType(name SchemaType) (*astmodel.PrimitiveType, error) {
switch name {
case String:
return astmodel.StringType, nil
case Int:
return astmodel.IntType, nil
case Number:
return astmodel.FloatType, nil
case Bool:
return astmodel.BoolType, nil
case AllOf:
case AnyOf:
case Array:
case Enum:
case Object:
case OneOf:
case Ref:
case Unknown:
return astmodel.AnyType, eris.Errorf("%s is not a simple type and no ast.NewIdent can be created", name)
}
panic(fmt.Sprintf("unhandled case in getPrimitiveType: %s", name)) // this is also checked by linter
}
// categorizeResourceType determines if this URL represents an ARM resource or not.
// If the URL represents a resource, a non-nil value is returned. If the URL does not represent
// a resource, nil is returned.
func categorizeResourceType(url *url.URL) *astmodel.ResourceScope {
fragmentParts := strings.FieldsFunc(url.Fragment, isURLPathSeparator)
resourceGroup := astmodel.ResourceScopeResourceGroup
extension := astmodel.ResourceScopeExtension
tenant := astmodel.ResourceScopeTenant
for _, fragmentPart := range fragmentParts {
// resourceDefinitions are "normal" resources
if fragmentPart == "resourceDefinitions" ||
// Treat all resourceBase things as resources so that "resourceness"
// is inherited:
strings.Contains(strings.ToLower(fragmentPart), "resourcebase") {
return &resourceGroup
}
if fragmentPart == "tenant_resourceDefinitions" {
return &tenant
}
// unknown_ResourceDefinitions or extension_resourceDefinitions are extension resources, see
// https://github.com/Azure/azure-resource-manager-schemas/blob/069dc7cbff0725aea3a3595e4bb777da966dbb6f/generator/generate.ts#L186
// to learn more.
if fragmentPart == "unknown_resourceDefinitions" ||
fragmentPart == "extension_resourceDefinitions" {
return &extension
}
}
return nil
}