1// Copyright 2019 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 conversions
16
17import (
18	"log"
19	"net/url"
20	"strings"
21
22	openapi3 "github.com/googleapis/gnostic/openapiv3"
23	discovery "github.com/googleapis/gnostic/discovery"
24)
25
26func pathForMethod(path string) string {
27	return "/" + strings.Replace(path, "{+", "{", -1)
28}
29
30func addOpenAPI3SchemaForSchema(d *openapi3.Document, name string, schema *discovery.Schema) {
31	d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties,
32		&openapi3.NamedSchemaOrReference{
33			Name:  name,
34			Value: buildOpenAPI3SchemaOrReferenceForSchema(schema),
35		})
36}
37
38func buildOpenAPI3SchemaOrReferenceForSchema(schema *discovery.Schema) *openapi3.SchemaOrReference {
39	if ref := schema.XRef; ref != "" {
40		return &openapi3.SchemaOrReference{
41			Oneof: &openapi3.SchemaOrReference_Reference{
42				Reference: &openapi3.Reference{
43					XRef: "#/definitions/" + ref,
44				},
45			},
46		}
47	}
48
49	s := &openapi3.Schema{}
50
51	if description := schema.Description; description != "" {
52		s.Description = description
53	}
54	if typeName := schema.Type; typeName != "" {
55		s.Type = typeName
56	}
57	if len(schema.Enum) > 0 {
58		for _, e := range schema.Enum {
59			s.Enum = append(s.Enum, &openapi3.Any{Yaml: e})
60		}
61	}
62	if schema.Items != nil {
63		s.Items = &openapi3.ItemsItem{
64			SchemaOrReference: []*openapi3.SchemaOrReference{buildOpenAPI3SchemaOrReferenceForSchema(schema.Items)},
65		}
66	}
67	if (schema.Properties != nil) && (len(schema.Properties.AdditionalProperties) > 0) {
68		s.Properties = &openapi3.Properties{}
69		for _, pair := range schema.Properties.AdditionalProperties {
70			s.Properties.AdditionalProperties = append(s.Properties.AdditionalProperties,
71				&openapi3.NamedSchemaOrReference{
72					Name:  pair.Name,
73					Value: buildOpenAPI3SchemaOrReferenceForSchema(pair.Value),
74				},
75			)
76		}
77	}
78	return &openapi3.SchemaOrReference{
79		Oneof: &openapi3.SchemaOrReference_Schema{
80			Schema: s,
81		},
82	}
83}
84
85func buildOpenAPI3ParameterForParameter(name string, p *discovery.Parameter) *openapi3.Parameter {
86	typeName := p.Type
87	format := p.Format
88	location := p.Location
89	switch location {
90	case "query", "path":
91		return &openapi3.Parameter{
92			Name:        name,
93			In:          location,
94			Description: p.Description,
95			Required:    p.Required,
96			Schema: &openapi3.SchemaOrReference{
97				Oneof: &openapi3.SchemaOrReference_Schema{
98					Schema: &openapi3.Schema{
99						Type:   typeName,
100						Format: format,
101					},
102				},
103			},
104		}
105	default:
106		return nil
107	}
108}
109
110func buildOpenAPI3RequestBodyForRequest(request *discovery.Request) *openapi3.RequestBody {
111	ref := request.XRef
112	if ref == "" {
113		log.Printf("WARNING: Unhandled request schema %+v", request)
114	}
115	return &openapi3.RequestBody{
116		Content: &openapi3.MediaTypes{
117			AdditionalProperties: []*openapi3.NamedMediaType{
118				&openapi3.NamedMediaType{
119					Name: "application/json",
120					Value: &openapi3.MediaType{
121						Schema: &openapi3.SchemaOrReference{
122							Oneof: &openapi3.SchemaOrReference_Reference{
123								Reference: &openapi3.Reference{
124									XRef: "#/definitions/" + ref,
125								},
126							},
127						},
128					},
129				},
130			},
131		},
132	}
133}
134
135func buildOpenAPI3ResponseForResponse(response *discovery.Response, hasDataWrapper bool) *openapi3.Response {
136	if response == nil {
137		return &openapi3.Response{
138			Description: "Successful operation",
139		}
140	} else {
141		ref := response.XRef
142		if ref == "" {
143			log.Printf("WARNING: Unhandled response %+v", response)
144		}
145		return &openapi3.Response{
146			Description: "Successful operation",
147			Content: &openapi3.MediaTypes{
148				AdditionalProperties: []*openapi3.NamedMediaType{
149					&openapi3.NamedMediaType{
150						Name: "application/json",
151						Value: &openapi3.MediaType{
152							Schema: &openapi3.SchemaOrReference{
153								Oneof: &openapi3.SchemaOrReference_Reference{
154									Reference: &openapi3.Reference{
155										XRef: "#/definitions/" + ref,
156									},
157								},
158							},
159						},
160					},
161				},
162			},
163		}
164	}
165}
166
167func buildOpenAPI3OperationForMethod(method *discovery.Method, hasDataWrapper bool) *openapi3.Operation {
168	if method == nil {
169		return nil
170	}
171	parameters := make([]*openapi3.ParameterOrReference, 0)
172	if method.Parameters != nil {
173		for _, pair := range method.Parameters.AdditionalProperties {
174			parameters = append(parameters, &openapi3.ParameterOrReference{
175				Oneof: &openapi3.ParameterOrReference_Parameter{
176					Parameter: buildOpenAPI3ParameterForParameter(pair.Name, pair.Value),
177				},
178			})
179		}
180	}
181	responses := &openapi3.Responses{
182		ResponseOrReference: []*openapi3.NamedResponseOrReference{
183			&openapi3.NamedResponseOrReference{
184				Name: "default",
185				Value: &openapi3.ResponseOrReference{
186					Oneof: &openapi3.ResponseOrReference_Response{
187						Response: buildOpenAPI3ResponseForResponse(method.Response, hasDataWrapper),
188					},
189				},
190			},
191		},
192	}
193	var requestBodyOrReference *openapi3.RequestBodyOrReference
194	if method.Request != nil {
195		requestBody := buildOpenAPI3RequestBodyForRequest(method.Request)
196		requestBodyOrReference = &openapi3.RequestBodyOrReference{
197			Oneof: &openapi3.RequestBodyOrReference_RequestBody{
198				RequestBody: requestBody,
199			},
200		}
201	}
202	return &openapi3.Operation{
203		Description: method.Description,
204		OperationId: method.Id,
205		Parameters:  parameters,
206		Responses:   responses,
207		RequestBody: requestBodyOrReference,
208	}
209}
210
211func getOpenAPI3PathItemForPath(d *openapi3.Document, path string) *openapi3.PathItem {
212	// First, try to find a path item with the specified path. If it exists, return it.
213	for _, item := range d.Paths.Path {
214		if item.Name == path {
215			return item.Value
216		}
217	}
218	// Otherwise, create and return a new path item.
219	pathItem := &openapi3.PathItem{}
220	d.Paths.Path = append(d.Paths.Path,
221		&openapi3.NamedPathItem{
222			Name:  path,
223			Value: pathItem,
224		},
225	)
226	return pathItem
227}
228
229func addOpenAPI3PathsForMethod(d *openapi3.Document, name string, method *discovery.Method, hasDataWrapper bool) {
230	operation := buildOpenAPI3OperationForMethod(method, hasDataWrapper)
231	pathItem := getOpenAPI3PathItemForPath(d, pathForMethod(method.Path))
232	switch method.HttpMethod {
233	case "GET":
234		pathItem.Get = operation
235	case "POST":
236		pathItem.Post = operation
237	case "PUT":
238		pathItem.Put = operation
239	case "DELETE":
240		pathItem.Delete = operation
241	case "PATCH":
242		pathItem.Patch = operation
243	default:
244		log.Printf("WARNING: Unknown HTTP method %s", method.HttpMethod)
245	}
246}
247
248func addOpenAPI3PathsForResource(d *openapi3.Document, resource *discovery.Resource, hasDataWrapper bool) {
249	if resource.Methods != nil {
250		for _, pair := range resource.Methods.AdditionalProperties {
251			addOpenAPI3PathsForMethod(d, pair.Name, pair.Value, hasDataWrapper)
252		}
253	}
254	if resource.Resources != nil {
255		for _, pair := range resource.Resources.AdditionalProperties {
256			addOpenAPI3PathsForResource(d, pair.Value, hasDataWrapper)
257		}
258	}
259}
260
261// OpenAPIv3 returns an OpenAPI v3 representation of a Discovery document
262func OpenAPIv3(api *discovery.Document) (*openapi3.Document, error) {
263	d := &openapi3.Document{}
264	d.Openapi = "3.0"
265	d.Info = &openapi3.Info{
266		Title:       api.Title,
267		Version:     api.Version,
268		Description: api.Description,
269	}
270	d.Servers = make([]*openapi3.Server, 0)
271
272	url, _ := url.Parse(api.RootUrl)
273	host := url.Host
274	basePath := api.BasePath
275	if basePath == "" {
276		basePath = "/"
277	}
278	d.Servers = append(d.Servers, &openapi3.Server{Url: "https://" + host + basePath})
279
280	hasDataWrapper := false
281	for _, feature := range api.Features {
282		if feature == "dataWrapper" {
283			hasDataWrapper = true
284		}
285	}
286
287	d.Components = &openapi3.Components{}
288	d.Components.Schemas = &openapi3.SchemasOrReferences{}
289	if api.Schemas != nil {
290		for _, pair := range api.Schemas.AdditionalProperties {
291			addOpenAPI3SchemaForSchema(d, pair.Name, pair.Value)
292		}
293	}
294
295	d.Paths = &openapi3.Paths{}
296	if api.Methods != nil {
297		for _, pair := range api.Methods.AdditionalProperties {
298			addOpenAPI3PathsForMethod(d, pair.Name, pair.Value, hasDataWrapper)
299		}
300	}
301	for _, pair := range api.Resources.AdditionalProperties {
302		addOpenAPI3PathsForResource(d, pair.Value, hasDataWrapper)
303	}
304
305	return d, nil
306}
307