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 main
16
17import (
18	"log"
19	"net/url"
20
21	discovery "github.com/googleapis/gnostic/discovery"
22	openapi2 "github.com/googleapis/gnostic/OpenAPIv2"
23)
24
25func addOpenAPI2SchemaForSchema(d *openapi2.Document, name string, schema *discovery.Schema) {
26	//log.Printf("SCHEMA %s\n", name)
27	d.Definitions.AdditionalProperties = append(d.Definitions.AdditionalProperties,
28		&openapi2.NamedSchema{
29			Name:  name,
30			Value: buildOpenAPI2SchemaForSchema(schema),
31		})
32}
33
34func buildOpenAPI2SchemaForSchema(schema *discovery.Schema) *openapi2.Schema {
35	s := &openapi2.Schema{}
36
37	if description := schema.Description; description != "" {
38		s.Description = description
39	}
40	if typeName := schema.Type; typeName != "" {
41		s.Type = &openapi2.TypeItem{[]string{typeName}}
42	}
43	if ref := schema.XRef; ref != "" {
44		s.XRef = "#/definitions/" + ref
45	}
46	if len(schema.Enum) > 0 {
47		for _, e := range schema.Enum {
48			s.Enum = append(s.Enum, &openapi2.Any{Yaml: e})
49		}
50	}
51	if schema.Items != nil {
52		s2 := buildOpenAPI2SchemaForSchema(schema.Items)
53		s.Items = &openapi2.ItemsItem{}
54		s.Items.Schema = append(s.Items.Schema, s2)
55	}
56	if schema.Properties != nil {
57		if len(schema.Properties.AdditionalProperties) > 0 {
58			s.Properties = &openapi2.Properties{}
59			for _, pair := range schema.Properties.AdditionalProperties {
60				s.Properties.AdditionalProperties = append(s.Properties.AdditionalProperties,
61					&openapi2.NamedSchema{
62						Name:  pair.Name,
63						Value: buildOpenAPI2SchemaForSchema(pair.Value),
64					},
65				)
66			}
67		}
68	}
69	// assume that all schemas are closed
70	s.AdditionalProperties = &openapi2.AdditionalPropertiesItem{Oneof: &openapi2.AdditionalPropertiesItem_Boolean{Boolean: false}}
71	return s
72}
73
74func buildOpenAPI2ParameterForParameter(name string, p *discovery.Parameter) *openapi2.Parameter {
75	//log.Printf("- PARAMETER %+v\n", p.Name)
76	typeName := p.Type
77	format := p.Format
78	location := p.Location
79	switch location {
80	case "query":
81		return &openapi2.Parameter{
82			Oneof: &openapi2.Parameter_NonBodyParameter{
83				NonBodyParameter: &openapi2.NonBodyParameter{
84					Oneof: &openapi2.NonBodyParameter_QueryParameterSubSchema{
85						QueryParameterSubSchema: &openapi2.QueryParameterSubSchema{
86							Name:        name,
87							In:          "query",
88							Description: p.Description,
89							Required:    p.Required,
90							Type:        typeName,
91							Format:      format,
92						},
93					},
94				},
95			},
96		}
97	case "path":
98		return &openapi2.Parameter{
99			Oneof: &openapi2.Parameter_NonBodyParameter{
100				NonBodyParameter: &openapi2.NonBodyParameter{
101					Oneof: &openapi2.NonBodyParameter_PathParameterSubSchema{
102						PathParameterSubSchema: &openapi2.PathParameterSubSchema{
103							Name:        name,
104							In:          "path",
105							Description: p.Description,
106							Required:    p.Required,
107							Type:        typeName,
108							Format:      format,
109						},
110					},
111				},
112			},
113		}
114	default:
115		return nil
116	}
117}
118
119func buildOpenAPI2ParameterForRequest(p *discovery.Request) *openapi2.Parameter {
120	return &openapi2.Parameter{
121		Oneof: &openapi2.Parameter_BodyParameter{
122			BodyParameter: &openapi2.BodyParameter{
123				Name:        "resource",
124				In:          "body",
125				Description: "",
126				Schema:      &openapi2.Schema{XRef: "#/definitions/" + p.XRef},
127			},
128		},
129	}
130}
131
132func buildOpenAPI2ResponseForResponse(response *discovery.Response) *openapi2.Response {
133	//log.Printf("- RESPONSE %+v\n", schema)
134	if response == nil {
135		return &openapi2.Response{
136			Description: "Successful operation",
137		}
138	}
139	ref := response.XRef
140	if ref == "" {
141		log.Printf("WARNING: Unhandled response %+v", response)
142	}
143	return &openapi2.Response{
144		Description: "Successful operation",
145		Schema: &openapi2.SchemaItem{
146			Oneof: &openapi2.SchemaItem_Schema{
147				Schema: &openapi2.Schema{
148					XRef: "#/definitions/" + ref,
149				},
150			},
151		},
152	}
153}
154
155func buildOpenAPI2OperationForMethod(method *discovery.Method) *openapi2.Operation {
156	//log.Printf("METHOD %s %s %s %s\n", method.Name, method.path(), method.HTTPMethod, method.ID)
157	//log.Printf("MAP %+v\n", method.JSONMap)
158	parameters := make([]*openapi2.ParametersItem, 0)
159	if method.Parameters != nil {
160		for _, pair := range method.Parameters.AdditionalProperties {
161			parameters = append(parameters, &openapi2.ParametersItem{
162				Oneof: &openapi2.ParametersItem_Parameter{
163					Parameter: buildOpenAPI2ParameterForParameter(pair.Name, pair.Value),
164				},
165			})
166		}
167	}
168	responses := &openapi2.Responses{
169		ResponseCode: []*openapi2.NamedResponseValue{
170			&openapi2.NamedResponseValue{
171				Name: "default",
172				Value: &openapi2.ResponseValue{
173					Oneof: &openapi2.ResponseValue_Response{
174						Response: buildOpenAPI2ResponseForResponse(method.Response),
175					},
176				},
177			},
178		},
179	}
180	if method.Request != nil {
181		parameter := buildOpenAPI2ParameterForRequest(method.Request)
182		parameters = append(parameters, &openapi2.ParametersItem{
183			Oneof: &openapi2.ParametersItem_Parameter{
184				Parameter: parameter,
185			},
186		})
187	}
188	return &openapi2.Operation{
189		Description: method.Description,
190		OperationId: method.Id,
191		Parameters:  parameters,
192		Responses:   responses,
193	}
194}
195
196func getOpenAPI2PathItemForPath(d *openapi2.Document, path string) *openapi2.PathItem {
197	// First, try to find a path item with the specified path. If it exists, return it.
198	for _, item := range d.Paths.Path {
199		if item.Name == path {
200			return item.Value
201		}
202	}
203	// Otherwise, create and return a new path item.
204	pathItem := &openapi2.PathItem{}
205	d.Paths.Path = append(d.Paths.Path,
206		&openapi2.NamedPathItem{
207			Name:  path,
208			Value: pathItem,
209		},
210	)
211	return pathItem
212}
213
214func addOpenAPI2PathsForMethod(d *openapi2.Document, name string, method *discovery.Method) {
215	operation := buildOpenAPI2OperationForMethod(method)
216	pathItem := getOpenAPI2PathItemForPath(d, pathForMethod(method.Path))
217	switch method.HttpMethod {
218	case "GET":
219		pathItem.Get = operation
220	case "POST":
221		pathItem.Post = operation
222	case "PUT":
223		pathItem.Put = operation
224	case "DELETE":
225		pathItem.Delete = operation
226	case "PATCH":
227		pathItem.Patch = operation
228	default:
229		log.Printf("WARNING: Unknown HTTP method %s", method.HttpMethod)
230	}
231}
232
233func addOpenAPI2PathsForResource(d *openapi2.Document, name string, resource *discovery.Resource) {
234	//log.Printf("RESOURCE %s (%s)\n", resource.Name, resource.FullName)
235	if resource.Methods != nil {
236		for _, pair := range resource.Methods.AdditionalProperties {
237			addOpenAPI2PathsForMethod(d, pair.Name, pair.Value)
238		}
239	}
240	if resource.Resources != nil {
241		for _, pair := range resource.Resources.AdditionalProperties {
242			addOpenAPI2PathsForResource(d, pair.Name, pair.Value)
243		}
244	}
245}
246
247func removeTrailingSlash(path string) string {
248	if len(path) > 1 && path[len(path)-1] == '/' {
249		return path[0: len(path)-1]
250	}
251	return path
252}
253
254// OpenAPIv2 returns an OpenAPI v2 representation of this Discovery document
255func OpenAPIv2(api *discovery.Document) (*openapi2.Document, error) {
256	d := &openapi2.Document{}
257	d.Swagger = "2.0"
258	d.Info = &openapi2.Info{
259		Title:       api.Title,
260		Version:     api.Version,
261		Description: api.Description,
262	}
263	url, _ := url.Parse(api.RootUrl)
264	d.Host = url.Host
265	d.BasePath = removeTrailingSlash(api.BasePath)
266	d.Schemes = []string{url.Scheme}
267	d.Consumes = []string{"application/json"}
268	d.Produces = []string{"application/json"}
269	d.Paths = &openapi2.Paths{}
270	d.Definitions = &openapi2.Definitions{}
271	if api.Schemas != nil {
272		for _, pair := range api.Schemas.AdditionalProperties {
273			addOpenAPI2SchemaForSchema(d, pair.Name, pair.Value)
274		}
275	}
276	if api.Methods != nil {
277		for _, pair := range api.Methods.AdditionalProperties {
278			addOpenAPI2PathsForMethod(d, pair.Name, pair.Value)
279		}
280	}
281	if api.Resources != nil {
282		for _, pair := range api.Resources.AdditionalProperties {
283			addOpenAPI2PathsForResource(d, pair.Name, pair.Value)
284		}
285	}
286	return d, nil
287}
288