blob: eb82b27e46b375ad7b001473dd1517064e44536b [file] [log] [blame]
// 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.
package surface_v1
import (
"errors"
"fmt"
"log"
"strings"
openapiv3 "github.com/googleapis/gnostic/OpenAPIv3"
)
// NewModelFromOpenAPIv3 builds a model of an API service for use in code generation.
func NewModelFromOpenAPI3(document *openapiv3.Document) (*Model, error) {
return newOpenAPI3Builder().buildModel(document)
}
type OpenAPI3Builder struct {
model *Model
}
func newOpenAPI3Builder() *OpenAPI3Builder {
return &OpenAPI3Builder{model: &Model{}}
}
func (b *OpenAPI3Builder) buildModel(document *openapiv3.Document) (*Model, error) {
// Set model properties from passed-in document.
b.model.Name = document.Info.Title
b.model.Types = make([]*Type, 0)
b.model.Methods = make([]*Method, 0)
err := b.build(document)
if err != nil {
return nil, err
}
return b.model, nil
}
// build builds an API service description, preprocessing its types and methods for code generation.
func (b *OpenAPI3Builder) build(document *openapiv3.Document) (err error) {
// Collect service type descriptions from Components/Schemas.
if document.Components != nil && document.Components.Schemas != nil {
for _, pair := range document.Components.Schemas.AdditionalProperties {
t, err := b.buildTypeFromSchemaOrReference(pair.Name, pair.Value)
if err != nil {
return err
}
if t != nil {
b.model.addType(t)
}
}
}
// Collect service method descriptions from each PathItem.
for _, pair := range document.Paths.Path {
b.buildMethodFromPathItem(pair.Name, pair.Value)
}
return err
}
// buildTypeFromSchemaOrReference builds a service type description from a schema in the API description.
func (b *OpenAPI3Builder) buildTypeFromSchemaOrReference(
name string,
schemaOrReference *openapiv3.SchemaOrReference) (t *Type, err error) {
if schema := schemaOrReference.GetSchema(); schema != nil {
t = &Type{}
t.Name = name
t.Description = "implements the service definition of " + name
t.Fields = make([]*Field, 0)
if schema.Properties != nil {
if len(schema.Properties.AdditionalProperties) > 0 {
// If the schema has properties, generate a struct.
t.Kind = TypeKind_STRUCT
}
for _, pair2 := range schema.Properties.AdditionalProperties {
if schema := pair2.Value; schema != nil {
var f Field
f.Name = pair2.Name
f.Kind, f.Type, f.Format = b.typeForSchemaOrReference(schema)
f.Serialize = true
t.addField(&f)
}
}
}
if len(t.Fields) == 0 {
if schema.AdditionalProperties != nil {
// If the schema has no fixed properties and additional properties of a specified type,
// generate a map pointing to objects of that type.
t.Kind = TypeKind_OBJECT
t.ContentType = typeForRef(schema.AdditionalProperties.GetSchemaOrReference().GetReference().GetXRef())
}
}
return t, err
} else {
return nil, errors.New("unable to determine service type for referenced schema " + name)
}
}
// buildMethodFromOperation builds a service method description
func (b *OpenAPI3Builder) buildMethodFromPathItem(
path string,
pathItem *openapiv3.PathItem) (err error) {
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 {
var m Method
m.Operation = op.OperationId
m.Path = path
m.Method = method
m.Name = sanitizeOperationName(op.OperationId)
if m.Name == "" {
m.Name = generateOperationName(method, path)
}
m.Description = op.Description
m.ParametersTypeName, err = b.buildTypeFromParameters(m.Name, op.Parameters, op.RequestBody)
m.ResponsesTypeName, err = b.buildTypeFromResponses(&m, m.Name, op.Responses)
b.model.addMethod(&m)
}
}
return err
}
// buildTypeFromParameters builds a service type description from the parameters of an API method
func (b *OpenAPI3Builder) buildTypeFromParameters(
name string,
parameters []*openapiv3.ParameterOrReference,
requestBody *openapiv3.RequestBodyOrReference) (typeName string, err error) {
t := &Type{}
t.Name = name + "Parameters"
t.Description = t.Name + " holds parameters to " + name
t.Kind = TypeKind_STRUCT
t.Fields = make([]*Field, 0)
for _, parametersItem := range parameters {
var f Field
f.Type = fmt.Sprintf("%+v", parametersItem)
parameter := parametersItem.GetParameter()
if parameter != nil {
switch parameter.In {
case "body":
f.Position = Position_BODY
case "header":
f.Position = Position_HEADER
case "formdata":
f.Position = Position_FORMDATA
case "query":
f.Position = Position_QUERY
case "path":
f.Position = Position_PATH
}
f.Name = parameter.Name
if parameter.GetSchema() != nil && parameter.GetSchema() != nil {
f.Kind, f.Type, f.Format = b.typeForSchemaOrReference(parameter.GetSchema())
}
f.Serialize = true
t.addField(&f)
}
}
if requestBody != nil {
content := requestBody.GetRequestBody().GetContent()
if content != nil {
for _, pair2 := range content.GetAdditionalProperties() {
var f Field
f.Position = Position_BODY
f.Kind, f.Type, f.Format = b.typeForSchemaOrReference(pair2.GetValue().GetSchema())
f.Name = strings.ToLower(f.Type) // use the schema name as the parameter name, since none is directly specified
f.Serialize = true
t.addField(&f)
}
}
}
if len(t.Fields) > 0 {
b.model.addType(t)
return t.Name, err
}
return "", err
}
// buildTypeFromResponses builds a service type description from the responses of an API method
func (b *OpenAPI3Builder) buildTypeFromResponses(
m *Method,
name string,
responses *openapiv3.Responses) (typeName string, err error) {
t := &Type{}
t.Name = name + "Responses"
t.Description = t.Name + " holds responses of " + name
t.Kind = TypeKind_STRUCT
t.Fields = make([]*Field, 0)
addResponse := func(name string, value *openapiv3.ResponseOrReference) {
var f Field
f.Name = name
f.Serialize = false
response := value.GetResponse()
if response != nil && response.GetContent() != nil {
for _, pair2 := range response.GetContent().GetAdditionalProperties() {
f.Kind, f.Type, f.Format = b.typeForSchemaOrReference(pair2.GetValue().GetSchema())
f.Kind = FieldKind_REFERENCE
t.addField(&f)
}
}
}
for _, pair := range responses.ResponseOrReference {
addResponse(pair.Name, pair.Value)
}
if responses.Default != nil {
addResponse("default", responses.Default)
}
if len(t.Fields) > 0 {
b.model.addType(t)
return t.Name, err
}
return "", err
}
// typeForSchemaOrReference determines the language-specific type of a schema or reference
func (b *OpenAPI3Builder) typeForSchemaOrReference(value *openapiv3.SchemaOrReference) (kind FieldKind, typeName, format string) {
if value.GetSchema() != nil {
return b.typeForSchema(value.GetSchema())
}
if value.GetReference() != nil {
return FieldKind_SCALAR, typeForRef(value.GetReference().XRef), ""
}
return FieldKind_SCALAR, "todo", ""
}
// typeForSchema determines the language-specific type of a schema
func (b *OpenAPI3Builder) typeForSchema(schema *openapiv3.Schema) (kind FieldKind, typeName, format string) {
if schema.Type != "" {
format := schema.Format
switch schema.Type {
case "string":
return FieldKind_SCALAR, "string", format
case "integer":
return FieldKind_SCALAR, "integer", format
case "number":
return FieldKind_SCALAR, "number", format
case "boolean":
return FieldKind_SCALAR, "boolean", format
case "array":
if schema.Items != nil {
// we have an array.., but of what?
items := schema.Items
if items != nil {
a := items.GetSchemaOrReference()
if a[0].GetReference().GetXRef() != "" {
return FieldKind_ARRAY, typeForRef(a[0].GetReference().GetXRef()), format
} else if a[0].GetSchema().Type == "string" {
return FieldKind_ARRAY, "string", format
} else if a[0].GetSchema().Type == "object" {
return FieldKind_ARRAY, "interface{}", format
}
}
}
case "object":
if schema.AdditionalProperties == nil {
return FieldKind_MAP, "object", format
}
default:
}
}
if schema.AdditionalProperties != nil {
additionalProperties := schema.AdditionalProperties
if propertySchema := additionalProperties.GetSchemaOrReference().GetReference(); propertySchema != nil {
if ref := propertySchema.XRef; ref != "" {
return FieldKind_MAP, "map[string]" + typeForRef(ref), ""
}
}
}
// this function is incomplete... return a string representing anything that we don't handle
log.Printf("unimplemented: %v", schema)
return FieldKind_SCALAR, fmt.Sprintf("unimplemented: %v", schema), ""
}