// Copyright 2017 Google Inc. 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.

// schema-generator is a support tool that generates the OpenAPI v3 JSON schema.
// Yes, it's gross, but the OpenAPI 3 spec, which defines REST APIs with a
// rigorous JSON schema, is itself defined with a Markdown file. Ironic?
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/googleapis/gnostic/jsonschema"
)

// convert the first character of a string to lower case
func lowerFirst(s string) string {
	if s == "" {
		return ""
	}
	r, n := utf8.DecodeRuneInString(s)
	return string(unicode.ToLower(r)) + s[n:]
}

// Section models a section of the OpenAPI specification text document.
type Section struct {
	Level    int
	Text     string
	Title    string
	Children []*Section
}

// ReadSection reads a section of the OpenAPI Specification, recursively dividing it into subsections
func ReadSection(text string, level int) (section *Section) {
	titlePattern := regexp.MustCompile("^" + strings.Repeat("#", level) + " .*$")
	subtitlePattern := regexp.MustCompile("^" + strings.Repeat("#", level+1) + " .*$")

	section = &Section{Level: level, Text: text}
	lines := strings.Split(string(text), "\n")
	subsection := ""
	for i, line := range lines {
		if i == 0 && titlePattern.Match([]byte(line)) {
			section.Title = line
		} else if subtitlePattern.Match([]byte(line)) {
			// we've found a subsection title.
			// if there's a subsection that we've already been reading, save it
			if len(subsection) != 0 {
				child := ReadSection(subsection, level+1)
				section.Children = append(section.Children, child)
			}
			// start a new subsection
			subsection = line + "\n"
		} else {
			// add to the subsection we've been reading
			subsection += line + "\n"
		}
	}
	// if this section has subsections, save the last one
	if len(section.Children) > 0 {
		child := ReadSection(subsection, level+1)
		section.Children = append(section.Children, child)
	}
	return
}

// Display recursively displays a section of the specification.
func (s *Section) Display(section string) {
	if len(s.Children) == 0 {
		//fmt.Printf("%s\n", s.Text)
	} else {
		for i, child := range s.Children {
			var subsection string
			if section == "" {
				subsection = fmt.Sprintf("%d", i)
			} else {
				subsection = fmt.Sprintf("%s.%d", section, i)
			}
			fmt.Printf("%-12s %s\n", subsection, child.NiceTitle())
			child.Display(subsection)
		}
	}
}

// remove a link from a string, leaving only the text that follows it
// if there is no link, just return the string
func stripLink(input string) (output string) {
	stringPattern := regexp.MustCompile("^(.*)$")
	stringWithLinkPattern := regexp.MustCompile("^<a .*</a>(.*)$")
	if matches := stringWithLinkPattern.FindSubmatch([]byte(input)); matches != nil {
		return string(matches[1])
	} else if matches := stringPattern.FindSubmatch([]byte(input)); matches != nil {
		return string(matches[1])
	} else {
		return input
	}
}

// NiceTitle returns a nice-to-display title for a section by removing the opening "###" and any links.
func (s *Section) NiceTitle() string {
	titlePattern := regexp.MustCompile("^#+ (.*)$")
	titleWithLinkPattern := regexp.MustCompile("^#+ <a .*</a>(.*)$")
	if matches := titleWithLinkPattern.FindSubmatch([]byte(s.Title)); matches != nil {
		return string(matches[1])
	} else if matches := titlePattern.FindSubmatch([]byte(s.Title)); matches != nil {
		return string(matches[1])
	} else {
		return ""
	}
}

// replace markdown links with their link text (removing the URL part)
func removeMarkdownLinks(input string) (output string) {
	markdownLink := regexp.MustCompile("\\[([^\\]\\[]*)\\]\\(([^\\)]*)\\)") // matches [link title](link url)
	output = string(markdownLink.ReplaceAll([]byte(input), []byte("$1")))
	return
}

// extract the fixed fields from a table in a section
func parseFixedFields(input string, schemaObject *SchemaObject) {
	lines := strings.Split(input, "\n")
	for _, line := range lines {

		// replace escaped bars with "OR", assuming these are used to describe union types
		line = strings.Replace(line, " \\| ", " OR ", -1)

		// split the table on the remaining bars
		parts := strings.Split(line, "|")
		if len(parts) > 1 {
			fieldName := strings.Trim(stripLink(parts[0]), " ")
			if fieldName != "Field Name" && fieldName != "---" {

				if len(parts) == 3 || len(parts) == 4 {
					// this is what we expect
				} else {
					log.Printf("ERROR: %+v", parts)
				}

				typeName := parts[1]
				typeName = strings.Replace(typeName, "{expression}", "Expression", -1)
				typeName = strings.Trim(typeName, " ")
				typeName = strings.Replace(typeName, "`", "", -1)
				typeName = removeMarkdownLinks(typeName)
				typeName = strings.Replace(typeName, " ", "", -1)
				typeName = strings.Replace(typeName, "Object", "", -1)
				isArray := false
				if typeName[0] == '[' && typeName[len(typeName)-1] == ']' {
					typeName = typeName[1 : len(typeName)-1]
					isArray = true
				}
				isMap := false
				mapPattern := regexp.MustCompile("^Mapstring,\\[(.*)\\]$")
				if matches := mapPattern.FindSubmatch([]byte(typeName)); matches != nil {
					typeName = string(matches[1])
					isMap = true
				} else {
					// match map[string,<typename>]
					mapPattern2 := regexp.MustCompile("^Map\\[string,(.+)\\]$")
					if matches := mapPattern2.FindSubmatch([]byte(typeName)); matches != nil {
						typeName = string(matches[1])
						isMap = true
					}
				}
				description := strings.Trim(parts[len(parts)-1], " ")
				description = removeMarkdownLinks(description)
				description = strings.Replace(description, "\n", " ", -1)

				requiredLabel1 := "**Required.** "
				requiredLabel2 := "**REQUIRED**."
				if strings.Contains(description, requiredLabel1) ||
					strings.Contains(description, requiredLabel2) {
					// only include required values if their "Validity" is "Any" or if no validity is specified
					valid := true
					if len(parts) == 4 {
						validity := parts[2]
						if strings.Contains(validity, "Any") {
							valid = true
						} else {
							valid = false
						}
					}
					if valid {
						schemaObject.RequiredFields = append(schemaObject.RequiredFields, fieldName)
					}
					description = strings.Replace(description, requiredLabel1, "", -1)
					description = strings.Replace(description, requiredLabel2, "", -1)
				}
				schemaField := SchemaObjectField{
					Name:        fieldName,
					Type:        typeName,
					IsArray:     isArray,
					IsMap:       isMap,
					Description: description,
				}
				schemaObject.FixedFields = append(schemaObject.FixedFields, schemaField)
			}
		}
	}
}

// extract the patterned fields from a table in a section
func parsePatternedFields(input string, schemaObject *SchemaObject) {
	lines := strings.Split(input, "\n")
	for _, line := range lines {

		line = strings.Replace(line, " \\| ", " OR ", -1)

		parts := strings.Split(line, "|")
		if len(parts) > 1 {
			fieldName := strings.Trim(stripLink(parts[0]), " ")
			fieldName = removeMarkdownLinks(fieldName)
			if fieldName == "HTTP Status Code" {
				fieldName = "^([0-9X]{3})$"
			}
			if fieldName != "Field Pattern" && fieldName != "---" {
				typeName := parts[1]
				typeName = strings.Trim(typeName, " ")
				typeName = strings.Replace(typeName, "`", "", -1)
				typeName = removeMarkdownLinks(typeName)
				typeName = strings.Replace(typeName, " ", "", -1)
				typeName = strings.Replace(typeName, "Object", "", -1)
				typeName = strings.Replace(typeName, "{expression}", "Expression", -1)
				isArray := false
				if typeName[0] == '[' && typeName[len(typeName)-1] == ']' {
					typeName = typeName[1 : len(typeName)-1]
					isArray = true
				}
				isMap := false
				mapPattern := regexp.MustCompile("^Mapstring,\\[(.*)\\]$")
				if matches := mapPattern.FindSubmatch([]byte(typeName)); matches != nil {
					typeName = string(matches[1])
					isMap = true
				}
				description := strings.Trim(parts[len(parts)-1], " ")
				description = removeMarkdownLinks(description)
				description = strings.Replace(description, "\n", " ", -1)

				schemaField := SchemaObjectField{
					Name:        fieldName,
					Type:        typeName,
					IsArray:     isArray,
					IsMap:       isMap,
					Description: description,
				}
				schemaObject.PatternedFields = append(schemaObject.PatternedFields, schemaField)
			}
		}
	}
}

// SchemaObjectField describes a field of a schema.
type SchemaObjectField struct {
	Name        string `json:"name"`
	Type        string `json:"type"`
	IsArray     bool   `json:"is_array"`
	IsMap       bool   `json:"is_map"`
	Description string `json:"description"`
}

// SchemaObject describes a schema.
type SchemaObject struct {
	Name            string              `json:"name"`
	ID              string              `json:"id"`
	Description     string              `json:"description"`
	Extendable      bool                `json:"extendable"`
	RequiredFields  []string            `json:"required"`
	FixedFields     []SchemaObjectField `json:"fixed"`
	PatternedFields []SchemaObjectField `json:"patterned"`
}

// SchemaModel is a collection of schemas.
type SchemaModel struct {
	Objects []SchemaObject
}

func (m *SchemaModel) objectWithID(id string) *SchemaObject {
	for _, object := range m.Objects {
		if object.ID == id {
			return &object
		}
	}
	return nil
}

// NewSchemaModel returns a new SchemaModel.
func NewSchemaModel(filename string) (schemaModel *SchemaModel, err error) {

	b, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	// divide the specification into sections
	document := ReadSection(string(b), 1)
	document.Display("")

	// read object names and their details
	specification := document.Children[4] // fragile! the section title is "Specification"
	schema := specification.Children[7]   // fragile! the section title is "Schema"
	anchor := regexp.MustCompile("^#### <a name=\"(.*)Object\"")
	schemaObjects := make([]SchemaObject, 0)
	for _, section := range schema.Children {
		if matches := anchor.FindSubmatch([]byte(section.Title)); matches != nil {

			id := string(matches[1])

			schemaObject := SchemaObject{
				Name:           section.NiceTitle(),
				ID:             id,
				RequiredFields: nil,
			}

			if len(section.Children) > 0 {
				description := section.Children[0].Text
				description = removeMarkdownLinks(description)
				description = strings.Trim(description, " \t\n")
				description = strings.Replace(description, "\n", " ", -1)
				schemaObject.Description = description
			}

			// is the object extendable?
			if strings.Contains(section.Text, "Specification Extensions") {
				schemaObject.Extendable = true
			}

			// look for fixed fields
			for _, child := range section.Children {
				if child.NiceTitle() == "Fixed Fields" {
					parseFixedFields(child.Text, &schemaObject)
				}
			}

			// look for patterned fields
			for _, child := range section.Children {
				if child.NiceTitle() == "Patterned Fields" {
					parsePatternedFields(child.Text, &schemaObject)
				}
			}

			schemaObjects = append(schemaObjects, schemaObject)
		}
	}

	return &SchemaModel{Objects: schemaObjects}, nil
}

// UnionType represents a union of two types.
type UnionType struct {
	Name        string
	ObjectType1 string
	ObjectType2 string
}

var unionTypes map[string]*UnionType

func noteUnionType(typeName, objectType1, objectType2 string) {
	if unionTypes == nil {
		unionTypes = make(map[string]*UnionType, 0)
	}
	unionTypes[typeName] = &UnionType{
		Name:        typeName,
		ObjectType1: objectType1,
		ObjectType2: objectType2,
	}
}

// MapType represents a map of a specified type (with string keys).
type MapType struct {
	Name       string
	ObjectType string
}

var mapTypes map[string]*MapType

func noteMapType(typeName, objectType string) {
	if mapTypes == nil {
		mapTypes = make(map[string]*MapType, 0)
	}
	mapTypes[typeName] = &MapType{
		Name:       typeName,
		ObjectType: objectType,
	}
}

func definitionNameForType(typeName string) string {
	name := typeName
	switch typeName {
	case "OAuthFlows":
		name = "oauthFlows"
	case "OAuthFlow":
		name = "oauthFlow"
	case "XML":
		name = "xml"
	case "ExternalDocumentation":
		name = "externalDocs"
	default:
		// does the name contain an "OR"
		if parts := strings.Split(typeName, "OR"); len(parts) > 1 {
			name = lowerFirst(parts[0]) + "Or" + parts[1]
			noteUnionType(name, parts[0], parts[1])
		} else {
			name = lowerFirst(typeName)
		}
	}
	return "#/definitions/" + name
}

func pluralize(name string) string {
	if name == "any" {
		return "anys"
	}
	switch name[len(name)-1] {
	case 'y':
		name = name[0:len(name)-1] + "ies"
	case 's':
		name = name + "Map"
	default:
		name = name + "s"
	}
	return name
}

func definitionNameForMapOfType(typeName string) string {
	// pluralize the type name to get the name of an object representing a map of them
	var elementTypeName string
	var mapTypeName string
	if parts := strings.Split(typeName, "OR"); len(parts) > 1 {
		elementTypeName = lowerFirst(parts[0]) + "Or" + parts[1]
		noteUnionType(elementTypeName, parts[0], parts[1])
		mapTypeName = pluralize(lowerFirst(parts[0])) + "Or" + pluralize(parts[1])
	} else {
		elementTypeName = lowerFirst(typeName)
		mapTypeName = pluralize(elementTypeName)
	}
	noteMapType(mapTypeName, elementTypeName)
	return "#/definitions/" + mapTypeName
}

func updateSchemaFieldWithModelField(schemaField *jsonschema.Schema, modelField *SchemaObjectField) {
	// fmt.Printf("IN %s:%+v\n", name, schemaField)
	// update the attributes of the schema field
	if modelField.IsArray {
		// is array
		itemSchema := &jsonschema.Schema{}
		switch modelField.Type {
		case "string":
			itemSchema.Type = jsonschema.NewStringOrStringArrayWithString("string")
		case "boolean":
			itemSchema.Type = jsonschema.NewStringOrStringArrayWithString("boolean")
		case "primitive":
			itemSchema.Ref = stringptr(definitionNameForType("Primitive"))
		default:
			itemSchema.Ref = stringptr(definitionNameForType(modelField.Type))
		}
		schemaField.Items = jsonschema.NewSchemaOrSchemaArrayWithSchema(itemSchema)
		schemaField.Type = jsonschema.NewStringOrStringArrayWithString("array")
		boolValue := true // not sure about this
		schemaField.UniqueItems = &boolValue
	} else if modelField.IsMap {
		schemaField.Ref = stringptr(definitionNameForMapOfType(modelField.Type))
	} else {
		// is scalar
		switch modelField.Type {
		case "string":
			schemaField.Type = jsonschema.NewStringOrStringArrayWithString("string")
		case "boolean":
			schemaField.Type = jsonschema.NewStringOrStringArrayWithString("boolean")
		case "primitive":
			schemaField.Ref = stringptr(definitionNameForType("Primitive"))
		default:
			schemaField.Ref = stringptr(definitionNameForType(modelField.Type))
		}
	}
}

func buildSchemaWithModel(modelObject *SchemaObject) (schema *jsonschema.Schema) {

	schema = &jsonschema.Schema{}
	schema.Type = jsonschema.NewStringOrStringArrayWithString("object")

	if modelObject.RequiredFields != nil && len(modelObject.RequiredFields) > 0 {
		// copy array
		arrayCopy := modelObject.RequiredFields
		schema.Required = &arrayCopy
	}

	schema.AdditionalProperties = jsonschema.NewSchemaOrBooleanWithBoolean(false)

	schema.Description = stringptr(modelObject.Description)

	// handle fixed fields
	if modelObject.FixedFields != nil {
		newNamedSchemas := make([]*jsonschema.NamedSchema, 0)
		for _, modelField := range modelObject.FixedFields {
			schemaField := schema.PropertyWithName(modelField.Name)
			if schemaField == nil {
				// create and add the schema field
				schemaField = &jsonschema.Schema{}
				namedSchema := &jsonschema.NamedSchema{Name: modelField.Name, Value: schemaField}
				newNamedSchemas = append(newNamedSchemas, namedSchema)
			}
			updateSchemaFieldWithModelField(schemaField, &modelField)
		}
		for _, pair := range newNamedSchemas {
			if schema.Properties == nil {
				properties := make([]*jsonschema.NamedSchema, 0)
				schema.Properties = &properties
			}
			*(schema.Properties) = append(*(schema.Properties), pair)
		}

	} else {
		if schema.Properties != nil {
			fmt.Printf("SCHEMA SHOULD NOT HAVE PROPERTIES %s\n", modelObject.ID)
		}
	}

	// handle patterned fields
	if modelObject.PatternedFields != nil {
		newNamedSchemas := make([]*jsonschema.NamedSchema, 0)

		for _, modelField := range modelObject.PatternedFields {
			schemaField := schema.PatternPropertyWithName(modelField.Name)
			if schemaField == nil {
				// create and add the schema field
				schemaField = &jsonschema.Schema{}
				// Component names should match "^[a-zA-Z0-9\.\-_]+$"
				// See https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#componentsObject
				nameRegex := "^[a-zA-Z0-9\\\\.\\\\-_]+$"
				if modelObject.Name == "Scopes Object" {
					nameRegex = "^"
				} else if modelObject.Name == "Headers Object" {
					nameRegex = "^[a-zA-Z0-9!#\\-\\$%&'\\*\\+\\\\\\.\\^_`\\|~]+"
				}
				propertyName := strings.Replace(modelField.Name, "{name}", nameRegex, -1)
				//  The field name MUST begin with a slash, see https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#paths-object
				// JSON Schema for OpenAPI v2 uses "^/" as regex for paths, see https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/schemas/v2.0/schema.json#L173
				propertyName = strings.Replace(propertyName, "/{path}", "^/", -1)
				// Replace human-friendly (and regex-confusing) description with a blank pattern
				propertyName = strings.Replace(propertyName, "{expression}", "^", -1)
				propertyName = strings.Replace(propertyName, "{property}", "^", -1)
				namedSchema := &jsonschema.NamedSchema{Name: propertyName, Value: schemaField}
				newNamedSchemas = append(newNamedSchemas, namedSchema)
			}
			updateSchemaFieldWithModelField(schemaField, &modelField)
		}

		for _, pair := range newNamedSchemas {
			if schema.PatternProperties == nil {
				properties := make([]*jsonschema.NamedSchema, 0)
				schema.PatternProperties = &properties
			}
			*(schema.PatternProperties) = append(*(schema.PatternProperties), pair)
		}

	} else {
		if schema.PatternProperties != nil && !modelObject.Extendable {
			fmt.Printf("SCHEMA SHOULD NOT HAVE PATTERN PROPERTIES %s\n", modelObject.ID)
		}
	}

	if modelObject.Extendable {
		schemaField := schema.PatternPropertyWithName("^x-")
		if schemaField != nil {
			schemaField.Ref = stringptr("#/definitions/specificationExtension")
		} else {
			schemaField = &jsonschema.Schema{}
			schemaField.Ref = stringptr("#/definitions/specificationExtension")
			namedSchema := &jsonschema.NamedSchema{Name: "^x-", Value: schemaField}
			if schema.PatternProperties == nil {
				properties := make([]*jsonschema.NamedSchema, 0)
				schema.PatternProperties = &properties
			}
			*(schema.PatternProperties) = append(*(schema.PatternProperties), namedSchema)
		}
	} else {
		schemaField := schema.PatternPropertyWithName("^x-")
		if schemaField != nil {
			fmt.Printf("INVALID EXTENSION SUPPORT %s:%s\n", modelObject.ID, "^x-")
		}
	}

	return schema
}

// return a pointer to a copy of a passed-in string
func stringptr(input string) (output *string) {
	return &input
}

func int64ptr(input int64) (output *int64) {
	return &input
}

func arrayOfSchema() *jsonschema.Schema {
	return &jsonschema.Schema{
		Type:     jsonschema.NewStringOrStringArrayWithString("array"),
		MinItems: int64ptr(1),
		Items:    jsonschema.NewSchemaOrSchemaArrayWithSchema(&jsonschema.Schema{Ref: stringptr("#/definitions/schemaOrReference")}),
	}
}

func main() {
	// read and parse the text specification into a model structure
	model, err := NewSchemaModel("3.1.0.md")
	if err != nil {
		panic(err)
	}

	// write the model as JSON (for debugging)
	modelJSON, _ := json.MarshalIndent(model, "", "  ")
	err = ioutil.WriteFile("model.json", modelJSON, 0644)
	if err != nil {
		panic(err)
	}

	// build the top-level schema using the "OAS" model
	oasModel := model.objectWithID("oas")
	if oasModel == nil {
		log.Printf("Unable to find OAS model. Has the source document structure changed?")
		os.Exit(-1)
	}
	schema := buildSchemaWithModel(oasModel)

	// manually set a few fields
	schema.Title = stringptr("A JSON Schema for OpenAPI 3.0.")
	schema.ID = stringptr("http://openapis.org/v3/schema.json#")
	schema.Schema = stringptr("http://json-schema.org/draft-04/schema#")

	// loop over all models and create the corresponding schema objects
	definitions := make([]*jsonschema.NamedSchema, 0)
	schema.Definitions = &definitions

	for _, modelObject := range model.Objects {
		if modelObject.ID == "oas" {
			continue
		}
		definitionSchema := buildSchemaWithModel(&modelObject)
		name := modelObject.ID
		if name == "externalDocumentation" {
			name = "externalDocs"
		}
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema(name, definitionSchema))
	}

	// copy the properties of headerObject from parameterObject
	headerObject := schema.DefinitionWithName("header")
	parameterObject := schema.DefinitionWithName("parameter")
	if parameterObject != nil {
		newArray := make([]*jsonschema.NamedSchema, 0)
		for _, property := range *(parameterObject.Properties) {
			// we need to remove a few properties...
			if property.Name != "name" && property.Name != "in" {
				newArray = append(newArray, property)
			}
		}
		headerObject.Properties = &newArray
		// "So a shorthand for copying array arr would be tmp := append([]int{}, arr...)"
		ppArray := make([]*jsonschema.NamedSchema, 0)
		ppArray = append(ppArray, *(parameterObject.PatternProperties)...)
		headerObject.PatternProperties = &ppArray
	}

	// generate implied union types
	unionTypeKeys := make([]string, 0, len(unionTypes))
	for key := range unionTypes {
		unionTypeKeys = append(unionTypeKeys, key)
	}
	sort.Strings(unionTypeKeys)
	for _, unionTypeKey := range unionTypeKeys {
		unionType := unionTypes[unionTypeKey]
		objectSchema := schema.DefinitionWithName(unionType.Name)
		if objectSchema == nil {
			objectSchema = &jsonschema.Schema{}
			oneOf := make([]*jsonschema.Schema, 0)
			oneOf = append(oneOf, &jsonschema.Schema{Ref: stringptr("#/definitions/" + lowerFirst(unionType.ObjectType1))})
			oneOf = append(oneOf, &jsonschema.Schema{Ref: stringptr("#/definitions/" + lowerFirst(unionType.ObjectType2))})
			objectSchema.OneOf = &oneOf
			*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema(unionType.Name, objectSchema))
		}
	}

	// generate implied map types
	mapTypeKeys := make([]string, 0, len(mapTypes))
	for key := range mapTypes {
		mapTypeKeys = append(mapTypeKeys, key)
	}
	sort.Strings(mapTypeKeys)
	for _, mapTypeKey := range mapTypeKeys {
		mapType := mapTypes[mapTypeKey]
		objectSchema := schema.DefinitionWithName(mapType.Name)
		if objectSchema == nil {
			objectSchema = &jsonschema.Schema{}
			objectSchema.Type = jsonschema.NewStringOrStringArrayWithString("object")
			additionalPropertiesSchema := &jsonschema.Schema{}
			if mapType.ObjectType == "string" {
				additionalPropertiesSchema.Type = jsonschema.NewStringOrStringArrayWithString("string")
			} else {
				additionalPropertiesSchema.Ref = stringptr("#/definitions/" + lowerFirst(mapType.ObjectType))
			}
			objectSchema.AdditionalProperties = jsonschema.NewSchemaOrBooleanWithSchema(additionalPropertiesSchema)
			*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema(mapType.Name, objectSchema))
		}
	}

	// add schema objects for "object", "any", and "expression"
	if true {
		objectSchema := &jsonschema.Schema{}
		objectSchema.Type = jsonschema.NewStringOrStringArrayWithString("object")
		objectSchema.AdditionalProperties = jsonschema.NewSchemaOrBooleanWithBoolean(true)
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema("object", objectSchema))
	}
	if true {
		objectSchema := &jsonschema.Schema{}
		objectSchema.AdditionalProperties = jsonschema.NewSchemaOrBooleanWithBoolean(true)
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema("any", objectSchema))
	}
	if true {
		objectSchema := &jsonschema.Schema{}
		objectSchema.Type = jsonschema.NewStringOrStringArrayWithString("object")
		objectSchema.AdditionalProperties = jsonschema.NewSchemaOrBooleanWithBoolean(true)
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema("expression", objectSchema))
	}

	// add schema objects for "specificationExtension"
	if true {
		objectSchema := &jsonschema.Schema{}
		objectSchema.Description = stringptr("Any property starting with x- is valid.")
		oneOf := make([]*jsonschema.Schema, 0)
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("null")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("number")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("boolean")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("string")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("object")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("array")})
		objectSchema.OneOf = &oneOf
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema("specificationExtension", objectSchema))
	}

	// add schema objects for "defaultType"
	if true {
		objectSchema := &jsonschema.Schema{}
		oneOf := make([]*jsonschema.Schema, 0)
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("null")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("array")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("object")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("number")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("boolean")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("string")})
		objectSchema.OneOf = &oneOf
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema("defaultType", objectSchema))
	}

	// add schema objects for "primitive"
	if false { // we don't seem to need these for 3.0 RC2
		objectSchema := &jsonschema.Schema{}
		oneOf := make([]*jsonschema.Schema, 0)
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("number")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("boolean")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("string")})
		objectSchema.OneOf = &oneOf
		*schema.Definitions = append(*schema.Definitions, jsonschema.NewNamedSchema("primitive", objectSchema))
	}

	// force a few more things into the "schema" schema
	schemaObject := schema.DefinitionWithName("schema")
	schemaObject.CopyOfficialSchemaProperties(
		[]string{
			"title",
			"multipleOf",
			"maximum",
			"exclusiveMaximum",
			"minimum",
			"exclusiveMinimum",
			"maxLength",
			"minLength",
			"pattern",
			"maxItems",
			"minItems",
			"uniqueItems",
			"maxProperties",
			"minProperties",
			"required",
			"enum",
		})
	schemaObject.AdditionalProperties = jsonschema.NewSchemaOrBooleanWithBoolean(false)
	schemaObject.AddProperty("type", &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("string")})
	schemaObject.AddProperty("allOf", arrayOfSchema())
	schemaObject.AddProperty("oneOf", arrayOfSchema())
	schemaObject.AddProperty("anyOf", arrayOfSchema())
	schemaObject.AddProperty("not", &jsonschema.Schema{Ref: stringptr("#/definitions/schema")})
	anyOf := make([]*jsonschema.Schema, 0)
	anyOf = append(anyOf, &jsonschema.Schema{Ref: stringptr("#/definitions/schemaOrReference")})
	anyOf = append(anyOf, arrayOfSchema())
	schemaObject.AddProperty("items",
		&jsonschema.Schema{AnyOf: &anyOf})
	schemaObject.AddProperty("properties", &jsonschema.Schema{
		Type: jsonschema.NewStringOrStringArrayWithString("object"),
		AdditionalProperties: jsonschema.NewSchemaOrBooleanWithSchema(
			&jsonschema.Schema{Ref: stringptr("#/definitions/schemaOrReference")})})

	if true { // add additionalProperties schema object
		oneOf := make([]*jsonschema.Schema, 0)
		oneOf = append(oneOf, &jsonschema.Schema{Ref: stringptr("#/definitions/schemaOrReference")})
		oneOf = append(oneOf, &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("boolean")})
		schemaObject.AddProperty("additionalProperties", &jsonschema.Schema{OneOf: &oneOf})
	}

	schemaObject.AddProperty("default", &jsonschema.Schema{Ref: stringptr("#/definitions/defaultType")})
	schemaObject.AddProperty("description", &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("string")})
	schemaObject.AddProperty("format", &jsonschema.Schema{Type: jsonschema.NewStringOrStringArrayWithString("string")})

	// fix the content object
	contentObject := schema.DefinitionWithName("content")
	if contentObject != nil {
		pairs := make([]*jsonschema.NamedSchema, 0)
		contentObject.PatternProperties = &pairs
		namedSchema := &jsonschema.NamedSchema{Name: "^", Value: &jsonschema.Schema{Ref: stringptr("#/definitions/mediaType")}}
		*(contentObject.PatternProperties) = append(*(contentObject.PatternProperties), namedSchema)
	}

	// fix the contact object
	contactObject := schema.DefinitionWithName("contact")
	if contactObject != nil {
		emailProperty := contactObject.PropertyWithName("email")
		if emailProperty != nil {
			emailProperty.Format = stringptr("email");
		}
		urlProperty := contactObject.PropertyWithName("url")
		if urlProperty != nil {
			urlProperty.Format = stringptr("uri");
		}
	}

	// write the updated schema
	output := schema.JSONString()
	err = ioutil.WriteFile("schema.json", []byte(output), 0644)
	if err != nil {
		panic(err)
	}
}
