Repository: getkin/kin-openapi (go)

Issue:
gorillamux router does not match on overridden servers
The gorillamux router does not match on overridden servers at the path and operation level.

These are valid in the OpenAPI spec: https://swagger.io/docs/specification/api-host-and-base-path/

Pull request:
Match on overridden servers at the path level, fixes #564
This adds support for overriding servers at the path level.

Support for overriding servers at the operation level is still not supported.

Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 70b6db6..d34061a 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -66,7 +66,7 @@ jobs:
       env:
         GOARCH: '386'
     - run: go test ./...
-    - run: go test -count=2 ./...
+    - run: go test -count=2 -covermode=atomic ./...
     - run: go test -v -run TestRaceyPatternSchema -race ./...
       env:
         CGO_ENABLED: '1'
@@ -151,7 +151,7 @@ jobs:
     - uses: actions/setup-go@v3
       with:
         go-version: '>=1.17.0'
-    - run: go install github.com/incu6us/goimports-reviser/v2@v2.5.1
+    - run: go install github.com/incu6us/goimports-reviser/v2@latest
     - run: which goimports-reviser
     - run: find . -type f -iname '*.go' ! -iname '*.pb.go' -exec goimports-reviser -file-path {} \;
     - run: git --no-pager diff --exit-code
diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go
index 2a98d68..6e946d8 100644
--- a/jsoninfo/marshal.go
+++ b/jsoninfo/marshal.go
@@ -7,8 +7,8 @@ import (
 )
 
 // MarshalStrictStruct function:
-//   * Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag.
-//   * Correctly handles StrictStruct semantics.
+//   - Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag.
+//   - Correctly handles StrictStruct semantics.
 func MarshalStrictStruct(value StrictStruct) ([]byte, error) {
 	encoder := NewObjectEncoder()
 	if err := value.EncodeWith(encoder, value); err != nil {
diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go
index ce3c337..eb6e758 100644
--- a/jsoninfo/unmarshal.go
+++ b/jsoninfo/unmarshal.go
@@ -7,8 +7,8 @@ import (
 )
 
 // UnmarshalStrictStruct function:
-//   * Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag.
-//   * Correctly handles StrictStruct
+//   - Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag.
+//   - Correctly handles StrictStruct
 func UnmarshalStrictStruct(data []byte, value StrictStruct) error {
 	decoder, err := NewObjectDecoder(data)
 	if err != nil {
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index d34dd31..53b4b40 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -84,7 +84,7 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) {
 	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)
+			r, err := ToV3Response(response, doc2.Produces)
 			if err != nil {
 				return nil, err
 			}
@@ -193,7 +193,7 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *
 	if responses := operation.Responses; responses != nil {
 		doc3Responses := make(openapi3.Responses, len(responses))
 		for k, response := range responses {
-			doc3, err := ToV3Response(response)
+			doc3, err := ToV3Response(response, operation.Produces)
 			if err != nil {
 				return nil, err
 			}
@@ -413,7 +413,7 @@ func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[
 	return nil, nil
 }
 
-func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
+func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) {
 	if ref := response.Ref; ref != "" {
 		return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil
 	}
@@ -422,8 +422,18 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
 		Description:    &response.Description,
 		ExtensionProps: response.ExtensionProps,
 	}
+
+	// Default to "application/json" if "produces" is not specified.
+	if len(produces) == 0 {
+		produces = []string{"application/json"}
+	}
+
 	if schemaRef := response.Schema; schemaRef != nil {
-		result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
+		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)
diff --git a/openapi3/errors.go b/openapi3/errors.go
index ce52cd4..da0970a 100644
--- a/openapi3/errors.go
+++ b/openapi3/errors.go
@@ -18,8 +18,8 @@ func (me MultiError) Error() string {
 	return buff.String()
 }
 
-//Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()`
-//It will also return true if any of the contained errors match target
+// Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()`
+// It will also return true if any of the contained errors match target
 func (me MultiError) Is(target error) bool {
 	if _, ok := target.(MultiError); ok {
 		return true
@@ -32,7 +32,7 @@ func (me MultiError) Is(target error) bool {
 	return false
 }
 
-//As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type
+// As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type
 func (me MultiError) As(target interface{}) bool {
 	for _, e := range me {
 		if errors.As(e, target) {
diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go
index 3a6cabb..1733a49 100644
--- a/openapi3/internalize_refs.go
+++ b/openapi3/internalize_refs.go
@@ -200,7 +200,7 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver)
 }
 
 func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) {
-	if s == nil {
+	if s == nil || doc.isVisitedSchema(s) {
 		return
 	}
 
@@ -229,6 +229,9 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) {
 func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver) {
 	for _, h := range hs {
 		doc.addHeaderToSpec(h, refNameResolver)
+		if doc.isVisitedHeader(h.Value) {
+			continue
+		}
 		doc.derefParameter(h.Value.Parameter, refNameResolver)
 	}
 }
@@ -322,8 +325,10 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso
 //
 // Example:
 //
-//   doc.InternalizeRefs(context.Background(), nil)
+//	doc.InternalizeRefs(context.Background(), nil)
 func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) {
+	doc.resetVisited()
+
 	if refNameResolver == nil {
 		refNameResolver = DefaultRefNameResolver
 	}
diff --git a/openapi3/loader.go b/openapi3/loader.go
index e2f131e..806b819 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -699,7 +699,8 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
-			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
+			foundPath := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath)
+			documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath)
 		}
 	}
 	value := component.Value
@@ -746,6 +747,21 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 	return nil
 }
 
+func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string {
+	if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" {
+		if cur != nil {
+			return path.Base(cur.Path)
+		}
+		return ""
+	}
+	// ref. to external file
+	if resolved.Ref != "" {
+		return resolved.Ref
+	}
+	// found dest spec. file
+	return path.Dir(found.Path)[len(loader.rootDir):]
+}
+
 func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) {
 	if component != nil && component.Value != nil {
 		if loader.visitedSecurityScheme == nil {
diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go
index c0188c2..cb9183d 100644
--- a/openapi3/openapi3.go
+++ b/openapi3/openapi3.go
@@ -21,6 +21,8 @@ type T struct {
 	Servers      Servers              `json:"servers,omitempty" yaml:"servers,omitempty"`
 	Tags         Tags                 `json:"tags,omitempty" yaml:"tags,omitempty"`
 	ExternalDocs *ExternalDocs        `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
+
+	visited visitedComponent
 }
 
 // MarshalJSON returns the JSON encoding of T.
diff --git a/openapi3/paths.go b/openapi3/paths.go
index b4ebe58..be7f3dc 100644
--- a/openapi3/paths.go
+++ b/openapi3/paths.go
@@ -98,10 +98,10 @@ func (paths Paths) Validate(ctx context.Context) error {
 //
 // For example:
 //
-//   paths := openapi3.Paths {
-//     "/person/{personName}": &openapi3.PathItem{},
-//   }
-//   pathItem := path.Find("/person/{name}")
+//	paths := openapi3.Paths {
+//	  "/person/{personName}": &openapi3.PathItem{},
+//	}
+//	pathItem := path.Find("/person/{name}")
 //
 // would return the correct path item.
 func (paths Paths) Find(key string) *PathItem {
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 4c8df1c..17f547e 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -24,6 +24,12 @@ const (
 	TypeNumber  = "number"
 	TypeObject  = "object"
 	TypeString  = "string"
+
+	// constants for integer formats
+	formatMinInt32 = float64(math.MinInt32)
+	formatMaxInt32 = float64(math.MaxInt32)
+	formatMinInt64 = float64(math.MinInt64)
+	formatMaxInt64 = float64(math.MaxInt64)
 )
 
 var (
@@ -808,6 +814,12 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
 	switch value := value.(type) {
 	case bool:
 		return schema.visitJSONBoolean(settings, value)
+	case int:
+		return schema.visitJSONNumber(settings, float64(value))
+	case int32:
+		return schema.visitJSONNumber(settings, float64(value))
+	case int64:
+		return schema.visitJSONNumber(settings, float64(value))
 	case float64:
 		return schema.visitJSONNumber(settings, value)
 	case string:
@@ -1019,7 +1031,7 @@ func (schema *Schema) VisitJSONNumber(value float64) error {
 func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error {
 	var me MultiError
 	schemaType := schema.Type
-	if schemaType == "integer" {
+	if schemaType == TypeInteger {
 		if bigFloat := big.NewFloat(value); !bigFloat.IsInt() {
 			if settings.failfast {
 				return errSchema
@@ -1039,6 +1051,39 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 		return schema.expectedType(settings, "number, integer")
 	}
 
+	// formats
+	if schemaType == TypeInteger && schema.Format != "" {
+		formatMin := float64(0)
+		formatMax := float64(0)
+		switch schema.Format {
+		case "int32":
+			formatMin = formatMinInt32
+			formatMax = formatMaxInt32
+		case "int64":
+			formatMin = formatMinInt64
+			formatMax = formatMaxInt64
+		default:
+			if !SchemaFormatValidationDisabled {
+				return unsupportedFormat(schema.Format)
+			}
+		}
+		if formatMin != 0 && formatMax != 0 && !(formatMin <= value && value <= formatMax) {
+			if settings.failfast {
+				return errSchema
+			}
+			err := &SchemaError{
+				Value:       value,
+				Schema:      schema,
+				SchemaField: "format",
+				Reason:      fmt.Sprintf("number must be an %s", schema.Format),
+			}
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
+		}
+	}
+
 	// "exclusiveMinimum"
 	if v := schema.ExclusiveMin; v && !(*schema.Min < value) {
 		if settings.failfast {
diff --git a/openapi3/visited.go b/openapi3/visited.go
new file mode 100644
index 0000000..67f970e
--- /dev/null
+++ b/openapi3/visited.go
@@ -0,0 +1,41 @@
+package openapi3
+
+func newVisited() visitedComponent {
+	return visitedComponent{
+		header: make(map[*Header]struct{}),
+		schema: make(map[*Schema]struct{}),
+	}
+}
+
+type visitedComponent struct {
+	header map[*Header]struct{}
+	schema map[*Schema]struct{}
+}
+
+// resetVisited clears visitedComponent map
+// should be called before recursion over doc *T
+func (doc *T) resetVisited() {
+	doc.visited = newVisited()
+}
+
+// isVisitedHeader returns `true` if the *Header pointer was already visited
+// otherwise it returns `false`
+func (doc *T) isVisitedHeader(h *Header) bool {
+	if _, ok := doc.visited.header[h]; ok {
+		return true
+	}
+
+	doc.visited.header[h] = struct{}{}
+	return false
+}
+
+// isVisitedHeader returns `true` if the *Schema pointer was already visited
+// otherwise it returns `false`
+func (doc *T) isVisitedSchema(s *Schema) bool {
+	if _, ok := doc.visited.schema[s]; ok {
+		return true
+	}
+
+	doc.visited.schema[s] = struct{}{}
+	return false
+}
diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go
index bf551a7..811ba7d 100644
--- a/routers/gorillamux/router.go
+++ b/routers/gorillamux/router.go
@@ -34,6 +34,13 @@ type routeMux struct {
 	varsUpdater varsf
 }
 
+type srv struct {
+	schemes     []string
+	host, base  string
+	server      *openapi3.Server
+	varsUpdater varsf
+}
+
 var singleVariableMatcher = regexp.MustCompile(`^\{([^{}]+)\}$`)
 
 // TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request))
@@ -42,78 +49,22 @@ var singleVariableMatcher = regexp.MustCompile(`^\{([^{}]+)\}$`)
 // Assumes spec is .Validate()d
 // Note that a variable for the port number MUST have a default value and only this value will match as the port (see issue #367).
 func NewRouter(doc *openapi3.T) (routers.Router, error) {
-	type srv struct {
-		schemes     []string
-		host, base  string
-		server      *openapi3.Server
-		varsUpdater varsf
+	servers, err := makeServers(doc.Servers)
+	if err != nil {
+		return nil, err
 	}
-	servers := make([]srv, 0, len(doc.Servers))
-	for _, server := range doc.Servers {
-		serverURL := server.URL
-		if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil {
-			sVar := submatch[1]
-			sVal := server.Variables[sVar].Default
-			serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal)
-			var varsUpdater varsf
-			if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" {
-				varsUpdater = func(vars map[string]string) { vars[sVar] = lhs }
-			}
-			servers = append(servers, srv{
-				base:        server.Variables[sVar].Default,
-				server:      server,
-				varsUpdater: varsUpdater,
-			})
-			continue
-		}
-
-		var schemes []string
-		if strings.Contains(serverURL, "://") {
-			scheme0 := strings.Split(serverURL, "://")[0]
-			schemes = permutePart(scheme0, server)
-			serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1)
-		}
 
-		// If a variable represents the port "http://domain.tld:{port}/bla"
-		// then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla"
-		// and mux is not able to set the {port} variable
-		// So we just use the default value for this variable.
-		// See https://github.com/getkin/kin-openapi/issues/367
-		var varsUpdater varsf
-		if lhs := strings.Index(serverURL, ":{"); lhs > 0 {
-			rest := serverURL[lhs+len(":{"):]
-			rhs := strings.Index(rest, "}")
-			portVariable := rest[:rhs]
-			portValue := server.Variables[portVariable].Default
-			serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue)
-			varsUpdater = func(vars map[string]string) {
-				vars[portVariable] = portValue
-			}
-		}
-
-		u, err := url.Parse(bEncode(serverURL))
-		if err != nil {
-			return nil, err
-		}
-		path := bDecode(u.EscapedPath())
-		if len(path) > 0 && path[len(path)-1] == '/' {
-			path = path[:len(path)-1]
-		}
-		servers = append(servers, srv{
-			host:        bDecode(u.Host), //u.Hostname()?
-			base:        path,
-			schemes:     schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624
-			server:      server,
-			varsUpdater: varsUpdater,
-		})
-	}
-	if len(servers) == 0 {
-		servers = append(servers, srv{})
-	}
 	muxRouter := mux.NewRouter().UseEncodedPath()
 	r := &Router{}
 	for _, path := range orderedPaths(doc.Paths) {
+		servers := servers
+
 		pathItem := doc.Paths[path]
+		if len(pathItem.Servers) > 0 {
+			if servers, err = makeServers(pathItem.Servers); err != nil {
+				return nil, err
+			}
+		}
 
 		operations := pathItem.Operations()
 		methods := make([]string, 0, len(operations))
@@ -177,6 +128,73 @@ func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string
 	return nil, nil, routers.ErrPathNotFound
 }
 
+func makeServers(in openapi3.Servers) ([]srv, error) {
+	servers := make([]srv, 0, len(in))
+	for _, server := range in {
+		serverURL := server.URL
+		if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil {
+			sVar := submatch[1]
+			sVal := server.Variables[sVar].Default
+			serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal)
+			var varsUpdater varsf
+			if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" {
+				varsUpdater = func(vars map[string]string) { vars[sVar] = lhs }
+			}
+			servers = append(servers, srv{
+				base:        server.Variables[sVar].Default,
+				server:      server,
+				varsUpdater: varsUpdater,
+			})
+			continue
+		}
+
+		var schemes []string
+		if strings.Contains(serverURL, "://") {
+			scheme0 := strings.Split(serverURL, "://")[0]
+			schemes = permutePart(scheme0, server)
+			serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1)
+		}
+
+		// If a variable represents the port "http://domain.tld:{port}/bla"
+		// then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla"
+		// and mux is not able to set the {port} variable
+		// So we just use the default value for this variable.
+		// See https://github.com/getkin/kin-openapi/issues/367
+		var varsUpdater varsf
+		if lhs := strings.Index(serverURL, ":{"); lhs > 0 {
+			rest := serverURL[lhs+len(":{"):]
+			rhs := strings.Index(rest, "}")
+			portVariable := rest[:rhs]
+			portValue := server.Variables[portVariable].Default
+			serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue)
+			varsUpdater = func(vars map[string]string) {
+				vars[portVariable] = portValue
+			}
+		}
+
+		u, err := url.Parse(bEncode(serverURL))
+		if err != nil {
+			return nil, err
+		}
+		path := bDecode(u.EscapedPath())
+		if len(path) > 0 && path[len(path)-1] == '/' {
+			path = path[:len(path)-1]
+		}
+		servers = append(servers, srv{
+			host:        bDecode(u.Host), //u.Hostname()?
+			base:        path,
+			schemes:     schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624
+			server:      server,
+			varsUpdater: varsUpdater,
+		})
+	}
+	if len(servers) == 0 {
+		servers = append(servers, srv{})
+	}
+
+	return servers, nil
+}
+
 func orderedPaths(paths map[string]*openapi3.PathItem) []string {
 	// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject
 	// When matching URLs, concrete (non-templated) paths would be matched
diff --git a/routers/legacy/pathpattern/node.go b/routers/legacy/pathpattern/node.go
index 8621998..011dda3 100644
--- a/routers/legacy/pathpattern/node.go
+++ b/routers/legacy/pathpattern/node.go
@@ -1,11 +1,11 @@
 // Package pathpattern implements path matching.
 //
 // Examples of supported patterns:
-//   * "/"
-//   * "/abc""
-//   * "/abc/{variable}" (matches until next '/' or end-of-string)
-//   * "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot)
-//   * "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions)
+//   - "/"
+//   - "/abc""
+//   - "/abc/{variable}" (matches until next '/' or end-of-string)
+//   - "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot)
+//   - "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions)
 package pathpattern
 
 import (
@@ -28,8 +28,8 @@ type Options struct {
 // PathFromHost converts a host pattern to a path pattern.
 //
 // Examples:
-//   * PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain"
-//   * PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some"
+//   - PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain"
+//   - PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some"
 func PathFromHost(host string, specialDashes bool) string {
 	buf := make([]byte, 0, len(host))
 	end := len(host)


Test patch:
diff --git a/jsoninfo/marshal_test.go b/jsoninfo/marshal_test.go
index 05a6ac3..1055154 100644
--- a/jsoninfo/marshal_test.go
+++ b/jsoninfo/marshal_test.go
@@ -64,9 +64,10 @@ type EmbeddedType1 struct {
 }
 
 // Example describes expected outcome of:
-//   1.Marshal JSON
-//   2.Unmarshal value
-//   3.Marshal value
+//
+//	1.Marshal JSON
+//	2.Unmarshal value
+//	3.Marshal value
 type Example struct {
 	NoMarshal   bool
 	NoUnmarshal bool
diff --git a/openapi2conv/issue573_test.go b/openapi2conv/issue573_test.go
new file mode 100644
index 0000000..cefac40
--- /dev/null
+++ b/openapi2conv/issue573_test.go
@@ -0,0 +1,48 @@
+package openapi2conv
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIssue573(t *testing.T) {
+	spec := []byte(`paths:
+  /ping:
+    get:
+      produces:
+        - application/toml
+        - application/xml
+      responses:
+        200:
+          schema:
+            type: object
+            properties:
+              username:
+                type: string
+                description: The user name.
+    post:
+      responses:
+        200:
+          schema:
+            type: object
+            properties:
+              username:
+                type: string
+                description: The user name.`)
+
+	v3, err := v2v3YAML(spec)
+	require.NoError(t, err)
+
+	// Make sure the response content appears for each mime-type originally
+	// appeared in "produces".
+	pingGetContent := v3.Paths["/ping"].Get.Responses["200"].Value.Content
+	require.Len(t, pingGetContent, 2)
+	require.Contains(t, pingGetContent, "application/toml")
+	require.Contains(t, pingGetContent, "application/xml")
+
+	// Is "produces" is not explicitly specified, default to "application/json".
+	pingPostContent := v3.Paths["/ping"].Post.Responses["200"].Value.Content
+	require.Len(t, pingPostContent, 1)
+	require.Contains(t, pingPostContent, "application/json")
+}
diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go
new file mode 100644
index 0000000..978ef8f
--- /dev/null
+++ b/openapi3/load_cicular_ref_with_external_file_test.go
@@ -0,0 +1,70 @@
+//go:build go1.16
+// +build go1.16
+
+package openapi3_test
+
+import (
+	"embed"
+	"encoding/json"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+//go:embed testdata/circularRef/*
+var circularResSpecs embed.FS
+
+func TestLoadCircularRefFromFile(t *testing.T) {
+	loader := openapi3.NewLoader()
+	loader.IsExternalRefsAllowed = true
+	loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) {
+		return circularResSpecs.ReadFile(uri.Path)
+	}
+
+	got, err := loader.LoadFromFile("testdata/circularRef/base.yml")
+	require.NoError(t, err)
+
+	foo := &openapi3.SchemaRef{
+		Value: &openapi3.Schema{
+			Properties: map[string]*openapi3.SchemaRef{
+				"foo2": {
+					Ref: "other.yml#/components/schemas/Foo2", // reference to an external file
+					Value: &openapi3.Schema{
+						Properties: map[string]*openapi3.SchemaRef{
+							"id": {
+								Value: &openapi3.Schema{Type: "string"}},
+						},
+					},
+				},
+			},
+		},
+	}
+	bar := &openapi3.SchemaRef{Value: &openapi3.Schema{Properties: make(map[string]*openapi3.SchemaRef)}}
+	// circular reference
+	bar.Value.Properties["foo"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Foo", Value: foo.Value}
+	foo.Value.Properties["bar"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Bar", Value: bar.Value}
+
+	want := &openapi3.T{
+		OpenAPI: "3.0.3",
+		Info: &openapi3.Info{
+			Title:   "Recursive cyclic refs example",
+			Version: "1.0",
+		},
+		Components: openapi3.Components{
+			Schemas: map[string]*openapi3.SchemaRef{
+				"Foo": foo,
+				"Bar": bar,
+			},
+		},
+	}
+
+	jsoner := func(doc *openapi3.T) string {
+		data, err := json.Marshal(doc)
+		require.NoError(t, err)
+		return string(data)
+	}
+	require.JSONEq(t, jsoner(want), jsoner(got))
+}
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index abdfb34..593fa17 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -4,13 +4,13 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
-	"fmt"
 	"math"
 	"reflect"
 	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
 )
 
 type schemaExample struct {
@@ -45,23 +45,25 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) {
 			require.NoError(t, err)
 			require.Equal(t, dataUnserialized, dataSchema)
 		}
-		for _, value := range example.AllValid {
-			err := validateSchema(t, schema, value)
-			require.NoError(t, err)
-		}
-		for _, value := range example.AllInvalid {
-			err := validateSchema(t, schema, value)
-			require.Error(t, err)
+		for validateFuncIndex, validateFunc := range validateSchemaFuncs {
+			for index, value := range example.AllValid {
+				err := validateFunc(t, schema, value)
+				require.NoErrorf(t, err, "ValidateFunc #%d, AllValid #%d: %#v", validateFuncIndex, index, value)
+			}
+			for index, value := range example.AllInvalid {
+				err := validateFunc(t, schema, value)
+				require.Errorf(t, err, "ValidateFunc #%d, AllInvalid #%d: %#v", validateFuncIndex, index, value)
+			}
 		}
 		// NaN and Inf aren't valid JSON but are handled
-		for _, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} {
+		for index, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} {
 			err := schema.VisitJSON(value)
-			require.Error(t, err)
+			require.Errorf(t, err, "NaNAndInf #%d: %#v", index, value)
 		}
 	}
 }
 
-func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error {
+func validateSchemaJSON(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error {
 	data, err := json.Marshal(value)
 	require.NoError(t, err)
 	var val interface{}
@@ -70,6 +72,22 @@ func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...Sch
 	return schema.VisitJSON(val, opts...)
 }
 
+func validateSchemaYAML(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error {
+	data, err := yaml.Marshal(value)
+	require.NoError(t, err)
+	var val interface{}
+	err = yaml.Unmarshal(data, &val)
+	require.NoError(t, err)
+	return schema.VisitJSON(val, opts...)
+}
+
+type validateSchemaFunc func(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error
+
+var validateSchemaFuncs = []validateSchemaFunc{
+	validateSchemaJSON,
+	validateSchemaYAML,
+}
+
 var schemaExamples = []schemaExample{
 	{
 		Title:         "EMPTY SCHEMA",
@@ -234,7 +252,56 @@ var schemaExamples = []schemaExample{
 			map[string]interface{}{},
 		},
 	},
-
+	{
+		Title:  "INTEGER OPTIONAL INT64 FORMAT",
+		Schema: NewInt64Schema(),
+		Serialization: map[string]interface{}{
+			"type":   "integer",
+			"format": "int64",
+		},
+		AllValid: []interface{}{
+			1,
+			256,
+			65536,
+			int64(math.MaxInt32) + 10,
+			int64(math.MinInt32) - 10,
+		},
+		AllInvalid: []interface{}{
+			nil,
+			false,
+			3.5,
+			true,
+			"",
+			[]interface{}{},
+			map[string]interface{}{},
+		},
+	},
+	{
+		Title:  "INTEGER OPTIONAL INT32 FORMAT",
+		Schema: NewInt32Schema(),
+		Serialization: map[string]interface{}{
+			"type":   "integer",
+			"format": "int32",
+		},
+		AllValid: []interface{}{
+			1,
+			256,
+			65536,
+			int64(math.MaxInt32),
+			int64(math.MaxInt32),
+		},
+		AllInvalid: []interface{}{
+			nil,
+			false,
+			3.5,
+			int64(math.MaxInt32) + 10,
+			int64(math.MinInt32) - 10,
+			true,
+			"",
+			[]interface{}{},
+			map[string]interface{}{},
+		},
+	},
 	{
 		Title: "STRING",
 		Schema: NewStringSchema().
@@ -350,7 +417,7 @@ var schemaExamples = []schemaExample{
 		AllInvalid: []interface{}{
 			nil,
 			" ",
-			"\n",
+			"\n\n", // a \n is ok for JSON but not for YAML decoder/encoder
 			"%",
 		},
 	},
@@ -1074,28 +1141,30 @@ func TestSchemasMultiError(t *testing.T) {
 func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*testing.T) {
 	return func(t *testing.T) {
 		schema := example.Schema
-		for i, value := range example.Values {
-			err := validateSchema(t, schema, value, MultiErrors())
-			require.Error(t, err)
-			require.IsType(t, MultiError{}, err)
+		for validateFuncIndex, validateFunc := range validateSchemaFuncs {
+			for i, value := range example.Values {
+				err := validateFunc(t, schema, value, MultiErrors())
+				require.Errorf(t, err, "ValidateFunc #%d, value #%d: %#", validateFuncIndex, i, value)
+				require.IsType(t, MultiError{}, err)
 
-			merr, _ := err.(MultiError)
-			expected := example.ExpectedErrors[i]
-			require.True(t, len(merr) > 0)
-			require.Len(t, merr, len(expected))
-			for _, e := range merr {
-				require.IsType(t, &SchemaError{}, e)
-				var found bool
-				scherr, _ := e.(*SchemaError)
-				for _, expectedErr := range expected {
-					expectedScherr, _ := expectedErr.(*SchemaError)
-					if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) &&
-						expectedScherr.SchemaField == scherr.SchemaField {
-						found = true
-						break
+				merr, _ := err.(MultiError)
+				expected := example.ExpectedErrors[i]
+				require.True(t, len(merr) > 0)
+				require.Len(t, merr, len(expected))
+				for _, e := range merr {
+					require.IsType(t, &SchemaError{}, e)
+					var found bool
+					scherr, _ := e.(*SchemaError)
+					for _, expectedErr := range expected {
+						expectedScherr, _ := expectedErr.(*SchemaError)
+						if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) &&
+							expectedScherr.SchemaField == scherr.SchemaField {
+							found = true
+							break
+						}
 					}
+					require.Truef(t, found, "ValidateFunc #%d, value #%d: missing %s error on %s", validateFunc, i, scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))
 				}
-				require.True(t, found, fmt.Sprintf("missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), ".")))
 			}
 		}
 	}
diff --git a/openapi3/testdata/circularRef/base.yml b/openapi3/testdata/circularRef/base.yml
new file mode 100644
index 0000000..ff8240e
--- /dev/null
+++ b/openapi3/testdata/circularRef/base.yml
@@ -0,0 +1,16 @@
+openapi: "3.0.3"
+info:
+  title: Recursive cyclic refs example
+  version: "1.0"
+components:
+  schemas:
+    Foo:
+      properties:
+        foo2:
+          $ref: "other.yml#/components/schemas/Foo2"
+        bar:
+          $ref: "#/components/schemas/Bar"
+    Bar:
+      properties:
+        foo:
+          $ref: "#/components/schemas/Foo"
diff --git a/openapi3/testdata/circularRef/other.yml b/openapi3/testdata/circularRef/other.yml
new file mode 100644
index 0000000..29b72d9
--- /dev/null
+++ b/openapi3/testdata/circularRef/other.yml
@@ -0,0 +1,10 @@
+openapi: "3.0.3"
+info:
+  title: Recursive cyclic refs example
+  version: "1.0"
+components:
+  schemas:
+    Foo2:
+      properties:
+        id:
+          type: string
diff --git a/openapi3/testdata/recursiveRef/components/Cat.yml b/openapi3/testdata/recursiveRef/components/Cat.yml
new file mode 100644
index 0000000..c476aa1
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/components/Cat.yml
@@ -0,0 +1,4 @@
+type: object
+properties:
+  cat:
+    $ref: ../openapi.yml#/components/schemas/Cat
diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml
index 3559c8e..675722a 100644
--- a/openapi3/testdata/recursiveRef/openapi.yml
+++ b/openapi3/testdata/recursiveRef/openapi.yml
@@ -13,3 +13,5 @@ components:
       $ref: ./components/Foo/Foo2.yml
     Bar:
       $ref: ./components/Bar.yml
+    Cat:
+      $ref: ./components/Cat.yml
diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
index d1260eb..0730590 100644
--- a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
+++ b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
@@ -29,6 +29,14 @@
           }
         },
         "type": "object"
+      },
+      "Cat": {
+        "properties": {
+          "cat": {
+            "$ref": "#/components/schemas/Cat"
+          }
+        },
+        "type": "object"
       }
     }
   },
diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go
index f8800ba..104056e 100644
--- a/routers/gorillamux/router_test.go
+++ b/routers/gorillamux/router_test.go
@@ -254,6 +254,47 @@ func TestServerPath(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func TestServerOverrideAtPathLevel(t *testing.T) {
+	helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
+	doc := &openapi3.T{
+		OpenAPI: "3.0.0",
+		Info: &openapi3.Info{
+			Title:   "rel",
+			Version: "1",
+		},
+		Servers: openapi3.Servers{
+			&openapi3.Server{
+				URL: "https://example.com",
+			},
+		},
+		Paths: openapi3.Paths{
+			"/hello": &openapi3.PathItem{
+				Servers: openapi3.Servers{
+					&openapi3.Server{
+						URL: "https://another.com",
+					},
+				},
+				Get: helloGET,
+			},
+		},
+	}
+	err := doc.Validate(context.Background())
+	require.NoError(t, err)
+	router, err := NewRouter(doc)
+	require.NoError(t, err)
+
+	req, err := http.NewRequest(http.MethodGet, "https://another.com/hello", nil)
+	require.NoError(t, err)
+	route, _, err := router.FindRoute(req)
+	require.Equal(t, "/hello", route.Path)
+
+	req, err = http.NewRequest(http.MethodGet, "https://example.com/hello", nil)
+	require.NoError(t, err)
+	route, _, err = router.FindRoute(req)
+	require.Nil(t, route)
+	require.Error(t, err)
+}
+
 func TestRelativeURL(t *testing.T) {
 	helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
 	doc := &openapi3.T{