blob: 8628202896dcd2a62e2cc77099e1357c91897c04 [file] [log] [blame]
// 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 surface_v1
import (
"github.com/googleapis/gnostic/compiler"
openapiv3 "github.com/googleapis/gnostic/openapiv3"
"log"
"strings"
)
type OpenAPI3Builder struct {
model *Model
document *openapiv3.Document
}
// NewModelFromOpenAPIv3 builds a model of an API service for use in code generation.
func NewModelFromOpenAPI3(document *openapiv3.Document, sourceName string) (*Model, error) {
return newOpenAPI3Builder(document).buildModel(document, sourceName)
}
func newOpenAPI3Builder(document *openapiv3.Document) *OpenAPI3Builder {
return &OpenAPI3Builder{model: &Model{}, document: document}
}
// Fills the surface model with information from a parsed OpenAPI description. The surface model provides that information
// in a way that is more processable by plugins like gnostic-go-generator or gnostic-grpc.
// Since OpenAPI schemas can be indefinitely nested, it is a recursive approach to build all Types and Methods.
// The basic idea is that whenever we have "named OpenAPI object" (e.g.: NamedSchemaOrReference, NamedMediaType) we:
// 1. Create a Type with that name
// 2. Recursively create sub schemas (see buildFromSchema function)
// 3. Return a FieldInfo object that describes how the created Type should be represented inside another Type as field.
func (b *OpenAPI3Builder) buildModel(document *openapiv3.Document, sourceName string) (*Model, error) {
b.model.Types = make([]*Type, 0)
b.model.Methods = make([]*Method, 0)
// Set model properties from passed-in document.
b.model.Name = document.Info.Title
b.buildFromDocument(document)
err := b.buildSymbolicReferences(document, sourceName)
if err != nil {
log.Printf("Error while building symbolic references. This might cause the plugin to fail: %v", err)
}
return b.model, nil
}
// Builds Types from the component section; builds Types and methods from paths;
func (b *OpenAPI3Builder) buildFromDocument(document *openapiv3.Document) {
b.buildFromComponents(document.Components)
b.buildFromPaths(document.Paths)
}
// Builds all Types from an "OpenAPI component" section
func (b *OpenAPI3Builder) buildFromComponents(components *openapiv3.Components) {
if components == nil {
return
}
for _, namedSchema := range components.GetSchemas().GetAdditionalProperties() {
fInfo := b.buildFromSchemaOrReference(namedSchema.Name, namedSchema.Value)
b.checkForExistence(namedSchema.Name, fInfo)
}
for _, namedParameter := range components.GetParameters().GetAdditionalProperties() {
// Parameters in OpenAPI have a name field. See: https://swagger.io/specification/#parameterObject
// The name gets passed up the callstack and is therefore contained inside fInfo. That is why we pass "" as fieldName
// A type with that parameter was never created, so we still need to do that.
t := makeType(namedParameter.Name)
fInfo := b.buildFromParamOrRef(namedParameter.Value)
makeFieldAndAppendToType(fInfo, t, "")
if len(t.Fields) > 0 {
b.model.addType(t)
}
}
for _, namedResponses := range components.GetResponses().GetAdditionalProperties() {
fInfo := b.buildFromResponseOrRef(namedResponses.Name, namedResponses.Value)
b.checkForExistence(namedResponses.Name, fInfo)
}
for _, namedRequestBody := range components.GetRequestBodies().GetAdditionalProperties() {
fInfo := b.buildFromRequestBodyOrRef(namedRequestBody.Name, namedRequestBody.Value)
b.checkForExistence(namedRequestBody.Name, fInfo)
}
}
// Builds Methods and Types (parameters, request bodies, responses) from all paths
func (b *OpenAPI3Builder) buildFromPaths(paths *openapiv3.Paths) {
for _, path := range paths.Path {
b.buildFromNamedPath(path.Name, path.Value)
}
}
// Builds all symbolic references. A symbolic reference is an URL to another OpenAPI description. We call "document.ResolveReferences"
// inside that method. This has the same effect like: "gnostic --resolve-refs"
func (b *OpenAPI3Builder) buildSymbolicReferences(document *openapiv3.Document, sourceName string) (err error) {
cache := compiler.GetInfoCache()
if len(cache) == 0 && sourceName != "" {
// Fills the compiler cache with all kind of references.
_, err = document.ResolveReferences(sourceName)
if err != nil {
return err
}
cache = compiler.GetInfoCache()
}
for ref := range cache {
if isSymbolicReference(ref) {
b.model.SymbolicReferences = append(b.model.SymbolicReferences, ref)
}
}
// Clear compiler cache for recursive calls
compiler.ClearInfoCache()
return nil
}
// Builds a Method and adds it to the surface model
func (b *OpenAPI3Builder) buildFromNamedPath(name string, pathItem *openapiv3.PathItem) {
for _, method := range []string{"GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"} {
var op *openapiv3.Operation
switch method {
case "GET":
op = pathItem.Get
case "PUT":
op = pathItem.Put
case "POST":
op = pathItem.Post
case "DELETE":
op = pathItem.Delete
case "OPTIONS":
op = pathItem.Options
case "HEAD":
op = pathItem.Head
case "PATCH":
op = pathItem.Patch
case "TRACE":
op = pathItem.Trace
}
if op != nil {
m := &Method{
Operation: op.OperationId,
Path: name,
Method: method,
Name: sanitizeOperationName(op.OperationId),
Description: op.Description,
}
if m.Name == "" {
m.Name = generateOperationName(method, name)
}
m.ParametersTypeName, m.ResponsesTypeName = b.buildFromNamedOperation(m.Name, op)
b.model.addMethod(m)
}
}
}
// Builds the "Parameters" and "Responses" types for an operation, adds them to the model, and returns the names of the types.
// If no such Type is added to the model an empty string is returned.
func (b *OpenAPI3Builder) buildFromNamedOperation(name string, operation *openapiv3.Operation) (parametersTypeName string, responseTypeName string) {
// At first, we build the operations input parameters. This includes parameters (like PATH or QUERY parameters) and a request body
operationParameters := makeType(name + "Parameters")
operationParameters.Description = operationParameters.Name + " holds parameters to " + name
for _, paramOrRef := range operation.Parameters {
fieldInfo := b.buildFromParamOrRef(paramOrRef)
// For parameters the name of the field is contained inside fieldInfo. That is why we pass "" as fieldName
makeFieldAndAppendToType(fieldInfo, operationParameters, "")
}
if operation.RequestBody != nil {
fInfo := b.buildFromRequestBodyOrRef(operation.OperationId+"RequestBody", operation.RequestBody)
makeFieldAndAppendToType(fInfo, operationParameters, "request_body")
}
if len(operationParameters.Fields) > 0 {
b.model.addType(operationParameters)
parametersTypeName = operationParameters.Name
}
// Secondly, we build the response values for the method.
if responses := operation.Responses; responses != nil {
operationResponses := makeType(name + "Responses")
operationResponses.Description = operationResponses.Name + " holds responses of " + name
for _, namedResponse := range responses.ResponseOrReference {
fieldInfo := b.buildFromResponseOrRef(operation.OperationId+convertStatusCodeToText(namedResponse.Name), namedResponse.Value)
makeFieldAndAppendToType(fieldInfo, operationResponses, namedResponse.Name)
}
if responses.Default != nil {
fieldInfo := b.buildFromResponseOrRef(operation.OperationId+"Default", responses.Default)
makeFieldAndAppendToType(fieldInfo, operationResponses, "default")
}
if len(operationResponses.Fields) > 0 {
b.model.addType(operationResponses)
responseTypeName = operationResponses.Name
}
}
return parametersTypeName, responseTypeName
}
// A helper method to differentiate between references and actual objects.
// The actual Field and Type are created in the functions which call this function
func (b *OpenAPI3Builder) buildFromParamOrRef(paramOrRef *openapiv3.ParameterOrReference) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
if param := paramOrRef.GetParameter(); param != nil {
fInfo = b.buildFromParam(param)
return fInfo
} else if ref := paramOrRef.GetReference(); ref != nil {
t := findType(b.model.Types, validTypeForRef(ref.XRef))
if t != nil && len(t.Fields) > 0 {
fInfo.fieldKind, fInfo.fieldType, fInfo.fieldName, fInfo.fieldPosition = FieldKind_REFERENCE, validTypeForRef(ref.XRef), t.Name, t.Fields[0].Position
return fInfo
}
// TODO: This might happen for symbolic references --> fInfo.Position defaults to 'BODY' which is wrong.
log.Printf("Not able to find parameter information for: %v", ref)
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(ref.XRef)
return fInfo // Lets return fInfo for now otherwise we may get null pointer exception
}
return nil
}
// Returns information on how to represent 'parameter' as field. This information gets propagated up the callstack.
func (b *OpenAPI3Builder) buildFromParam(parameter *openapiv3.Parameter) (fInfo *FieldInfo) {
if schemaOrRef := parameter.Schema; schemaOrRef != nil {
fInfo = b.buildFromSchemaOrReference(parameter.Name, schemaOrRef)
fInfo.fieldName = parameter.Name
switch parameter.In {
case "body":
fInfo.fieldPosition = Position_BODY
case "header":
fInfo.fieldPosition = Position_HEADER
case "formdata":
fInfo.fieldPosition = Position_FORMDATA
case "query":
fInfo.fieldPosition = Position_QUERY
case "path":
fInfo.fieldPosition = Position_PATH
}
return fInfo
}
return nil
}
// A helper method to differentiate between references and actual objects
func (b *OpenAPI3Builder) buildFromRequestBodyOrRef(name string, reqBodyOrRef *openapiv3.RequestBodyOrReference) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
if requestBody := reqBodyOrRef.GetRequestBody(); requestBody != nil {
fInfo = b.buildFromRequestBody(name, requestBody)
return fInfo
} else if ref := reqBodyOrRef.GetReference(); ref != nil {
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(ref.XRef)
return fInfo
}
return nil
}
// Builds a Type for 'reqBody' and returns information on how to use this Type as field.
func (b *OpenAPI3Builder) buildFromRequestBody(name string, reqBody *openapiv3.RequestBody) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
if reqBody.Content != nil {
schemaType := makeType(name)
for _, namedMediaType := range reqBody.Content.AdditionalProperties {
fieldInfo := b.buildFromSchemaOrReference(name+namedMediaType.Name, namedMediaType.GetValue().GetSchema())
makeFieldAndAppendToType(fieldInfo, schemaType, namedMediaType.Name)
}
b.model.addType(schemaType)
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, schemaType.Name
return fInfo
}
return nil
}
// A helper method to differentiate between references and actual objects
func (b *OpenAPI3Builder) buildFromResponseOrRef(name string, responseOrRef *openapiv3.ResponseOrReference) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
if response := responseOrRef.GetResponse(); response != nil {
fInfo = b.buildFromResponse(name, response)
return fInfo
} else if ref := responseOrRef.GetReference(); ref != nil {
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(ref.XRef)
return fInfo
}
return nil
}
// Builds a Type for 'response' and returns information on how to use this Type as field.
func (b *OpenAPI3Builder) buildFromResponse(name string, response *openapiv3.Response) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
if response.Content != nil && response.Content.AdditionalProperties != nil {
schemaType := makeType(name)
for _, namedMediaType := range response.Content.AdditionalProperties {
fieldInfo := b.buildFromSchemaOrReference(name+namedMediaType.Name, namedMediaType.GetValue().GetSchema())
makeFieldAndAppendToType(fieldInfo, schemaType, namedMediaType.Name)
}
b.model.addType(schemaType)
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, schemaType.Name
return fInfo
}
return nil
}
// A helper method to differentiate between references and actual objects
func (b *OpenAPI3Builder) buildFromSchemaOrReference(name string, schemaOrReference *openapiv3.SchemaOrReference) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
if schema := schemaOrReference.GetSchema(); schema != nil {
fInfo = b.buildFromSchema(name, schema)
return fInfo
} else if ref := schemaOrReference.GetReference(); ref != nil {
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(ref.XRef)
return fInfo
}
return nil
}
// Given an OpenAPI schema there are two possibilities:
// 1. The schema is an object/array: We create a type for the object, recursively call according methods for child
// schemas, and then return information on how to use the created Type as field.
// 2. The schema has a scalar type: We return information on how to represent a scalar schema as Field. Fields are
// created whenever Types are created (higher up in the callstack). This possibility can be considered as the "base condition"
// for the recursive approach.
func (b *OpenAPI3Builder) buildFromSchema(name string, schema *openapiv3.Schema) (fInfo *FieldInfo) {
fInfo = &FieldInfo{}
// Data types according to: https://swagger.io/docs/specification/data-models/data-types/
switch schema.Type {
case "":
fallthrough
case "object":
schemaType := makeType(name)
for _, namedSchema := range schema.GetProperties().GetAdditionalProperties() {
fieldInfo := b.buildFromSchemaOrReference(namedSchema.Name, namedSchema.Value)
makeFieldAndAppendToType(fieldInfo, schemaType, namedSchema.Name)
}
if schemaOrRef := schema.AdditionalProperties.GetSchemaOrReference(); schemaOrRef != nil {
// AdditionalProperties are represented as map
fieldInfo := b.buildFromSchemaOrReference(name+"AdditionalProperties", schemaOrRef)
if fieldInfo != nil {
mapValueType := determineMapValueType(*fieldInfo)
fieldInfo.fieldKind, fieldInfo.fieldType, fieldInfo.fieldFormat = FieldKind_MAP, "map[string]"+mapValueType, ""
makeFieldAndAppendToType(fieldInfo, schemaType, "additional_properties")
}
}
for _, schemaOrRef := range schema.AnyOf {
b.buildFromOneOfAnyOfAndAllOf(schemaOrRef, schemaType)
}
for _, schemaOrRef := range schema.OneOf {
b.buildFromOneOfAnyOfAndAllOf(schemaOrRef, schemaType)
}
for _, schemaOrRef := range schema.AllOf {
b.buildFromOneOfAnyOfAndAllOf(schemaOrRef, schemaType)
}
if schema.Items != nil {
for _, schemaOrRef := range schema.Items.SchemaOrReference {
fieldInfo := b.buildFromSchemaOrReference(name+"Items", schemaOrRef)
makeFieldAndAppendToType(fieldInfo, schemaType, "items")
}
}
if len(schemaType.Fields) == 0 {
schemaType.Kind = TypeKind_OBJECT
schemaType.ContentType = "interface{}"
}
if t := findType(b.model.Types, schemaType.Name); t == nil {
b.model.addType(schemaType)
}
fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, schemaType.Name
return fInfo
case "array":
// I do believe there is a bug inside of OpenAPIv3.proto. I think the type of "schema.Items.SchemaOrReference"
// shouldn't be an array of pointers but rather a single pointer.
// According to: https://swagger.io/specification/#schemaObject
// The 'items' "Value MUST be an object and not an array" and "Inline or referenced schema MUST be of a Schema Object"
for _, schemaOrRef := range schema.Items.SchemaOrReference {
arrayFieldInfo := b.buildFromSchemaOrReference(name, schemaOrRef)
if arrayFieldInfo != nil {
fInfo.fieldKind, fInfo.fieldType, fInfo.fieldFormat, fInfo.enumValues = FieldKind_ARRAY, arrayFieldInfo.fieldType, arrayFieldInfo.fieldFormat, arrayFieldInfo.enumValues
return fInfo
}
}
default:
for _, enum := range schema.GetEnum() {
fInfo.enumValues = append(fInfo.enumValues, strings.TrimSuffix(enum.Yaml, "\n"))
}
// We go a scalar value
fInfo.fieldKind, fInfo.fieldType, fInfo.fieldFormat = FieldKind_SCALAR, schema.Type, schema.Format
return fInfo
}
log.Printf("Unimplemented: could not find field info for schema: %v", schema)
return nil
}
// buildFromOneOfAnyOfAndAllOf adds appropriate fields to the 'schemaType' given a new 'schemaOrRef'.
func (b *OpenAPI3Builder) buildFromOneOfAnyOfAndAllOf(schemaOrRef *openapiv3.SchemaOrReference, schemaType *Type) {
// Related: https://github.com/googleapis/gnostic-grpc/issues/22
if schema := schemaOrRef.GetSchema(); schema != nil {
// Build a temporary type that has the required fields; add the fields to the current schema; remove the
// temporary type
fieldInfo := b.buildFromSchemaOrReference("ATemporaryTypeThatWillBeRemoved", schemaOrRef)
t := findType(b.model.Types, "ATemporaryTypeThatWillBeRemoved")
if t == nil {
// schemaOrRef is some kind of primitive schema (e.g. of type string)
makeFieldAndAppendToType(fieldInfo, schemaType, "value")
return
}
schemaType.Fields = append(schemaType.Fields, t.Fields...)
b.removeType(t)
} else if ref := schemaOrRef.GetReference(); ref != nil {
referencedSchemaName := validTypeForRef(ref.XRef)
// Make sure that the referenced type exists, before we add the fields to the current schema
for _, namedSchema := range b.document.GetComponents().GetSchemas().GetAdditionalProperties() {
if referencedSchemaName == namedSchema.Name {
fInfo := b.buildFromSchemaOrReference(namedSchema.Name, namedSchema.Value)
b.checkForExistence(namedSchema.Name, fInfo)
break
}
}
t := findType(b.model.Types, referencedSchemaName)
if t == nil {
log.Printf("Unable to construct from OneOf, AnyOf, or AllOf schema. Referenced schema not found: %v", ref)
return
}
schemaType.Fields = append(schemaType.Fields, t.Fields...)
}
}
// removeType removes the Type 'toRemove' from the model.
func (b *OpenAPI3Builder) removeType(toRemove *Type) {
res := make([]*Type, 0)
for _, t := range b.model.Types {
if t != toRemove {
res = append(res, t)
}
}
b.model.Types = res
}
// checkForExistence creates a type (if a type with 'name' does not already exist) and adds a field with the
// information from 'fInfo'.
func (b *OpenAPI3Builder) checkForExistence(name string, fInfo *FieldInfo) {
// In certain cases no type will be created during the recursion. (e.g.: the schema is a primitive schema)
if t := findType(b.model.Types, name); t == nil {
t = makeType(name)
makeFieldAndAppendToType(fInfo, t, "value")
b.model.addType(t)
}
}