package openapi2conv import ( "errors" "fmt" "net/url" "sort" "strings" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" ) // ToV3 converts an OpenAPIv2 spec to an OpenAPIv3 spec func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { doc3 := &openapi3.T{ OpenAPI: "3.0.3", Info: &doc2.Info, Components: &openapi3.Components{}, Tags: doc2.Tags, Extensions: stripNonExtensions(doc2.Extensions), ExternalDocs: doc2.ExternalDocs, } if host := doc2.Host; host != "" { if strings.Contains(host, "/") { err := fmt.Errorf("invalid host %q. This MUST be the host only and does not include the scheme nor sub-paths.", host) return nil, err } schemes := doc2.Schemes if len(schemes) == 0 { schemes = []string{"https"} } basePath := doc2.BasePath if basePath == "" { basePath = "/" } for _, scheme := range schemes { u := url.URL{ Scheme: scheme, Host: host, Path: basePath, } doc3.AddServer(&openapi3.Server{URL: u.String()}) } } doc3.Components.Schemas = make(map[string]*openapi3.SchemaRef) if parameters := doc2.Parameters; len(parameters) != 0 { doc3.Components.Parameters = make(map[string]*openapi3.ParameterRef) doc3.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(doc3.Components, parameter, doc2.Consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: doc3.Components.RequestBodies[k] = v3RequestBody case v3SchemaMap != nil: for _, v3Schema := range v3SchemaMap { doc3.Components.Schemas[k] = v3Schema } default: doc3.Components.Parameters[k] = v3Parameter } } } if paths := doc2.Paths; len(paths) != 0 { doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { r, err := ToV3PathItem(doc2, doc3.Components, pathItem, doc2.Consumes) if err != nil { return nil, err } doc3Paths[path] = r } doc3.Paths = doc3Paths } if responses := doc2.Responses; len(responses) != 0 { doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) for k, response := range responses { r, err := ToV3Response(response, doc2.Produces) if err != nil { return nil, err } doc3.Components.Responses[k] = r } } for key, schema := range ToV3Schemas(doc2.Definitions) { doc3.Components.Schemas[key] = schema } if m := doc2.SecurityDefinitions; len(m) != 0 { doc3SecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef) for k, v := range m { r, err := ToV3SecurityScheme(v) if err != nil { return nil, err } doc3SecuritySchemes[k] = r } doc3.Components.SecuritySchemes = doc3SecuritySchemes } doc3.Security = ToV3SecurityRequirements(doc2.Security) { sl := openapi3.NewLoader() if err := sl.ResolveRefsIn(doc3, nil); err != nil { return nil, err } } return doc3, nil } func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { doc3 := &openapi3.PathItem{ Extensions: stripNonExtensions(pathItem.Extensions), } for method, operation := range pathItem.Operations() { doc3Operation, err := ToV3Operation(doc2, components, pathItem, operation, consumes) if err != nil { return nil, err } doc3.SetOperation(method, doc3Operation) } for _, parameter := range pathItem.Parameters { v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter, consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: return nil, errors.New("pathItem must not have a body parameter") case v3Schema != nil: return nil, errors.New("pathItem must not have a schema parameter") default: doc3.Parameters = append(doc3.Parameters, v3Parameter) } } return doc3, nil } func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation, consumes []string) (*openapi3.Operation, error) { if operation == nil { return nil, nil } doc3 := &openapi3.Operation{ OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, Deprecated: operation.Deprecated, Tags: operation.Tags, Extensions: stripNonExtensions(operation.Extensions), } if v := operation.Security; v != nil { doc3Security := ToV3SecurityRequirements(*v) doc3.Security = &doc3Security } if len(operation.Consumes) > 0 { consumes = operation.Consumes } var reqBodies []*openapi3.RequestBodyRef formDataSchemas := make(map[string]*openapi3.SchemaRef) for _, parameter := range operation.Parameters { v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter, consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: reqBodies = append(reqBodies, v3RequestBody) case v3SchemaMap != nil: for key, v3Schema := range v3SchemaMap { formDataSchemas[key] = v3Schema } default: doc3.Parameters = append(doc3.Parameters, v3Parameter) } } var err error if doc3.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components, consumes); err != nil { return nil, err } if responses := operation.Responses; responses != nil { doc3Responses := make(openapi3.Responses, len(responses)) for k, response := range responses { doc3, err := ToV3Response(response, operation.Produces) if err != nil { return nil, err } doc3Responses[k] = doc3 } doc3.Responses = doc3Responses } return doc3, nil } func getParameterNameFromOldRef(ref string) string { cleanPath := strings.TrimPrefix(ref, "#/parameters/") pathSections := strings.SplitN(cleanPath, "/", 1) return pathSections[0] } func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter, consumes []string) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) { if ref := parameter.Ref; ref != "" { if strings.HasPrefix(ref, "#/parameters/") { name := getParameterNameFromOldRef(ref) if _, ok := components.RequestBodies[name]; ok { v3Ref := strings.Replace(ref, "#/parameters/", "#/components/requestBodies/", 1) return nil, &openapi3.RequestBodyRef{Ref: v3Ref}, nil, nil } else if schema, ok := components.Schemas[name]; ok { schemaRefMap := make(map[string]*openapi3.SchemaRef) if val, ok := schema.Value.Extensions["x-formData-name"]; ok { name = val.(string) } v3Ref := strings.Replace(ref, "#/parameters/", "#/components/schemas/", 1) schemaRefMap[name] = &openapi3.SchemaRef{Ref: v3Ref} return nil, nil, schemaRefMap, nil } } return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil } switch parameter.In { case "body": result := &openapi3.RequestBody{ Description: parameter.Description, Required: parameter.Required, Extensions: stripNonExtensions(parameter.Extensions), } if parameter.Name != "" { if result.Extensions == nil { result.Extensions = make(map[string]interface{}, 1) } result.Extensions["x-originalParamName"] = parameter.Name } if schemaRef := parameter.Schema; schemaRef != nil { result.WithSchemaRef(ToV3SchemaRef(schemaRef), consumes) } return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil case "formData": format, typ := parameter.Format, parameter.Type if typ == "file" { format, typ = "binary", "string" } if parameter.Extensions == nil { parameter.Extensions = make(map[string]interface{}, 1) } parameter.Extensions["x-formData-name"] = parameter.Name var required []string if parameter.Required { required = []string{parameter.Name} } schemaRef := &openapi3.SchemaRef{Value: &openapi3.Schema{ Description: parameter.Description, Type: typ, Extensions: stripNonExtensions(parameter.Extensions), Format: format, Enum: parameter.Enum, Min: parameter.Minimum, Max: parameter.Maximum, ExclusiveMin: parameter.ExclusiveMin, ExclusiveMax: parameter.ExclusiveMax, MinLength: parameter.MinLength, MaxLength: parameter.MaxLength, Default: parameter.Default, Items: parameter.Items, MinItems: parameter.MinItems, MaxItems: parameter.MaxItems, Pattern: parameter.Pattern, AllowEmptyValue: parameter.AllowEmptyValue, UniqueItems: parameter.UniqueItems, MultipleOf: parameter.MultipleOf, Required: required, }} schemaRefMap := make(map[string]*openapi3.SchemaRef, 1) schemaRefMap[parameter.Name] = schemaRef return nil, nil, schemaRefMap, nil default: required := parameter.Required if parameter.In == openapi3.ParameterInPath { required = true } var schemaRefRef string if schemaRef := parameter.Schema; schemaRef != nil && schemaRef.Ref != "" { schemaRefRef = schemaRef.Ref } result := &openapi3.Parameter{ In: parameter.In, Name: parameter.Name, Description: parameter.Description, Required: required, Extensions: stripNonExtensions(parameter.Extensions), Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ Type: parameter.Type, Format: parameter.Format, Enum: parameter.Enum, Min: parameter.Minimum, Max: parameter.Maximum, ExclusiveMin: parameter.ExclusiveMin, ExclusiveMax: parameter.ExclusiveMax, MinLength: parameter.MinLength, MaxLength: parameter.MaxLength, Default: parameter.Default, Items: parameter.Items, MinItems: parameter.MinItems, MaxItems: parameter.MaxItems, Pattern: parameter.Pattern, AllowEmptyValue: parameter.AllowEmptyValue, UniqueItems: parameter.UniqueItems, MultipleOf: parameter.MultipleOf, }, Ref: schemaRefRef, }), } return &openapi3.ParameterRef{Value: result}, nil, nil, nil } } func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, consumes []string) *openapi3.RequestBodyRef { if len(bodies) != len(reqs) { panic(`request bodies and them being required must match`) } requireds := make([]string, 0, len(reqs)) for propName, req := range reqs { if _, ok := bodies[propName]; !ok { panic(`request bodies and them being required must match`) } if req { requireds = append(requireds, propName) } } schema := &openapi3.Schema{ Type: "object", Properties: ToV3Schemas(bodies), Required: requireds, } return &openapi3.RequestBodyRef{ Value: openapi3.NewRequestBody().WithSchema(schema, consumes), } } func getParameterNameFromNewRef(ref string) string { cleanPath := strings.TrimPrefix(ref, "#/components/schemas/") pathSections := strings.SplitN(cleanPath, "/", 1) return pathSections[0] } func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components, consumes []string) (*openapi3.RequestBodyRef, error) { if len(bodies) > 1 { return nil, errors.New("multiple body parameters cannot exist for the same operation") } if len(bodies) != 0 && len(formDataSchemas) != 0 { return nil, errors.New("body and form parameters cannot exist together for the same operation") } for _, requestBodyRef := range bodies { return requestBodyRef, nil } if len(formDataSchemas) > 0 { formDataParams := make(map[string]*openapi3.SchemaRef, len(formDataSchemas)) formDataReqs := make(map[string]bool, len(formDataSchemas)) for formDataName, formDataSchema := range formDataSchemas { if formDataSchema.Ref != "" { name := getParameterNameFromNewRef(formDataSchema.Ref) if schema := components.Schemas[name]; schema != nil && schema.Value != nil { if tempName, ok := schema.Value.Extensions["x-formData-name"]; ok { name = tempName.(string) } formDataParams[name] = formDataSchema formDataReqs[name] = false for _, req := range schema.Value.Required { if name == req { formDataReqs[name] = true } } } } else if formDataSchema.Value != nil { formDataParams[formDataName] = formDataSchema formDataReqs[formDataName] = false for _, req := range formDataSchema.Value.Required { if formDataName == req { formDataReqs[formDataName] = true } } } } return formDataBody(formDataParams, formDataReqs, consumes), nil } return nil, nil } func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) { if ref := response.Ref; ref != "" { return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil } result := &openapi3.Response{ Description: &response.Description, Extensions: stripNonExtensions(response.Extensions), } // Default to "application/json" if "produces" is not specified. if len(produces) == 0 { produces = []string{"application/json"} } if schemaRef := response.Schema; schemaRef != nil { schema := ToV3SchemaRef(schemaRef) result.Content = make(openapi3.Content, len(produces)) for _, mime := range produces { result.Content[mime] = openapi3.NewMediaType().WithSchemaRef(schema) } } if headers := response.Headers; len(headers) > 0 { result.Headers = ToV3Headers(headers) } return &openapi3.ResponseRef{Value: result}, nil } func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers { headers := make(openapi3.Headers, len(defs)) for name, header := range defs { header.In = "" header.Name = "" if ref := header.Ref; ref != "" { headers[name] = &openapi3.HeaderRef{Ref: ToV3Ref(ref)} } else { parameter, _, _, _ := ToV3Parameter(nil, &header.Parameter, nil) headers[name] = &openapi3.HeaderRef{Value: &openapi3.Header{ Parameter: *parameter.Value, }} } } return headers } func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { schemas := make(map[string]*openapi3.SchemaRef, len(defs)) for name, schema := range defs { schemas[name] = ToV3SchemaRef(schema) } return schemas } func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { if ref := schema.Ref; ref != "" { return &openapi3.SchemaRef{Ref: ToV3Ref(ref)} } if schema.Value == nil { return schema } if schema.Value.Items != nil { schema.Value.Items = ToV3SchemaRef(schema.Value.Items) } for k, v := range schema.Value.Properties { schema.Value.Properties[k] = ToV3SchemaRef(v) } if v := schema.Value.AdditionalProperties.Schema; v != nil { schema.Value.AdditionalProperties.Schema = ToV3SchemaRef(v) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i] = ToV3SchemaRef(v) } if val, ok := schema.Value.Extensions["x-nullable"]; ok { schema.Value.Nullable, _ = val.(bool) delete(schema.Value.Extensions, "x-nullable") } return schema } var ref2To3 = map[string]string{ "#/definitions/": "#/components/schemas/", "#/responses/": "#/components/responses/", "#/parameters/": "#/components/parameters/", } func ToV3Ref(ref string) string { for old, new := range ref2To3 { if strings.HasPrefix(ref, old) { ref = strings.Replace(ref, old, new, 1) } } return ref } func FromV3Ref(ref string) string { for new, old := range ref2To3 { if strings.HasPrefix(ref, old) { ref = strings.Replace(ref, old, new, 1) } else if strings.HasPrefix(ref, "#/components/requestBodies/") { ref = strings.Replace(ref, "#/components/requestBodies/", "#/parameters/", 1) } } return ref } func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements { if requirements == nil { return nil } result := make(openapi3.SecurityRequirements, len(requirements)) for i, item := range requirements { result[i] = item } return result } func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.SecuritySchemeRef, error) { if securityScheme == nil { return nil, nil } result := &openapi3.SecurityScheme{ Description: securityScheme.Description, Extensions: stripNonExtensions(securityScheme.Extensions), } switch securityScheme.Type { case "basic": result.Type = "http" result.Scheme = "basic" case "apiKey": result.Type = "apiKey" result.In = securityScheme.In result.Name = securityScheme.Name case "oauth2": result.Type = "oauth2" flows := &openapi3.OAuthFlows{} result.Flows = flows scopesMap := make(map[string]string) for scope, desc := range securityScheme.Scopes { scopesMap[scope] = desc } flow := &openapi3.OAuthFlow{ AuthorizationURL: securityScheme.AuthorizationURL, TokenURL: securityScheme.TokenURL, Scopes: scopesMap, } switch securityScheme.Flow { case "implicit": flows.Implicit = flow case "accessCode": flows.AuthorizationCode = flow case "password": flows.Password = flow case "application": flows.ClientCredentials = flow default: return nil, fmt.Errorf("unsupported flow %q", securityScheme.Flow) } } return &openapi3.SecuritySchemeRef{ Ref: ToV3Ref(securityScheme.Ref), Value: result, }, nil } // FromV3 converts an OpenAPIv3 spec to an OpenAPIv2 spec func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { doc2Responses, err := FromV3Responses(doc3.Components.Responses, doc3.Components) if err != nil { return nil, err } schemas, parameters := FromV3Schemas(doc3.Components.Schemas, doc3.Components) doc2 := &openapi2.T{ Swagger: "2.0", Info: *doc3.Info, Definitions: schemas, Parameters: parameters, Responses: doc2Responses, Tags: doc3.Tags, Extensions: stripNonExtensions(doc3.Extensions), ExternalDocs: doc3.ExternalDocs, } isHTTPS := false isHTTP := false servers := doc3.Servers for i, server := range servers { parsedURL, err := url.Parse(server.URL) if err == nil { // See which schemes seem to be supported if parsedURL.Scheme == "https" { isHTTPS = true } else if parsedURL.Scheme == "http" { isHTTP = true } // The first server is assumed to provide the base path if i == 0 { doc2.Host = parsedURL.Host doc2.BasePath = parsedURL.Path } } } if isHTTPS { doc2.Schemes = append(doc2.Schemes, "https") } if isHTTP { doc2.Schemes = append(doc2.Schemes, "http") } for path, pathItem := range doc3.Paths { if pathItem == nil { continue } doc2.AddOperation(path, "GET", nil) addPathExtensions(doc2, path, stripNonExtensions(pathItem.Extensions)) for method, operation := range pathItem.Operations() { if operation == nil { continue } doc2Operation, err := FromV3Operation(doc3, operation) if err != nil { return nil, err } doc2.AddOperation(path, method, doc2Operation) } params := openapi2.Parameters{} for _, param := range pathItem.Parameters { p, err := FromV3Parameter(param, doc3.Components) if err != nil { return nil, err } params = append(params, p) } sort.Sort(params) doc2.Paths[path].Parameters = params } for name, param := range doc3.Components.Parameters { if doc2.Parameters[name], err = FromV3Parameter(param, doc3.Components); err != nil { return nil, err } } for name, requestBodyRef := range doc3.Components.RequestBodies { bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, doc3.Components) if err != nil { return nil, err } if len(formDataParameters) != 0 { for _, param := range formDataParameters { doc2.Parameters[param.Name] = param } } else if len(bodyOrRefParameters) != 0 { for _, param := range bodyOrRefParameters { doc2.Parameters[name] = param } } if len(consumes) != 0 { doc2.Consumes = consumesToArray(consumes) } } if m := doc3.Components.SecuritySchemes; m != nil { doc2SecuritySchemes := make(map[string]*openapi2.SecurityScheme) for id, securityScheme := range m { v, err := FromV3SecurityScheme(doc3, securityScheme) if err != nil { return nil, err } doc2SecuritySchemes[id] = v } doc2.SecurityDefinitions = doc2SecuritySchemes } doc2.Security = FromV3SecurityRequirements(doc3.Security) return doc2, nil } func consumesToArray(consumes map[string]struct{}) []string { consumesArr := make([]string, 0, len(consumes)) for key := range consumes { consumesArr = append(consumesArr, key) } sort.Strings(consumesArr) return consumesArr } func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, components *openapi3.Components) ( bodyOrRefParameters openapi2.Parameters, formParameters openapi2.Parameters, consumes map[string]struct{}, err error, ) { if ref := requestBodyRef.Ref; ref != "" { bodyOrRefParameters = append(bodyOrRefParameters, &openapi2.Parameter{Ref: FromV3Ref(ref)}) return } //Only select one formData or request body for an individual requestBody as OpenAPI 2 does not support multiples if requestBodyRef.Value != nil { for contentType, mediaType := range requestBodyRef.Value.Content { if consumes == nil { consumes = make(map[string]struct{}) } consumes[contentType] = struct{}{} if contentType == "application/x-www-form-urlencoded" || contentType == "multipart/form-data" { formParameters = FromV3RequestBodyFormData(mediaType) continue } paramName := name if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { paramName = originalName.(string) } var r *openapi2.Parameter if r, err = FromV3RequestBody(paramName, requestBodyRef, mediaType, components); err != nil { return } bodyOrRefParameters = append(bodyOrRefParameters, r) } } return } func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) { v2Defs := make(map[string]*openapi3.SchemaRef) v2Params := make(map[string]*openapi2.Parameter) for name, schema := range schemas { schemaConv, parameterConv := FromV3SchemaRef(schema, components) if schemaConv != nil { v2Defs[name] = schemaConv } else if parameterConv != nil { if parameterConv.Name == "" { parameterConv.Name = name } v2Params[name] = parameterConv } } return v2Defs, v2Params } func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) { if ref := schema.Ref; ref != "" { name := getParameterNameFromNewRef(ref) if val, ok := components.Schemas[name]; ok { if val.Value.Format == "binary" { v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1) return nil, &openapi2.Parameter{Ref: v2Ref} } } return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil } if schema.Value == nil { return schema, nil } if schema.Value != nil { if schema.Value.Type == "string" && schema.Value.Format == "binary" { paramType := "file" required := false value, _ := schema.Value.Extensions["x-formData-name"] originalName, _ := value.(string) for _, prop := range schema.Value.Required { if originalName == prop { required = true break } } return nil, &openapi2.Parameter{ In: "formData", Name: originalName, Description: schema.Value.Description, Type: paramType, Enum: schema.Value.Enum, Minimum: schema.Value.Min, Maximum: schema.Value.Max, ExclusiveMin: schema.Value.ExclusiveMin, ExclusiveMax: schema.Value.ExclusiveMax, MinLength: schema.Value.MinLength, MaxLength: schema.Value.MaxLength, Default: schema.Value.Default, Items: schema.Value.Items, MinItems: schema.Value.MinItems, MaxItems: schema.Value.MaxItems, AllowEmptyValue: schema.Value.AllowEmptyValue, UniqueItems: schema.Value.UniqueItems, MultipleOf: schema.Value.MultipleOf, Extensions: stripNonExtensions(schema.Value.Extensions), Required: required, } } } if v := schema.Value.Items; v != nil { schema.Value.Items, _ = FromV3SchemaRef(v, components) } keys := make([]string, 0, len(schema.Value.Properties)) for k := range schema.Value.Properties { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) } if v := schema.Value.AdditionalProperties.Schema; v != nil { schema.Value.AdditionalProperties.Schema, _ = FromV3SchemaRef(v, components) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) } if schema.Value.Nullable { schema.Value.Nullable = false schema.Value.Extensions["x-nullable"] = true } return schema, nil } func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements { if requirements == nil { return nil } result := make([]map[string][]string, 0, len(requirements)) for _, item := range requirements { result = append(result, item) } return result } func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { result := &openapi2.PathItem{ Extensions: stripNonExtensions(pathItem.Extensions), } for method, operation := range pathItem.Operations() { r, err := FromV3Operation(doc3, operation) if err != nil { return nil, err } result.SetOperation(method, r) } for _, parameter := range pathItem.Parameters { p, err := FromV3Parameter(parameter, doc3.Components) if err != nil { return nil, err } result.Parameters = append(result.Parameters, p) } return result, nil } func findNameForRequestBody(operation *openapi3.Operation) string { nameSearch: for _, name := range attemptedBodyParameterNames { for _, parameterRef := range operation.Parameters { parameter := parameterRef.Value if parameter != nil && parameter.Name == name { continue nameSearch } } return name } return "" } func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters { parameters := openapi2.Parameters{} for propName, schemaRef := range mediaType.Schema.Value.Properties { if ref := schemaRef.Ref; ref != "" { v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1) parameters = append(parameters, &openapi2.Parameter{Ref: v2Ref}) continue } val := schemaRef.Value typ := val.Type if val.Format == "binary" { typ = "file" } required := false for _, name := range val.Required { if name == propName { required = true break } } parameter := &openapi2.Parameter{ Name: propName, Description: val.Description, Type: typ, In: "formData", Extensions: stripNonExtensions(val.Extensions), Enum: val.Enum, ExclusiveMin: val.ExclusiveMin, ExclusiveMax: val.ExclusiveMax, MinLength: val.MinLength, MaxLength: val.MaxLength, Default: val.Default, Items: val.Items, MinItems: val.MinItems, MaxItems: val.MaxItems, Maximum: val.Max, Minimum: val.Min, Pattern: val.Pattern, // CollectionFormat: val.CollectionFormat, // Format: val.Format, AllowEmptyValue: val.AllowEmptyValue, Required: required, UniqueItems: val.UniqueItems, MultipleOf: val.MultipleOf, } parameters = append(parameters, parameter) } return parameters } func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2.Operation, error) { if operation == nil { return nil, nil } result := &openapi2.Operation{ OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, Deprecated: operation.Deprecated, Tags: operation.Tags, Extensions: stripNonExtensions(operation.Extensions), } if v := operation.Security; v != nil { resultSecurity := FromV3SecurityRequirements(*v) result.Security = &resultSecurity } for _, parameter := range operation.Parameters { r, err := FromV3Parameter(parameter, doc3.Components) if err != nil { return nil, err } result.Parameters = append(result.Parameters, r) } if v := operation.RequestBody; v != nil { // Find parameter name that we can use for the body name := findNameForRequestBody(operation) if name == "" { return nil, errors.New("could not find a name for request body") } bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, doc3.Components) if err != nil { return nil, err } if len(formDataParameters) != 0 { result.Parameters = append(result.Parameters, formDataParameters...) } else if len(bodyOrRefParameters) != 0 { for _, param := range bodyOrRefParameters { result.Parameters = append(result.Parameters, param) break // add a single request body } } if len(consumes) != 0 { result.Consumes = consumesToArray(consumes) } } sort.Sort(result.Parameters) if responses := operation.Responses; responses != nil { resultResponses, err := FromV3Responses(responses, doc3.Components) if err != nil { return nil, err } result.Responses = resultResponses } return result, nil } func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, mediaType *openapi3.MediaType, components *openapi3.Components) (*openapi2.Parameter, error) { requestBody := requestBodyRef.Value result := &openapi2.Parameter{ In: "body", Name: name, Description: requestBody.Description, Required: requestBody.Required, Extensions: stripNonExtensions(requestBody.Extensions), } if mediaType != nil { result.Schema, _ = FromV3SchemaRef(mediaType.Schema, components) } return result, nil } func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error) { if ref := ref.Ref; ref != "" { return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil } parameter := ref.Value if parameter == nil { return nil, nil } result := &openapi2.Parameter{ Description: parameter.Description, In: parameter.In, Name: parameter.Name, Required: parameter.Required, Extensions: stripNonExtensions(parameter.Extensions), } if schemaRef := parameter.Schema; schemaRef != nil { schemaRef, _ = FromV3SchemaRef(schemaRef, components) if ref := schemaRef.Ref; ref != "" { result.Schema = &openapi3.SchemaRef{Ref: FromV3Ref(ref)} return result, nil } schema := schemaRef.Value result.Type = schema.Type result.Format = schema.Format result.Enum = schema.Enum result.Minimum = schema.Min result.Maximum = schema.Max result.ExclusiveMin = schema.ExclusiveMin result.ExclusiveMax = schema.ExclusiveMax result.MinLength = schema.MinLength result.MaxLength = schema.MaxLength result.Pattern = schema.Pattern result.Default = schema.Default result.Items = schema.Items result.MinItems = schema.MinItems result.MaxItems = schema.MaxItems result.AllowEmptyValue = schema.AllowEmptyValue // result.CollectionFormat = schema.CollectionFormat result.UniqueItems = schema.UniqueItems result.MultipleOf = schema.MultipleOf } return result, nil } func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) { v2Responses := make(map[string]*openapi2.Response, len(responses)) for k, response := range responses { r, err := FromV3Response(response, components) if err != nil { return nil, err } v2Responses[k] = r } return v2Responses, nil } func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) { if ref := ref.Ref; ref != "" { return &openapi2.Response{Ref: FromV3Ref(ref)}, nil } response := ref.Value if response == nil { return nil, nil } description := "" if desc := response.Description; desc != nil { description = *desc } result := &openapi2.Response{ Description: description, Extensions: stripNonExtensions(response.Extensions), } if content := response.Content; content != nil { if ct := content["application/json"]; ct != nil { result.Schema, _ = FromV3SchemaRef(ct.Schema, components) } } if headers := response.Headers; len(headers) > 0 { var err error if result.Headers, err = FromV3Headers(headers, components); err != nil { return nil, err } } return result, nil } func FromV3Headers(defs openapi3.Headers, components *openapi3.Components) (map[string]*openapi2.Header, error) { headers := make(map[string]*openapi2.Header, len(defs)) for name, header := range defs { ref := openapi3.ParameterRef{Ref: header.Ref, Value: &header.Value.Parameter} parameter, err := FromV3Parameter(&ref, components) if err != nil { return nil, err } parameter.In = "" parameter.Name = "" headers[name] = &openapi2.Header{Parameter: *parameter} } return headers, nil } func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) { securityScheme := ref.Value if securityScheme == nil { return nil, nil } result := &openapi2.SecurityScheme{ Ref: FromV3Ref(ref.Ref), Description: securityScheme.Description, Extensions: stripNonExtensions(securityScheme.Extensions), } switch securityScheme.Type { case "http": switch securityScheme.Scheme { case "basic": result.Type = "basic" default: result.Type = "apiKey" result.In = "header" result.Name = "Authorization" } case "apiKey": result.Type = "apiKey" result.In = securityScheme.In result.Name = securityScheme.Name case "oauth2": result.Type = "oauth2" flows := securityScheme.Flows if flows != nil { var flow *openapi3.OAuthFlow // TODO: Is this the right priority? What if multiple defined? switch { case flows.Implicit != nil: result.Flow = "implicit" flow = flows.Implicit result.AuthorizationURL = flow.AuthorizationURL case flows.AuthorizationCode != nil: result.Flow = "accessCode" flow = flows.AuthorizationCode result.AuthorizationURL = flow.AuthorizationURL result.TokenURL = flow.TokenURL case flows.Password != nil: result.Flow = "password" flow = flows.Password result.TokenURL = flow.TokenURL case flows.ClientCredentials != nil: result.Flow = "application" flow = flows.ClientCredentials result.TokenURL = flow.TokenURL default: return nil, nil } result.Scopes = make(map[string]string, len(flow.Scopes)) for scope, desc := range flow.Scopes { result.Scopes[scope] = desc } } default: return nil, fmt.Errorf("unsupported security scheme type %q", securityScheme.Type) } return result, nil } var attemptedBodyParameterNames = []string{ "body", "requestBody", } // stripNonExtensions removes invalid extensions: those not prefixed by "x-" and returns them func stripNonExtensions(extensions map[string]interface{}) map[string]interface{} { for extName := range extensions { if !strings.HasPrefix(extName, "x-") { delete(extensions, extName) } } return extensions } func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]interface{}) { if doc2.Paths == nil { doc2.Paths = make(map[string]*openapi2.PathItem) } pathItem := doc2.Paths[path] if pathItem == nil { pathItem = &openapi2.PathItem{} doc2.Paths[path] = pathItem } pathItem.Extensions = extensions }