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