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