// Copyright 2017 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "log" "sort" "strings" "github.com/googleapis/gnostic/jsonschema" ) // Domain models a collection of types that is defined by a schema. type Domain struct { TypeModels map[string]*TypeModel // models of the types in the domain Prefix string // type prefix to use Schema *jsonschema.Schema // top-level schema TypeNameOverrides map[string]string // a configured mapping from patterns to type names PropertyNameOverrides map[string]string // a configured mapping from patterns to property names ObjectTypeRequests map[string]*TypeRequest // anonymous types implied by type instantiation MapTypeRequests map[string]string // "NamedObject" types that will be used to implement ordered maps Version string // OpenAPI Version ("v2" or "v3") } // NewDomain creates a domain representation. func NewDomain(schema *jsonschema.Schema, version string) *Domain { cc := &Domain{} cc.TypeModels = make(map[string]*TypeModel, 0) cc.TypeNameOverrides = make(map[string]string, 0) cc.PropertyNameOverrides = make(map[string]string, 0) cc.ObjectTypeRequests = make(map[string]*TypeRequest, 0) cc.MapTypeRequests = make(map[string]string, 0) cc.Schema = schema cc.Version = version return cc } // TypeNameForStub returns a capitalized name to use for a generated type. func (domain *Domain) TypeNameForStub(stub string) string { return domain.Prefix + strings.ToUpper(stub[0:1]) + stub[1:len(stub)] } // typeNameForReference returns a capitalized name to use for a generated type based on a JSON reference func (domain *Domain) typeNameForReference(reference string) string { parts := strings.Split(reference, "/") first := parts[0] last := parts[len(parts)-1] if first == "#" { return domain.TypeNameForStub(last) } return "Schema" } // propertyNameForReference returns a property name to use for a JSON reference func (domain *Domain) propertyNameForReference(reference string) *string { parts := strings.Split(reference, "/") first := parts[0] last := parts[len(parts)-1] if first == "#" { return &last } return nil } // arrayItemTypeForSchema determines the item type for arrays defined by a schema func (domain *Domain) arrayItemTypeForSchema(propertyName string, schema *jsonschema.Schema) string { // default itemTypeName := "Any" if schema.Items != nil { if schema.Items.SchemaArray != nil { if len(*(schema.Items.SchemaArray)) > 0 { ref := (*schema.Items.SchemaArray)[0].Ref if ref != nil { itemTypeName = domain.typeNameForReference(*ref) } else { types := (*schema.Items.SchemaArray)[0].Type if types == nil { // do nothing } else if (types.StringArray != nil) && len(*(types.StringArray)) == 1 { itemTypeName = (*types.StringArray)[0] } else if (types.StringArray != nil) && len(*(types.StringArray)) > 1 { itemTypeName = fmt.Sprintf("%+v", types.StringArray) } else if types.String != nil { itemTypeName = *(types.String) } else { itemTypeName = "UNKNOWN" } } } } else if schema.Items.Schema != nil { types := schema.Items.Schema.Type if schema.Items.Schema.Ref != nil { itemTypeName = domain.typeNameForReference(*schema.Items.Schema.Ref) } else if schema.Items.Schema.OneOf != nil { // this type is implied by the "oneOf" itemTypeName = domain.TypeNameForStub(propertyName + "Item") domain.ObjectTypeRequests[itemTypeName] = NewTypeRequest(itemTypeName, propertyName, schema.Items.Schema) } else if types == nil { // do nothing } else if (types.StringArray != nil) && len(*(types.StringArray)) == 1 { itemTypeName = (*types.StringArray)[0] } else if (types.StringArray != nil) && len(*(types.StringArray)) > 1 { itemTypeName = fmt.Sprintf("%+v", types.StringArray) } else if types.String != nil { itemTypeName = *(types.String) } else { itemTypeName = "UNKNOWN" } } } return itemTypeName } func (domain *Domain) buildTypeProperties(typeModel *TypeModel, schema *jsonschema.Schema) { if schema.Properties != nil { for _, pair := range *(schema.Properties) { propertyName := pair.Name propertySchema := pair.Value if propertySchema.Ref != nil { // the property schema is a reference, so we will add a property with the type of the referenced schema propertyTypeName := domain.typeNameForReference(*(propertySchema.Ref)) typeProperty := NewTypeProperty() typeProperty.Name = propertyName typeProperty.Type = propertyTypeName typeModel.addProperty(typeProperty) } else if propertySchema.Type != nil { // the property schema specifies a type, so add a property with the specified type if propertySchema.TypeIs("string") { typeProperty := NewTypePropertyWithNameAndType(propertyName, "string") if propertySchema.Description != nil { typeProperty.Description = *propertySchema.Description } if propertySchema.Enumeration != nil { allowedValues := make([]string, 0) for _, enumValue := range *propertySchema.Enumeration { if enumValue.String != nil { allowedValues = append(allowedValues, *enumValue.String) } } typeProperty.StringEnumValues = allowedValues } typeModel.addProperty(typeProperty) } else if propertySchema.TypeIs("boolean") { typeProperty := NewTypePropertyWithNameAndType(propertyName, "bool") if propertySchema.Description != nil { typeProperty.Description = *propertySchema.Description } typeModel.addProperty(typeProperty) } else if propertySchema.TypeIs("number") { typeProperty := NewTypePropertyWithNameAndType(propertyName, "float") if propertySchema.Description != nil { typeProperty.Description = *propertySchema.Description } typeModel.addProperty(typeProperty) } else if propertySchema.TypeIs("integer") { typeProperty := NewTypePropertyWithNameAndType(propertyName, "int") if propertySchema.Description != nil { typeProperty.Description = *propertySchema.Description } typeModel.addProperty(typeProperty) } else if propertySchema.TypeIs("object") { // the property has an "anonymous" object schema, so define a new type for it and request its creation anonymousObjectTypeName := domain.TypeNameForStub(propertyName) domain.ObjectTypeRequests[anonymousObjectTypeName] = NewTypeRequest(anonymousObjectTypeName, propertyName, propertySchema) // add a property with the type of the requested type typeProperty := NewTypePropertyWithNameAndType(propertyName, anonymousObjectTypeName) if propertySchema.Description != nil { typeProperty.Description = *propertySchema.Description } typeModel.addProperty(typeProperty) } else if propertySchema.TypeIs("array") { // the property has an array type, so define it as a repeated property of the specified type propertyTypeName := domain.arrayItemTypeForSchema(propertyName, propertySchema) typeProperty := NewTypePropertyWithNameAndType(propertyName, propertyTypeName) typeProperty.Repeated = true if propertySchema.Description != nil { typeProperty.Description = *propertySchema.Description } if typeProperty.Type == "string" { itemSchema := propertySchema.Items.Schema if itemSchema != nil { if itemSchema.Enumeration != nil { allowedValues := make([]string, 0) for _, enumValue := range *itemSchema.Enumeration { if enumValue.String != nil { allowedValues = append(allowedValues, *enumValue.String) } } typeProperty.StringEnumValues = allowedValues } } } typeModel.addProperty(typeProperty) } else { log.Printf("ignoring %+v, which has an unsupported property type '%s'", propertyName, propertySchema.Type.Description()) } } else if propertySchema.IsEmpty() { // an empty schema can contain anything, so add an accessor for a generic object typeName := "Any" typeProperty := NewTypePropertyWithNameAndType(propertyName, typeName) typeModel.addProperty(typeProperty) } else if propertySchema.OneOf != nil { anonymousObjectTypeName := domain.TypeNameForStub(propertyName + "Item") domain.ObjectTypeRequests[anonymousObjectTypeName] = NewTypeRequest(anonymousObjectTypeName, propertyName, propertySchema) typeProperty := NewTypePropertyWithNameAndType(propertyName, anonymousObjectTypeName) typeModel.addProperty(typeProperty) } else if propertySchema.AnyOf != nil { anonymousObjectTypeName := domain.TypeNameForStub(propertyName + "Item") domain.ObjectTypeRequests[anonymousObjectTypeName] = NewTypeRequest(anonymousObjectTypeName, propertyName, propertySchema) typeProperty := NewTypePropertyWithNameAndType(propertyName, anonymousObjectTypeName) typeModel.addProperty(typeProperty) } else { log.Printf("ignoring %s.%s, which has an unrecognized schema:\n%+v", typeModel.Name, propertyName, propertySchema.String()) } } } } func (domain *Domain) buildTypeRequirements(typeModel *TypeModel, schema *jsonschema.Schema) { if schema.Required != nil { typeModel.Required = (*schema.Required) } } func (domain *Domain) buildPatternPropertyAccessors(typeModel *TypeModel, schema *jsonschema.Schema) { if schema.PatternProperties != nil { typeModel.OpenPatterns = make([]string, 0) for _, pair := range *(schema.PatternProperties) { propertyPattern := pair.Name propertySchema := pair.Value typeModel.OpenPatterns = append(typeModel.OpenPatterns, propertyPattern) if propertySchema.Ref != nil { typeName := domain.typeNameForReference(*propertySchema.Ref) if _, ok := domain.TypeNameOverrides[typeName]; ok { typeName = domain.TypeNameOverrides[typeName] } propertyName := domain.typeNameForReference(*propertySchema.Ref) if _, ok := domain.PropertyNameOverrides[propertyName]; ok { propertyName = domain.PropertyNameOverrides[propertyName] } propertyTypeName := fmt.Sprintf("Named%s", typeName) property := NewTypePropertyWithNameTypeAndPattern(propertyName, propertyTypeName, propertyPattern) property.Implicit = true property.MapType = typeName property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) } else { log.Printf("unhandled pattern property %+v", pair) } } } } func (domain *Domain) buildAdditionalPropertyAccessors(typeModel *TypeModel, schema *jsonschema.Schema) { if schema.AdditionalProperties != nil { if schema.AdditionalProperties.Boolean != nil { if *schema.AdditionalProperties.Boolean == true { typeModel.Open = true propertyName := "additionalProperties" typeName := "NamedAny" property := NewTypePropertyWithNameAndType(propertyName, typeName) property.Implicit = true property.MapType = "Any" property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) return } } else if schema.AdditionalProperties.Schema != nil { typeModel.Open = true schema := schema.AdditionalProperties.Schema if schema.Ref != nil { propertyName := "additionalProperties" mapType := domain.typeNameForReference(*schema.Ref) typeName := fmt.Sprintf("Named%s", mapType) property := NewTypePropertyWithNameAndType(propertyName, typeName) property.Implicit = true property.MapType = mapType property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) return } else if schema.Type != nil { typeName := *schema.Type.String if typeName == "string" { propertyName := "additionalProperties" typeName := "NamedString" property := NewTypePropertyWithNameAndType(propertyName, typeName) property.Implicit = true property.MapType = "string" property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) return } else if typeName == "array" { if schema.Items != nil { itemType := *schema.Items.Schema.Type.String if itemType == "string" { propertyName := "additionalProperties" typeName := "NamedStringArray" property := NewTypePropertyWithNameAndType(propertyName, typeName) property.Implicit = true property.MapType = "StringArray" property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) return } } } } else if schema.OneOf != nil { propertyTypeName := domain.TypeNameForStub(typeModel.Name + "Item") propertyName := "additionalProperties" typeName := fmt.Sprintf("Named%s", propertyTypeName) property := NewTypePropertyWithNameAndType(propertyName, typeName) property.Implicit = true property.MapType = propertyTypeName property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) domain.ObjectTypeRequests[propertyTypeName] = NewTypeRequest(propertyTypeName, propertyName, schema) } } } } func (domain *Domain) buildOneOfAccessors(typeModel *TypeModel, schema *jsonschema.Schema) { oneOfs := schema.OneOf if oneOfs == nil { return } typeModel.Open = true typeModel.OneOfWrapper = true for _, oneOf := range *oneOfs { if oneOf.Ref != nil { ref := *oneOf.Ref typeName := domain.typeNameForReference(ref) propertyName := domain.propertyNameForReference(ref) if propertyName != nil { typeProperty := NewTypePropertyWithNameAndType(*propertyName, typeName) typeModel.addProperty(typeProperty) } } else if oneOf.Type != nil && oneOf.Type.String != nil { switch *oneOf.Type.String { case "boolean": typeProperty := NewTypePropertyWithNameAndType("boolean", "bool") typeModel.addProperty(typeProperty) case "integer": typeProperty := NewTypePropertyWithNameAndType("integer", "int") typeModel.addProperty(typeProperty) case "number": typeProperty := NewTypePropertyWithNameAndType("number", "float") typeModel.addProperty(typeProperty) case "string": typeProperty := NewTypePropertyWithNameAndType("string", "string") typeModel.addProperty(typeProperty) default: log.Printf("Unsupported oneOf:\n%+v", oneOf.String()) } } else { log.Printf("Unsupported oneOf:\n%+v", oneOf.String()) } } } func schemaIsContainedInArray(s1 *jsonschema.Schema, s2 *jsonschema.Schema) bool { if s2.TypeIs("array") { if s2.Items.Schema != nil { if s1.IsEqual(s2.Items.Schema) { return true } } } return false } func (domain *Domain) addAnonymousAccessorForSchema( typeModel *TypeModel, schema *jsonschema.Schema, repeated bool) { ref := schema.Ref if ref != nil { typeName := domain.typeNameForReference(*ref) propertyName := domain.propertyNameForReference(*ref) if propertyName != nil { property := NewTypePropertyWithNameAndType(*propertyName, typeName) property.Repeated = true typeModel.addProperty(property) typeModel.IsItemArray = true } } else { typeName := "string" propertyName := "value" property := NewTypePropertyWithNameAndType(propertyName, typeName) property.Repeated = true typeModel.addProperty(property) typeModel.IsStringArray = true } } func (domain *Domain) buildAnyOfAccessors(typeModel *TypeModel, schema *jsonschema.Schema) { anyOfs := schema.AnyOf if anyOfs == nil { return } if len(*anyOfs) == 2 { if schemaIsContainedInArray((*anyOfs)[0], (*anyOfs)[1]) { //log.Printf("ARRAY OF %+v", (*anyOfs)[0].String()) schema := (*anyOfs)[0] domain.addAnonymousAccessorForSchema(typeModel, schema, true) } else if schemaIsContainedInArray((*anyOfs)[1], (*anyOfs)[0]) { //log.Printf("ARRAY OF %+v", (*anyOfs)[1].String()) schema := (*anyOfs)[1] domain.addAnonymousAccessorForSchema(typeModel, schema, true) } else { for _, anyOf := range *anyOfs { ref := anyOf.Ref if ref != nil { typeName := domain.typeNameForReference(*ref) propertyName := domain.propertyNameForReference(*ref) if propertyName != nil { property := NewTypePropertyWithNameAndType(*propertyName, typeName) typeModel.addProperty(property) } } else { typeName := "bool" propertyName := "boolean" property := NewTypePropertyWithNameAndType(propertyName, typeName) typeModel.addProperty(property) } } } } else { log.Printf("Unhandled anyOfs:\n%s", schema.String()) } } func (domain *Domain) buildDefaultAccessors(typeModel *TypeModel, schema *jsonschema.Schema) { typeModel.Open = true propertyName := "additionalProperties" typeName := "NamedAny" property := NewTypePropertyWithNameAndType(propertyName, typeName) property.MapType = "Any" property.Repeated = true domain.MapTypeRequests[property.MapType] = property.MapType typeModel.addProperty(property) } // BuildTypeForDefinition creates a type representation for a schema definition. func (domain *Domain) BuildTypeForDefinition( typeName string, propertyName string, schema *jsonschema.Schema) *TypeModel { if (schema.Type == nil) || (*schema.Type.String == "object") { return domain.buildTypeForDefinitionObject(typeName, propertyName, schema) } return nil } func (domain *Domain) buildTypeForDefinitionObject( typeName string, propertyName string, schema *jsonschema.Schema) *TypeModel { typeModel := NewTypeModel() typeModel.Name = typeName if schema.IsEmpty() { domain.buildDefaultAccessors(typeModel, schema) } else { if schema.Description != nil { typeModel.Description = *schema.Description } domain.buildTypeProperties(typeModel, schema) domain.buildTypeRequirements(typeModel, schema) domain.buildPatternPropertyAccessors(typeModel, schema) domain.buildAdditionalPropertyAccessors(typeModel, schema) domain.buildOneOfAccessors(typeModel, schema) domain.buildAnyOfAccessors(typeModel, schema) } return typeModel } // Build builds a domain model. func (domain *Domain) Build() (err error) { if (domain.Schema == nil) || (domain.Schema.Definitions == nil) { return errors.New("missing definitions section") } // create a type for the top-level schema typeName := domain.Prefix + "Document" typeModel := NewTypeModel() typeModel.Name = typeName domain.buildTypeProperties(typeModel, domain.Schema) domain.buildTypeRequirements(typeModel, domain.Schema) domain.buildPatternPropertyAccessors(typeModel, domain.Schema) domain.buildAdditionalPropertyAccessors(typeModel, domain.Schema) domain.buildOneOfAccessors(typeModel, domain.Schema) domain.buildAnyOfAccessors(typeModel, domain.Schema) if len(typeModel.Properties) > 0 { domain.TypeModels[typeName] = typeModel } // create a type for each object defined in the schema if domain.Schema.Definitions != nil { for _, pair := range *(domain.Schema.Definitions) { definitionName := pair.Name definitionSchema := pair.Value typeName := domain.TypeNameForStub(definitionName) typeModel := domain.BuildTypeForDefinition(typeName, definitionName, definitionSchema) if typeModel != nil { // open the reference types ($ref) to allow other fields to be specified but ignored if definitionName == "reference" || definitionName == "jsonReference" { typeModel.Open = true } domain.TypeModels[typeName] = typeModel } } } // iterate over anonymous object types to be instantiated and generate a type for each // we loop because these implied types could imply other types. // when implied types are instantiated (with buildTypeForDefinitionObject), // new requests might be added to domain.ObjectTypeRequests for len(domain.ObjectTypeRequests) > 0 { typeRequests := domain.ObjectTypeRequests domain.ObjectTypeRequests = make(map[string]*TypeRequest, 0) for typeName, typeRequest := range typeRequests { // this could add to domain.ObjectTypeRequests domain.TypeModels[typeRequest.Name] = domain.buildTypeForDefinitionObject(typeName, typeRequest.PropertyName, typeRequest.Schema) } } // iterate over map item types to be instantiated and generate a type for each mapTypeNames := make([]string, 0) for mapTypeName := range domain.MapTypeRequests { mapTypeNames = append(mapTypeNames, mapTypeName) } sort.Strings(mapTypeNames) for _, mapTypeName := range mapTypeNames { typeName := "Named" + strings.Title(mapTypeName) typeModel := NewTypeModel() typeModel.Name = typeName typeModel.Description = fmt.Sprintf( "Automatically-generated message used to represent maps of %s as ordered (name,value) pairs.", mapTypeName) typeModel.IsPair = true typeModel.PairValueType = mapTypeName nameProperty := NewTypeProperty() nameProperty.Name = "name" nameProperty.Type = "string" nameProperty.Description = "Map key" typeModel.addProperty(nameProperty) valueProperty := NewTypeProperty() valueProperty.Name = "value" valueProperty.Type = mapTypeName valueProperty.Description = "Mapped value" typeModel.addProperty(valueProperty) domain.TypeModels[typeName] = typeModel } // add a type for string arrays stringArrayType := NewTypeModel() stringArrayType.Name = "StringArray" stringProperty := NewTypeProperty() stringProperty.Name = "value" stringProperty.Type = "string" stringProperty.Repeated = true stringArrayType.addProperty(stringProperty) domain.TypeModels[stringArrayType.Name] = stringArrayType // add a type for "Any" anyType := NewTypeModel() anyType.Name = "Any" anyType.Open = true anyType.IsBlob = true valueProperty := NewTypeProperty() valueProperty.Name = "value" valueProperty.Type = "google.protobuf.Any" anyType.addProperty(valueProperty) yamlProperty := NewTypeProperty() yamlProperty.Name = "yaml" yamlProperty.Type = "string" anyType.addProperty(yamlProperty) domain.TypeModels[anyType.Name] = anyType return err } func (domain *Domain) sortedTypeNames() []string { typeNames := make([]string, 0) for typeName := range domain.TypeModels { typeNames = append(typeNames, typeName) } sort.Strings(typeNames) return typeNames } // Description returns a string representation of a domain. func (domain *Domain) Description() string { typeNames := domain.sortedTypeNames() result := "" for _, typeName := range typeNames { result += domain.TypeModels[typeName].description() } return result }