1// Copyright 2017 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package surface_v1 16 17import ( 18 openapiv2 "github.com/googleapis/gnostic/openapiv2" 19 "github.com/googleapis/gnostic/compiler" 20 "log" 21 "strconv" 22) 23 24type OpenAPI2Builder struct { 25 model *Model 26} 27 28// NewModelFromOpenAPI2 builds a model of an API service for use in code generation. 29func NewModelFromOpenAPI2(document *openapiv2.Document, sourceName string) (*Model, error) { 30 return newOpenAPI2Builder().buildModel(document, sourceName) 31} 32 33func newOpenAPI2Builder() *OpenAPI2Builder { 34 return &OpenAPI2Builder{model: &Model{}} 35} 36 37// Fills the surface model with information from a parsed OpenAPI description. The surface model provides that information 38// in a way that is more processable by plugins like gnostic-go-generator or gnostic-grpc. 39// Since OpenAPI schemas can be indefinitely nested, it is a recursive approach to build all Types and Methods. 40// The basic idea is that whenever we have "named OpenAPI object" (e.g.: NamedSchemaOrReference, NamedMediaType) we: 41// 1. Create a Type with that name 42// 2. Recursively execute according methods on child schemas (see buildFromSchema function) 43// 3. Return a FieldInfo object that describes how the created Type should be represented inside another Type as field. 44func (b *OpenAPI2Builder) buildModel(document *openapiv2.Document, sourceName string) (*Model, error) { 45 b.model.Types = make([]*Type, 0) 46 b.model.Methods = make([]*Method, 0) 47 // Set model properties from passed-in document. 48 b.model.Name = document.Info.Title 49 b.buildFromDocument(document) 50 err := b.buildSymbolicReferences(document, sourceName) 51 if err != nil { 52 log.Printf("Error while building symbolic references. This might cause the plugin to fail: %v", err) 53 } 54 return b.model, nil 55} 56 57// Builds Types from definitions; builds Types and Methods from paths 58func (b *OpenAPI2Builder) buildFromDocument(document *openapiv2.Document) { 59 b.buildFromDefinitions(document.Definitions) 60 b.buildFromParameterDefinitions(document.Parameters) 61 b.buildFromResponseDefinitions(document.Responses) 62 b.buildFromPaths(document.Paths) 63} 64 65// Build surface Types from OpenAPI definitions 66func (b *OpenAPI2Builder) buildFromDefinitions(definitions *openapiv2.Definitions) { 67 if definitions == nil { 68 return 69 } 70 71 if schemas := definitions.AdditionalProperties; schemas != nil { 72 for _, namedSchema := range schemas { 73 fInfo := b.buildFromSchemaOrReference(namedSchema.Name, namedSchema.Value) 74 // In certain cases no type will be created during the recursion: e.g.: the schema is of type scalar, array 75 // or an reference. So we check whether the surface model Type already exists, and if not then we create it. 76 if t := findType(b.model.Types, namedSchema.Name); t == nil { 77 t = makeType(namedSchema.Name) 78 makeFieldAndAppendToType(fInfo, t, "value") 79 b.model.addType(t) 80 } 81 } 82 } 83} 84 85// Build surface model Types from OpenAPI parameter definitions 86func (b *OpenAPI2Builder) buildFromParameterDefinitions(parameters *openapiv2.ParameterDefinitions) { 87 if parameters == nil { 88 return 89 } 90 91 for _, namedParameter := range parameters.AdditionalProperties { 92 // Parameters in OpenAPI have a name field. The name gets passed up the callstack and is therefore contained 93 // inside fInfo. That is why we pass "" as fieldName. A type with that parameter was never created, so we still 94 // need to do that. 95 t := makeType(namedParameter.Name) 96 fInfo := b.buildFromParam(namedParameter.Value) 97 makeFieldAndAppendToType(fInfo, t, "") 98 if len(t.Fields) > 0 { 99 b.model.addType(t) 100 } 101 } 102} 103 104// Build surface model Types from OpenAPI response definitions 105func (b *OpenAPI2Builder) buildFromResponseDefinitions(responses *openapiv2.ResponseDefinitions) { 106 if responses == nil { 107 return 108 } 109 for _, namedResponse := range responses.AdditionalProperties { 110 fInfo := b.buildFromResponse(namedResponse.Name, namedResponse.Value) 111 // In certain cases no type will be created during the recursion: e.g.: the schema is of type scalar, array 112 // or an reference. So we check whether the surface model Type already exists, and if not then we create it. 113 if t := findType(b.model.Types, namedResponse.Name); t == nil { 114 t = makeType(namedResponse.Name) 115 makeFieldAndAppendToType(fInfo, t, "value") 116 b.model.addType(t) 117 } 118 } 119} 120 121// Builds all symbolic references. A symbolic reference is an URL to another OpenAPI description. We call "document.ResolveReferences" 122// inside that method. This has the same effect like: "gnostic --resolve-refs" 123func (b *OpenAPI2Builder) buildSymbolicReferences(document *openapiv2.Document, sourceName string) (err error) { 124 cache := compiler.GetInfoCache() 125 if len(cache) == 0 && sourceName != "" { 126 // Fills the compiler cache with all kind of references. 127 _, err = document.ResolveReferences(sourceName) 128 if err != nil { 129 return err 130 } 131 cache = compiler.GetInfoCache() 132 } 133 134 for ref := range cache { 135 if isSymbolicReference(ref) { 136 b.model.SymbolicReferences = append(b.model.SymbolicReferences, ref) 137 } 138 } 139 // Clear compiler cache for recursive calls 140 compiler.ClearInfoCache() 141 return nil 142} 143 144// Build Method and Types (parameter, request bodies, responses) from all paths 145func (b *OpenAPI2Builder) buildFromPaths(paths *openapiv2.Paths) { 146 for _, path := range paths.Path { 147 b.buildFromNamedPath(path.Name, path.Value) 148 } 149} 150 151// Builds a Method and adds it to the surface model 152func (b *OpenAPI2Builder) buildFromNamedPath(name string, pathItem *openapiv2.PathItem) { 153 for _, method := range []string{"GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH"} { 154 var op *openapiv2.Operation 155 switch method { 156 case "GET": 157 op = pathItem.Get 158 case "PUT": 159 op = pathItem.Put 160 case "POST": 161 op = pathItem.Post 162 case "DELETE": 163 op = pathItem.Delete 164 case "OPTIONS": 165 op = pathItem.Options 166 case "HEAD": 167 op = pathItem.Head 168 case "PATCH": 169 op = pathItem.Patch 170 } 171 if op != nil { 172 m := &Method{ 173 Operation: op.OperationId, 174 Path: name, 175 Method: method, 176 Name: sanitizeOperationName(op.OperationId), 177 Description: op.Description, 178 } 179 if m.Name == "" { 180 m.Name = generateOperationName(method, name) 181 } 182 m.ParametersTypeName, m.ResponsesTypeName = b.buildFromNamedOperation(m.Name, op) 183 b.model.addMethod(m) 184 } 185 } 186} 187 188// Builds the "Parameters" and "Responses" types for an operation, adds them to the model, and returns the names of the types. 189// If no such Type is added to the model an empty string is returned. 190func (b *OpenAPI2Builder) buildFromNamedOperation(name string, operation *openapiv2.Operation) (parametersTypeName string, responseTypeName string) { 191 // At first, we build the operations input parameters. This includes parameters (like PATH or QUERY parameters). 192 operationParameters := makeType(name + "Parameters") 193 operationParameters.Description = operationParameters.Name + " holds parameters to " + name 194 for _, paramOrRef := range operation.Parameters { 195 fieldInfo := b.buildFromParamOrRef(paramOrRef) 196 // For parameters the name of the field is contained inside fieldInfo. That is why we pass "" as fieldName 197 makeFieldAndAppendToType(fieldInfo, operationParameters, "") 198 } 199 if len(operationParameters.Fields) > 0 { 200 b.model.addType(operationParameters) 201 parametersTypeName = operationParameters.Name 202 } 203 204 // Secondly, we build the response values for the method. 205 if responses := operation.Responses; responses != nil { 206 operationResponses := makeType(name + "Responses") 207 operationResponses.Description = operationResponses.Name + " holds responses of " + name 208 for _, namedResponse := range responses.ResponseCode { 209 fieldInfo := b.buildFromResponseOrRef(operation.OperationId+convertStatusCodeToText(namedResponse.Name), namedResponse.Value) 210 makeFieldAndAppendToType(fieldInfo, operationResponses, namedResponse.Name) 211 } 212 if len(operationResponses.Fields) > 0 { 213 b.model.addType(operationResponses) 214 responseTypeName = operationResponses.Name 215 } 216 } 217 return parametersTypeName, responseTypeName 218} 219 220// A helper method to differentiate between references and actual objects. 221// The actual Field and Type are created in the functions which call this function 222func (b *OpenAPI2Builder) buildFromParamOrRef(paramOrRef *openapiv2.ParametersItem) (fInfo *FieldInfo) { 223 fInfo = &FieldInfo{} 224 if param := paramOrRef.GetParameter(); param != nil { 225 fInfo = b.buildFromParam(param) 226 return fInfo 227 } else if ref := paramOrRef.GetJsonReference(); ref != nil { 228 t := findType(b.model.Types, validTypeForRef(ref.XRef)) 229 if t != nil && len(t.Fields) > 0 { 230 fInfo.fieldKind, fInfo.fieldType, fInfo.fieldName, fInfo.fieldPosition = FieldKind_REFERENCE, validTypeForRef(ref.XRef), t.Name, t.Fields[0].Position 231 return fInfo 232 } 233 // TODO: This might happen for symbolic references --> fInfo.Position defaults to 'BODY' which is wrong. 234 log.Printf("Not able to find parameter information for: %v", ref) 235 fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(ref.XRef) 236 return fInfo // Lets return fInfo for now otherwise we may get null pointer exception 237 } 238 return nil 239} 240 241// Returns information on how to represent 'parameter' as field. This information gets propagated up the callstack. 242// We have to differentiate between 'body' and 'non-body' parameters 243func (b *OpenAPI2Builder) buildFromParam(parameter *openapiv2.Parameter) (fInfo *FieldInfo) { 244 if bodyParam := parameter.GetBodyParameter(); bodyParam != nil { 245 fInfo = b.buildFromSchemaOrReference(bodyParam.Name, bodyParam.Schema) 246 if fInfo != nil { 247 fInfo.fieldName, fInfo.fieldPosition = bodyParam.Name, Position_BODY 248 return fInfo 249 } 250 } else if nonBodyParam := parameter.GetNonBodyParameter(); nonBodyParam != nil { 251 fInfo = b.buildFromNonBodyParameter(nonBodyParam) 252 return fInfo 253 } 254 log.Printf("Couldn't build from parameter: %v", parameter) 255 return nil 256} 257 258// Differentiates between different kind of non-body parameters 259func (b *OpenAPI2Builder) buildFromNonBodyParameter(nonBodyParameter *openapiv2.NonBodyParameter) (fInfo *FieldInfo) { 260 fInfo = &FieldInfo{} 261 headerParameter := nonBodyParameter.GetHeaderParameterSubSchema() 262 if headerParameter != nil { 263 fInfo.fieldName, fInfo.fieldPosition, fInfo.fieldFormat = headerParameter.Name, Position_HEADER, headerParameter.Format 264 b.adaptFieldKindAndFieldType(fInfo, headerParameter.Type, headerParameter.Items) 265 } 266 formDataParameter := nonBodyParameter.GetFormDataParameterSubSchema() 267 if formDataParameter != nil { 268 fInfo.fieldName, fInfo.fieldPosition, fInfo.fieldFormat = formDataParameter.Name, Position_FORMDATA, formDataParameter.Format 269 b.adaptFieldKindAndFieldType(fInfo, formDataParameter.Type, formDataParameter.Items) 270 } 271 queryParameter := nonBodyParameter.GetQueryParameterSubSchema() 272 if queryParameter != nil { 273 fInfo.fieldName, fInfo.fieldPosition, fInfo.fieldFormat = queryParameter.Name, Position_QUERY, queryParameter.Format 274 b.adaptFieldKindAndFieldType(fInfo, queryParameter.Type, queryParameter.Items) 275 } 276 pathParameter := nonBodyParameter.GetPathParameterSubSchema() 277 if pathParameter != nil { 278 fInfo.fieldName, fInfo.fieldPosition, fInfo.fieldFormat = pathParameter.Name, Position_PATH, pathParameter.Format 279 b.adaptFieldKindAndFieldType(fInfo, pathParameter.Type, pathParameter.Items) 280 } 281 return fInfo 282} 283 284// Changes the fieldKind and fieldType inside of 'fInfo' based on different conditions. In case of an array we have to 285// consider that it consists of indefinitely nested items. 286func (b *OpenAPI2Builder) adaptFieldKindAndFieldType(fInfo *FieldInfo, parameterType string, parameterItems *openapiv2.PrimitivesItems) { 287 fInfo.fieldKind, fInfo.fieldType = FieldKind_SCALAR, parameterType 288 289 if parameterType == "array" && parameterItems != nil { 290 fInfo.fieldKind, fInfo.fieldType = FieldKind_ARRAY, "string" // Default to string in case we don't find the type 291 if parameterItems.Type != "" { 292 // We only need the fieldType here because we know for sure that it is an array. 293 fInfo.fieldType = b.buildFromPrimitiveItems(fInfo.fieldName, parameterItems, 0).fieldType 294 } 295 } 296 297 if parameterType == "file" { 298 fInfo.fieldKind, fInfo.fieldType = FieldKind_SCALAR, "string" 299 } 300} 301 302// A recursive method that build Types for nested PrimitiveItems. The 'ctr' is used for naming the different Types. 303// The base condition is if we have scalar value (not an array). 304func (b *OpenAPI2Builder) buildFromPrimitiveItems(name string, items *openapiv2.PrimitivesItems, ctr int) (fInfo *FieldInfo) { 305 fInfo = &FieldInfo{} 306 switch items.Type { 307 case "array": 308 t := makeType(name) 309 fieldInfo := b.buildFromPrimitiveItems(name+strconv.Itoa(ctr), items.Items, ctr+1) 310 makeFieldAndAppendToType(fieldInfo, t, "items") 311 312 if len(t.Fields) > 0 { 313 b.model.addType(t) 314 fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, t.Name 315 return fInfo 316 } 317 default: 318 // We got a scalar value 319 fInfo.fieldKind, fInfo.fieldType, fInfo.fieldFormat = FieldKind_SCALAR, items.Type, items.Format 320 return fInfo 321 } 322 return nil 323} 324 325// A helper method to differentiate between references and actual objects 326func (b *OpenAPI2Builder) buildFromResponseOrRef(name string, responseOrRef *openapiv2.ResponseValue) (fInfo *FieldInfo) { 327 fInfo = &FieldInfo{} 328 if response := responseOrRef.GetResponse(); response != nil { 329 fInfo = b.buildFromResponse(name, response) 330 return fInfo 331 } else if ref := responseOrRef.GetJsonReference(); ref != nil { 332 fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(ref.XRef) 333 return fInfo 334 } 335 return nil 336} 337 338// A helper method to propagate the information up the call stack 339func (b *OpenAPI2Builder) buildFromResponse(name string, response *openapiv2.Response) (fInfo *FieldInfo) { 340 if response.Schema != nil && response.Schema.GetSchema() != nil { 341 fInfo = b.buildFromSchemaOrReference(name, response.Schema.GetSchema()) 342 return fInfo 343 } 344 return nil 345} 346 347// A helper method to differentiate between references and actual objects 348func (b *OpenAPI2Builder) buildFromSchemaOrReference(name string, schema *openapiv2.Schema) (fInfo *FieldInfo) { 349 fInfo = &FieldInfo{} 350 if schema.XRef != "" { 351 fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, validTypeForRef(schema.XRef) 352 return fInfo 353 } else { 354 fInfo = b.buildFromSchema(name, schema) 355 return fInfo 356 } 357} 358 359// Given an OpenAPI schema there are two possibilities: 360// 1. The schema is an object/array: We create a type for the object, recursively call according methods for child 361// schemas, and then return information on how to use the created Type as field. 362// 2. The schema has a scalar type: We return information on how to represent a scalar schema as Field. Fields are 363// created whenever Types are created (higher up in the callstack). This possibility can be considered as the "base condition" 364// for the recursive approach. 365func (b *OpenAPI2Builder) buildFromSchema(name string, schema *openapiv2.Schema) (fInfo *FieldInfo) { 366 fInfo = &FieldInfo{} 367 368 t := "" 369 if schema.Type != nil && len(schema.Type.Value) == 1 && schema.Type.Value[0] != "null" { 370 t = schema.Type.Value[0] 371 } 372 switch t { 373 case "": 374 fallthrough 375 case "object": 376 schemaType := makeType(name) 377 if schema.Properties != nil && schema.Properties.AdditionalProperties != nil { 378 for _, namedSchema := range schema.Properties.AdditionalProperties { 379 fieldInfo := b.buildFromSchemaOrReference(namedSchema.Name, namedSchema.Value) 380 makeFieldAndAppendToType(fieldInfo, schemaType, namedSchema.Name) 381 } 382 } 383 if schema := schema.AdditionalProperties.GetSchema(); schema != nil { 384 // AdditionalProperties are represented as map 385 fieldInfo := b.buildFromSchemaOrReference(name+"AdditionalProperties", schema) 386 if fieldInfo != nil { 387 mapValueType := determineMapValueType(*fieldInfo) 388 fieldInfo.fieldKind, fieldInfo.fieldType, fieldInfo.fieldFormat = FieldKind_MAP, "map[string]"+mapValueType, "" 389 makeFieldAndAppendToType(fieldInfo, schemaType, "additional_properties") 390 } 391 } 392 393 for idx, schemaOrRef := range schema.AllOf { 394 fieldInfo := b.buildFromSchemaOrReference(name+"AllOf"+strconv.Itoa(idx+1), schemaOrRef) 395 makeFieldAndAppendToType(fieldInfo, schemaType, "all_of_"+strconv.Itoa(idx+1)) 396 } 397 398 if schema.Items != nil { 399 for idx, schema := range schema.Items.Schema { 400 fieldInfo := b.buildFromSchemaOrReference(name+"Items"+strconv.Itoa(idx+1), schema) 401 makeFieldAndAppendToType(fieldInfo, schemaType, "items_"+strconv.Itoa(idx+1)) 402 } 403 } 404 405 if schema.Enum != nil { 406 // TODO: It is not defined how enums should be represented inside the surface model 407 fieldInfo := &FieldInfo{} 408 fieldInfo.fieldKind, fieldInfo.fieldType, fieldInfo.fieldName = FieldKind_ANY, "string", "enum" 409 makeFieldAndAppendToType(fieldInfo, schemaType, fieldInfo.fieldName) 410 } 411 412 if len(schemaType.Fields) == 0 { 413 schemaType.Kind = TypeKind_OBJECT 414 schemaType.ContentType = "interface{}" 415 } 416 b.model.addType(schemaType) 417 fInfo.fieldKind, fInfo.fieldType = FieldKind_REFERENCE, schemaType.Name 418 return fInfo 419 case "array": 420 // Same as for OpenAPI v3. I believe this is a bug: schema.Items.Schema should not be an array 421 // but rather a single object describing the values of the array. Printing 'len(schema.Items.Schema)' 422 // for 2000+ API descriptions from API-guru always resulted with an array of length of 1. 423 for _, s := range schema.Items.Schema { 424 arrayFieldInfo := b.buildFromSchemaOrReference(name, s) 425 if arrayFieldInfo != nil { 426 fInfo.fieldKind, fInfo.fieldType, fInfo.fieldFormat = FieldKind_ARRAY, arrayFieldInfo.fieldType, arrayFieldInfo.fieldFormat 427 return fInfo 428 } 429 } 430 default: 431 // We got a scalar value 432 fInfo.fieldKind, fInfo.fieldType, fInfo.fieldFormat = FieldKind_SCALAR, t, schema.Format 433 return fInfo 434 } 435 log.Printf("Unimplemented: could not find field info for schema with name: '%v' and properties: %v", name, schema) 436 return nil 437} 438