Repository: getkin/kin-openapi (go)

Issue:
openapi2conv.ToV3Swagger() doesn't convert $refs.
After conversion, the resulting v3 structure's type refs are still in the form "#/definitions/" instead of "#/components/schemas/".

go.mod: github.com/getkin/kin-openapi v0.1.0



Pull request:
Rewrite refs to definitions and responses
This rewrites refs to definitions and responses and includes those members when creating a v2 spec from a v3 spec.

Fixes #75.

Patch:
diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go
index 792f693..86e7370 100644
--- a/openapi2/openapi2.go
+++ b/openapi2/openapi2.go
@@ -155,6 +155,9 @@ type Parameter struct {
 	MinLength    uint64              `json:"minLength,omitempty"`
 	MaxLength    *uint64             `json:"maxLength,omitempty"`
 	Pattern      string              `json:"pattern,omitempty"`
+	Items        *openapi3.SchemaRef `json:"items,omitempty"`
+	MinItems     uint64              `json:"minItems,omitempty"`
+	MaxItems     *uint64             `json:"maxItems,omitempty"`
 	Default      interface{}         `json:"default,omitempty"`
 }
 
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index 97a0e71..de69625 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"strings"
 
 	"github.com/getkin/kin-openapi/openapi2"
 	"github.com/getkin/kin-openapi/openapi3"
@@ -12,7 +13,7 @@ import (
 
 func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 	result := &openapi3.Swagger{
-		OpenAPI:    "3.0",
+		OpenAPI:    "3.0.2",
 		Info:       &swagger.Info,
 		Components: openapi3.Components{},
 		Tags:       swagger.Tags,
@@ -74,7 +75,7 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 			result.Components.Responses[k] = r
 		}
 	}
-	result.Components.Schemas = swagger.Definitions
+	result.Components.Schemas = ToV3Schemas(swagger.Definitions)
 	if m := swagger.SecurityDefinitions; m != nil {
 		resultSecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef)
 		for k, v := range m {
@@ -163,7 +164,7 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 		}
 		if schemaRef := parameter.Schema; schemaRef != nil {
 			// Assume it's JSON
-			result.WithJSONSchemaRef(schemaRef)
+			result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
 		}
 		return nil, &openapi3.RequestBodyRef{
 			Value: result,
@@ -174,10 +175,10 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 		Name:        parameter.Name,
 		Description: parameter.Description,
 		Required:    parameter.Required,
-		Schema:      parameter.Schema,
 	}
+
 	if parameter.Type != "" {
-		result.Schema = &openapi3.SchemaRef{
+		schema := &openapi3.SchemaRef{
 			Value: &openapi3.Schema{
 				Type:         parameter.Type,
 				Format:       parameter.Format,
@@ -189,8 +190,12 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 				MinLength:    parameter.MinLength,
 				MaxLength:    parameter.MaxLength,
 				Default:      parameter.Default,
+				Items:        parameter.Items,
+				MinItems:     parameter.MinItems,
+				MaxItems:     parameter.MaxItems,
 			},
 		}
+		result.Schema = ToV3SchemaRef(schema)
 	}
 	return &openapi3.ParameterRef{
 		Value: result,
@@ -200,20 +205,69 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
 	if ref := response.Ref; len(ref) > 0 {
 		return &openapi3.ResponseRef{
-			Ref: ref,
+			Ref: ToV3Ref(ref),
 		}, nil
 	}
 	result := &openapi3.Response{
 		Description: response.Description,
 	}
 	if schemaRef := response.Schema; schemaRef != nil {
-		result.WithJSONSchemaRef(schemaRef)
+		result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
 	}
 	return &openapi3.ResponseRef{
 		Value: result,
 	}, nil
 }
 
+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; len(ref) > 0 {
+		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)
+	}
+	return schema
+}
+
+var ref2To3 = map[string]string{
+	"#/definitions/": "#/components/schemas/",
+	"#/responses/":   "#/components/responses/",
+}
+
+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)
+		}
+	}
+	return ref
+}
+
 func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements {
 	if requirements == nil {
 		return nil
@@ -265,15 +319,21 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
 		}
 	}
 	return &openapi3.SecuritySchemeRef{
-		Ref:   securityScheme.Ref,
+		Ref:   ToV3Ref(securityScheme.Ref),
 		Value: result,
 	}, nil
 }
 
 func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
+	resultResponses, err := FromV3Responses(swagger.Components.Responses)
+	if err != nil {
+		return nil, err
+	}
 	result := &openapi2.Swagger{
-		Info: *swagger.Info,
-		Tags: swagger.Tags,
+		Info:        *swagger.Info,
+		Definitions: FromV3Schemas(swagger.Components.Schemas),
+		Responses:   resultResponses,
+		Tags:        swagger.Tags,
 	}
 	isHTTPS := false
 	isHTTP := false
@@ -330,6 +390,32 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 	return result, nil
 }
 
+func FromV3Schemas(schemas map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef {
+	v2Defs := make(map[string]*openapi3.SchemaRef, len(schemas))
+	for name, schema := range schemas {
+		v2Defs[name] = FromV3SchemaRef(schema)
+	}
+	return v2Defs
+}
+
+func FromV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
+	if ref := schema.Ref; len(ref) > 0 {
+		return &openapi3.SchemaRef{
+			Ref: FromV3Ref(ref),
+		}
+	}
+	if schema.Value == nil {
+		return schema
+	}
+	if schema.Value.Items != nil {
+		schema.Value.Items = FromV3SchemaRef((schema.Value.Items))
+	}
+	for k, v := range schema.Value.Properties {
+		schema.Value.Properties[k] = FromV3SchemaRef(v)
+	}
+	return schema
+}
+
 func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements {
 	if requirements == nil {
 		return nil
@@ -403,15 +489,11 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (
 		result.Parameters = append(result.Parameters, r)
 	}
 	if responses := operation.Responses; responses != nil {
-		resultResponses := make(map[string]*openapi2.Response, len(responses))
-		result.Responses = resultResponses
-		for k, response := range responses {
-			r, err := FromV3Response(response)
-			if err != nil {
-				return nil, err
-			}
-			resultResponses[k] = r
+		resultResponses, err := FromV3Responses(responses)
+		if err != nil {
+			return nil, err
 		}
+		result.Responses = resultResponses
 	}
 	return result, nil
 }
@@ -419,7 +501,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (
 func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) {
 	if ref := requestBodyRef.Ref; len(ref) > 0 {
 		return &openapi2.Parameter{
-			Ref: ref,
+			Ref: FromV3Ref(ref),
 		}, nil
 	}
 	requestBody := requestBodyRef.Value
@@ -449,7 +531,7 @@ func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation,
 func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
 	if v := ref.Ref; len(v) > 0 {
 		return &openapi2.Parameter{
-			Ref: v,
+			Ref: FromV3Ref(v),
 		}, nil
 	}
 	parameter := ref.Value
@@ -463,6 +545,7 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
 		Required:    parameter.Required,
 	}
 	if schemaRef := parameter.Schema; schemaRef != nil {
+		schemaRef = FromV3SchemaRef(schemaRef)
 		schema := schemaRef.Value
 		result.Type = schema.Type
 		result.Format = schema.Format
@@ -475,11 +558,32 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
 		result.MaxLength = schema.MaxLength
 		result.Pattern = schema.Pattern
 		result.Default = schema.Default
+		result.Items = schema.Items
+		result.MinItems = schema.MinItems
+		result.MaxItems = schema.MaxItems
 	}
 	return result, nil
 }
 
+func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*openapi2.Response, error) {
+	v2Responses := make(map[string]*openapi2.Response, len(responses))
+	for k, response := range responses {
+		r, err := FromV3Response(response)
+		if err != nil {
+			return nil, err
+		}
+		v2Responses[k] = r
+	}
+	return v2Responses, nil
+}
+
 func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
+	if v := ref.Ref; len(v) > 0 {
+		return &openapi2.Response{
+			Ref: FromV3Ref(v),
+		}, nil
+	}
+
 	response := ref.Value
 	if response == nil {
 		return nil, nil
@@ -489,7 +593,7 @@ func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
 	}
 	if content := response.Content; content != nil {
 		if ct := content["application/json"]; ct != nil {
-			result.Schema = ct.Schema
+			result.Schema = FromV3SchemaRef(ct.Schema)
 		}
 	}
 	return result, nil
@@ -501,7 +605,7 @@ func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchem
 		return nil, nil
 	}
 	result := &openapi2.SecurityScheme{
-		Ref:         ref.Ref,
+		Ref:         FromV3Ref(ref.Ref),
 		Description: securityScheme.Description,
 	}
 	switch securityScheme.Type {


Test patch:
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index 84a053c..3f7cf5a 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -54,6 +54,9 @@ const exampleV2 = `
           "default": {
             "description": "default response"
           },
+          "403": {
+            "$ref": "#/responses/ForbiddenError"
+          },
           "404": {
             "description": "404 response"
           }
@@ -80,6 +83,17 @@ const exampleV2 = `
             "maximum": 10000,
             "default": 250
           },
+          {
+            "in": "query",
+            "name": "bbox",
+            "description": "Only return results that intersect the provided bounding box.",
+            "maxItems": 4,
+            "minItems": 4,
+            "type": "array",
+            "items": {
+              "type": "number"
+            }
+          },
           {
             "in": "body",
             "name": "body",
@@ -87,6 +101,15 @@ const exampleV2 = `
           }
         ],
         "responses": {
+          "200": {
+            "description": "ok",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Item"
+              }
+            }
+          },
           "default": {
             "description": "default response"
           },
@@ -126,6 +149,36 @@ const exampleV2 = `
       }
     }
   },
+  "responses": {
+    "ForbiddenError": {
+      "description": "Insufficient permission to perform the requested action.",
+      "schema": {
+        "$ref": "#/definitions/Error"
+      }
+    }
+  },
+  "definitions": {
+    "Item": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      }
+    },
+    "Error": {
+      "description": "Error response.",
+      "type": "object",
+      "required": [
+        "message"
+      ],
+      "properties": {
+        "message": {
+          "type": "string"
+        }
+      }
+    }
+  },
   "security": [
     {
       "default_security_0": [
@@ -140,9 +193,44 @@ const exampleV2 = `
 
 const exampleV3 = `
 {
-  "openapi": "3.0",
+  "openapi": "3.0.2",
   "info": {"title":"MyAPI","version":"0.1"},
-  "components": {},
+  "components": {
+    "responses": {
+      "ForbiddenError": {
+        "content": {
+          "application/json": {
+            "schema": {
+              "$ref": "#/components/schemas/Error"
+            }
+          }
+        },
+        "description": "Insufficient permission to perform the requested action."
+      }
+    },
+    "schemas": {
+      "Item": {
+        "type": "object",
+        "properties": {
+          "foo": {
+            "type": "string"
+          }
+        }
+      },
+      "Error": {
+        "description": "Error response.",
+        "properties": {
+          "message": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "message"
+        ],
+        "type": "object"
+      }
+    }
+  },
   "tags": [
     {
       "name": "Example",
@@ -162,6 +250,9 @@ const exampleV3 = `
           "default": {
             "description": "default response"
           },
+          "403": {
+            "$ref": "#/components/responses/ForbiddenError"
+          },
           "404": {
             "description": "404 response"
           }
@@ -189,6 +280,19 @@ const exampleV3 = `
               "minimum": 1,
               "type": "integer"
             }
+          },
+          {
+            "description": "Only return results that intersect the provided bounding box.",
+            "in": "query",
+            "name": "bbox",
+            "schema": {
+              "type": "array",
+              "items": {
+                "type": "number"
+              },
+              "minItems": 4,
+              "maxItems": 4
+            }
           }
         ],
         "requestBody": {
@@ -199,6 +303,19 @@ const exampleV3 = `
           }
         },
         "responses": {
+          "200": {
+            "description": "ok",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "items": {
+                    "$ref": "#/components/schemas/Item"
+                  },
+                  "type": "array"
+                }
+              }
+            }
+          },
           "default": {
             "description": "default response"
           },