Repository: getkin/kin-openapi
Pull request #614 (2022-09-23 08:41:00)

Allow validations options when creating legace Router
My proposal to implement https://github.com/getkin/kin-openapi/issues/613


Patch:
diff --git a/openapi3/errors.go b/openapi3/errors.go
index da0970a..b530101 100644
--- a/openapi3/errors.go
+++ b/openapi3/errors.go
@@ -11,9 +11,11 @@ type MultiError []error
 
 func (me MultiError) Error() string {
 	buff := &bytes.Buffer{}
-	for _, e := range me {
+	for i, e := range me {
 		buff.WriteString(e.Error())
-		buff.WriteString(" | ")
+		if i != len(me)-1 {
+			buff.WriteString(" | ")
+		}
 	}
 	return buff.String()
 }
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 28836f3..637e1ac 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -36,7 +36,8 @@ type Loader struct {
 
 	Context context.Context
 
-	rootDir string
+	rootDir      string
+	rootLocation string
 
 	visitedPathItemRefs map[string]struct{}
 
@@ -148,6 +149,7 @@ func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T,
 func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) {
 	if loader.visitedDocuments == nil {
 		loader.visitedDocuments = make(map[string]*T)
+		loader.rootLocation = location.Path
 	}
 	uri := location.String()
 	if doc, ok := loader.visitedDocuments[uri]; ok {
@@ -420,6 +422,11 @@ func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef
 	if loader.rootDir == "" {
 		return current
 	}
+
+	if resolvedRef == "" {
+		return &url.URL{Path: loader.rootLocation}
+	}
+
 	return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)}
 }
 
@@ -783,6 +790,10 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string {
 	if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" {
 		if cur != nil {
+			if loader.rootDir != "" && strings.HasPrefix(cur.Path, loader.rootDir) {
+				return cur.Path[len(loader.rootDir)+1:]
+			}
+
 			return path.Base(cur.Path)
 		}
 		return ""
diff --git a/routers/legacy/router.go b/routers/legacy/router.go
index fb8d462..74e3873 100644
--- a/routers/legacy/router.go
+++ b/routers/legacy/router.go
@@ -58,8 +58,8 @@ type Router struct {
 //
 // If the given OpenAPIv3 document has servers, router will use them.
 // All operations of the document will be added to the router.
-func NewRouter(doc *openapi3.T) (routers.Router, error) {
-	if err := doc.Validate(context.Background()); err != nil {
+func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) {
+	if err := doc.Validate(context.Background(), opts...); err != nil {
 		return nil, fmt.Errorf("validating OpenAPI failed: %w", err)
 	}
 	router := &Router{doc: doc}


Test patch:
diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go
index b1ca846..fe6b29d 100644
--- a/openapi3/internalize_refs_test.go
+++ b/openapi3/internalize_refs_test.go
@@ -2,7 +2,7 @@ package openapi3
 
 import (
 	"context"
-	"io/ioutil"
+	"os"
 	"regexp"
 	"testing"
 
@@ -41,25 +41,25 @@ func TestInternalizeRefs(t *testing.T) {
 			err = doc.Validate(ctx)
 			require.NoError(t, err, "validating internalized spec")
 
-			data, err := doc.MarshalJSON()
+			actual, err := doc.MarshalJSON()
 			require.NoError(t, err, "marshalling internalized spec")
 
 			// run a static check over the file, making sure each occurence of a
 			// reference is followed by a #
-			numRefs := len(regexpRef.FindAll(data, -1))
-			numInternalRefs := len(regexpRefInternal.FindAll(data, -1))
+			numRefs := len(regexpRef.FindAll(actual, -1))
+			numInternalRefs := len(regexpRefInternal.FindAll(actual, -1))
 			require.Equal(t, numRefs, numInternalRefs, "checking all references are internal")
 
-			// load from data, but with the path set to the current directory
-			doc2, err := sl.LoadFromData(data)
+			// load from actual, but with the path set to the current directory
+			doc2, err := sl.LoadFromData(actual)
 			require.NoError(t, err, "reloading spec")
 			err = doc2.Validate(ctx)
 			require.NoError(t, err, "validating reloaded spec")
 
 			// compare with expected
-			data0, err := ioutil.ReadFile(test.filename + ".internalized.yml")
+			expected, err := os.ReadFile(test.filename + ".internalized.yml")
 			require.NoError(t, err)
-			require.JSONEq(t, string(data), string(data0))
+			require.JSONEq(t, string(expected), string(actual))
 		})
 	}
 }
diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go
index 5b7c150..924cb6b 100644
--- a/openapi3/loader_recursive_ref_test.go
+++ b/openapi3/loader_recursive_ref_test.go
@@ -14,6 +14,8 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) {
 	err = doc.Validate(loader.Context)
 	require.NoError(t, err)
 	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example)
+	require.Equal(t, "ErrorDetails", doc.Paths["/foo"].Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title)
+	require.Equal(t, "ErrorDetails", doc.Paths["/double-ref-foo"].Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title)
 }
 
 func TestIssue447(t *testing.T) {
diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go
index d492e24..684e1b4 100644
--- a/openapi3/loader_test.go
+++ b/openapi3/loader_test.go
@@ -263,6 +263,17 @@ func TestLoadWithReferenceInReference(t *testing.T) {
 	require.Equal(t, "string", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type)
 }
 
+func TestLoadWithReferenceInReferenceInProperty(t *testing.T) {
+	loader := NewLoader()
+	loader.IsExternalRefsAllowed = true
+	doc, err := loader.LoadFromFile("testdata/refInRefInProperty/openapi.yaml")
+	require.NoError(t, err)
+	require.NotNil(t, doc)
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+	require.Equal(t, "Problem details", doc.Paths["/api/test/ref/in/ref/in/property"].Post.Responses["401"].Value.Content["application/json"].Schema.Value.Properties["error"].Value.Title)
+}
+
 func TestLoadFileWithExternalSchemaRef(t *testing.T) {
 	loader := NewLoader()
 	loader.IsExternalRefsAllowed = true
diff --git a/openapi3/testdata/recursiveRef/components/models/error.yaml b/openapi3/testdata/recursiveRef/components/models/error.yaml
new file mode 100644
index 0000000..b4d4047
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/components/models/error.yaml
@@ -0,0 +1,2 @@
+type: object
+title: ErrorDetails
diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml
index 675722a..9f884c7 100644
--- a/openapi3/testdata/recursiveRef/openapi.yml
+++ b/openapi3/testdata/recursiveRef/openapi.yml
@@ -5,6 +5,13 @@ info:
 paths:
   /foo:
     $ref: ./paths/foo.yml
+  /double-ref-foo:
+    get:
+      summary: Double ref response
+      description: Reference response with double reference.
+      responses:
+        "400":
+          $ref: "#/components/responses/400"
 components:
   schemas:
     Foo:
@@ -15,3 +22,12 @@ components:
       $ref: ./components/Bar.yml
     Cat:
       $ref: ./components/Cat.yml
+    Error:
+      $ref: ./components/models/error.yaml
+  responses:
+    "400":
+      description: 400 Bad Request
+      content:
+        application/json:
+          schema:
+            $ref: "#/components/schemas/Error"
diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
index 0730590..0d50852 100644
--- a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
+++ b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
@@ -9,11 +9,27 @@
         }
       }
     },
+    "responses": {
+      "400": {
+        "content": {
+          "application/json": {
+            "schema": {
+              "$ref": "#/components/schemas/Error"
+            }
+          }
+        },
+        "description": "400 Bad Request"
+      }
+    },
     "schemas": {
       "Bar": {
         "example": "bar",
         "type": "string"
       },
+      "Error":{
+        "title":"ErrorDetails",
+        "type":"object"
+      },
       "Foo": {
         "properties": {
           "bar": {
@@ -30,6 +46,10 @@
         },
         "type": "object"
       },
+      "error":{
+        "title":"ErrorDetails",
+        "type":"object"
+      },
       "Cat": {
         "properties": {
           "cat": {
@@ -46,6 +66,17 @@
   },
   "openapi": "3.0.3",
   "paths": {
+    "/double-ref-foo": {
+      "get": {
+        "description": "Reference response with double reference.",
+        "responses": {
+          "400": {
+            "$ref": "#/components/responses/400"
+          }
+        },
+        "summary": "Double ref response"
+      }
+    },
     "/foo": {
       "get": {
         "responses": {
@@ -63,6 +94,9 @@
               }
             },
             "description": "OK"
+          },
+          "400": {
+            "$ref": "#/components/responses/400"
           }
         }
       },
diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml
index 43e03b7..4c845b5 100644
--- a/openapi3/testdata/recursiveRef/paths/foo.yml
+++ b/openapi3/testdata/recursiveRef/paths/foo.yml
@@ -11,3 +11,5 @@ get:
             properties:
               foo2:
                 $ref: ../openapi.yml#/components/schemas/Foo2
+    "400":
+      $ref: "../openapi.yml#/components/responses/400"
diff --git a/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json b/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json
new file mode 100644
index 0000000..47547ab
--- /dev/null
+++ b/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json
@@ -0,0 +1,93 @@
+{
+    "title": "Problem details",
+    "description": "Common data object for describing an error details",
+    "type": "object",
+    "additionalProperties": false,
+    "required": [
+        "type",
+        "title",
+        "status"
+    ],
+    "properties": {
+        "type": {
+            "type": "string",
+            "description": "Unique error code",
+            "minLength": 1,
+            "example": "unauthorized",
+            "x-docs-examples": [
+                "validation-error",
+                "unauthorized",
+                "forbidden",
+                "internal-server-error",
+                "wrong-basket",
+                "not-found"
+            ]
+        },
+        "title": {
+            "type": "string",
+            "description": "Human readable error message",
+            "minLength": 1,
+            "example": "Your request parameters didn't validate",
+            "x-docs-examples": [
+                "Your request parameters didn't validate",
+                "Requested resource is not available",
+                "Internal server error"
+            ]
+        },
+        "status": {
+            "type": "integer",
+            "description": "HTTP status code",
+            "maximum": 599,
+            "minimum": 100,
+            "example": 200,
+            "x-docs-examples": [
+                "200",
+                "201",
+                "400",
+                "503"
+            ]
+        },
+        "detail": {
+            "type": "string",
+            "description": "Human readable error description. Only for human",
+            "example": "Basket must have more then 1 item",
+            "x-docs-examples": [
+                "Basket must have more then 1 item"
+            ]
+        },
+        "invalid-params": {
+            "type": "array",
+            "description": "Param list with errors",
+            "items": {
+                "type": "object",
+                "additionalProperties": false,
+                "required": [
+                    "name",
+                    "reason"
+                ],
+                "properties": {
+                    "name": {
+                        "type": "string",
+                        "description": "field name",
+                        "minLength": 1,
+                        "example": "age",
+                        "x-docs-examples": [
+                            "age",
+                            "color"
+                        ]
+                    },
+                    "reason": {
+                        "type": "string",
+                        "description": "Field validation error text",
+                        "minLength": 1,
+                        "example": "must be a positive integer",
+                        "x-docs-examples": [
+                            "must be a positive integer",
+                            "must be 'green', 'red' or 'blue'"
+                        ]
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/openapi3/testdata/refInRefInProperty/components/errors.yaml b/openapi3/testdata/refInRefInProperty/components/errors.yaml
new file mode 100644
index 0000000..1dc8fa7
--- /dev/null
+++ b/openapi3/testdata/refInRefInProperty/components/errors.yaml
@@ -0,0 +1,66 @@
+components:
+  schemas:
+    Error:
+      type: object
+      description: Error info in problem-details-0.0.1 format
+      properties:
+        error:
+          $ref: "../common-data-objects/problem-details-0.0.1.schema.json"
+
+  responses:
+    400:
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: "#/components/schemas/Error"
+          examples:
+            json:
+              $ref: "#/components/examples/BadRequest"
+    401:
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: "#/components/schemas/Error"
+          examples:
+            json:
+              $ref: "#/components/examples/Unauthorized"
+    500:
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: "#/components/schemas/Error"
+          examples:
+            json:
+              $ref: "#/components/examples/InternalServerError"
+
+  examples:
+    BadRequest:
+      summary: Wrong format
+      value:
+        error:
+          type: validation-error
+          title: Your request parameters didn't validate.
+          status: 400
+          invalid-params:
+            - name: age
+              reason: must be a positive integer
+            - name: color
+              reason: must be 'green', 'red' or 'blue'
+    Unauthorized:
+      summary: Not authenticated
+      value:
+        error:
+          type: unauthorized
+          title: The request has not been applied because it lacks valid authentication credentials
+            for the target resource.
+          status: 401
+    InternalServerError:
+      summary: Not handled internal server error
+      value:
+        error:
+          type: internal-server-error
+          title: Internal server error.
+          status: 500
diff --git a/openapi3/testdata/refInRefInProperty/openapi.yaml b/openapi3/testdata/refInRefInProperty/openapi.yaml
new file mode 100644
index 0000000..d44b216
--- /dev/null
+++ b/openapi3/testdata/refInRefInProperty/openapi.yaml
@@ -0,0 +1,29 @@
+openapi: 3.0.3
+
+info:
+  title: "Reference in reference in property example"
+  version: "1.0.0"
+paths:
+  /api/test/ref/in/ref/in/property:
+    post:
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                file:
+                  type: array
+                  items:
+                    type: string
+                    format: binary
+        required: true
+      responses:
+        200:
+          description: "Files are saved successfully"
+        400:
+          $ref: "./components/errors.yaml#/components/responses/400"
+        401:
+          $ref: "./components/errors.yaml#/components/responses/401"
+        500:
+          $ref: "./components/errors.yaml#/components/responses/500"
diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml
index e3b61bf..4a55448 100644
--- a/openapi3filter/testdata/petstore.yaml
+++ b/openapi3filter/testdata/petstore.yaml
@@ -30,6 +30,12 @@ paths:
       summary: "Add a new pet to the store"
       description: ""
       operationId: "addPet"
+      parameters:
+      - name: num
+        in: query
+        schema:
+          type: integer
+          minimum: 1
       requestBody:
         required: true
         content: 
diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go
index 4242177..552e20e 100644
--- a/openapi3filter/unpack_errors_test.go
+++ b/openapi3filter/unpack_errors_test.go
@@ -66,7 +66,7 @@ func Example() {
 
 	// (note invalid type for name and invalid status)
 	body := strings.NewReader(`{"name": 100, "photoUrls": [], "status": "invalidStatus"}`)
-	req, err := http.NewRequest("POST", ts.URL+"/pet", body)
+	req, err := http.NewRequest("POST", ts.URL+"/pet?num=0", body)
 	if err != nil {
 		panic(err)
 	}
@@ -108,17 +108,25 @@ func Example() {
 	// Value:
 	//   "invalidStatus"
 	//
+	// ===== Start New Error =====
+	// query.num:
+	// 	parameter "num" in query has an error: number must be at least 1
+	// Schema:
+	//   {
+	//     "minimum": 1,
+	//     "type": "integer"
+	//   }
+	//
+	// Value:
+	//   0
+	//
 	// response: 400 {}
 }
 
-const (
-	prefixBody = "@body"
-	unknown    = "@unknown"
-)
-
 func convertError(me openapi3.MultiError) map[string][]string {
 	issues := make(map[string][]string)
 	for _, err := range me {
+		const prefixBody = "@body"
 		switch err := err.(type) {
 		case *openapi3.SchemaError:
 			// Can inspect schema validation errors here, e.g. err.Value
@@ -126,47 +134,32 @@ func convertError(me openapi3.MultiError) map[string][]string {
 			if path := err.JSONPointer(); len(path) > 0 {
 				field = fmt.Sprintf("%s.%s", field, strings.Join(path, "."))
 			}
-			if _, ok := issues[field]; !ok {
-				issues[field] = make([]string, 0, 3)
-			}
 			issues[field] = append(issues[field], err.Error())
 		case *openapi3filter.RequestError: // possible there were multiple issues that failed validation
-			if err, ok := err.Err.(openapi3.MultiError); ok {
-				for k, v := range convertError(err) {
-					if _, ok := issues[k]; !ok {
-						issues[k] = make([]string, 0, 3)
-					}
-					issues[k] = append(issues[k], v...)
-				}
-				continue
-			}
 
 			// check if invalid HTTP parameter
 			if err.Parameter != nil {
 				prefix := err.Parameter.In
 				name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name)
-				if _, ok := issues[name]; !ok {
-					issues[name] = make([]string, 0, 3)
-				}
 				issues[name] = append(issues[name], err.Error())
 				continue
 			}
 
+			if err, ok := err.Err.(openapi3.MultiError); ok {
+				for k, v := range convertError(err) {
+					issues[k] = append(issues[k], v...)
+				}
+				continue
+			}
+
 			// check if requestBody
 			if err.RequestBody != nil {
-				if _, ok := issues[prefixBody]; !ok {
-					issues[prefixBody] = make([]string, 0, 3)
-				}
 				issues[prefixBody] = append(issues[prefixBody], err.Error())
 				continue
 			}
 		default:
-			reasons, ok := issues[unknown]
-			if !ok {
-				reasons = make([]string, 0, 3)
-			}
-			reasons = append(reasons, err.Error())
-			issues[unknown] = reasons
+			const unknown = "@unknown"
+			issues[unknown] = append(issues[unknown], err.Error())
 		}
 	}
 	return issues
diff --git a/routers/issue356_test.go b/routers/issue356_test.go
index 3e4b9fa..de9f06b 100644
--- a/routers/issue356_test.go
+++ b/routers/issue356_test.go
@@ -68,8 +68,11 @@ paths:
 		require.NoError(t, err)
 		err = doc.Validate(context.Background())
 		require.NoError(t, err)
+		gorillamuxNewRouterWrapped := func(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) {
+			return gorillamux.NewRouter(doc)
+		}
 
-		for i, newRouter := range []func(*openapi3.T) (routers.Router, error){gorillamux.NewRouter, legacy.NewRouter} {
+		for i, newRouter := range []func(*openapi3.T, ...openapi3.ValidationOption) (routers.Router, error){gorillamuxNewRouterWrapped, legacy.NewRouter} {
 			t.Logf("using NewRouter from %s", map[int]string{0: "gorillamux", 1: "legacy"}[i])
 			router, err := newRouter(doc)
 			require.NoError(t, err)
diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go
index d4779f5..e9b8759 100644
--- a/routers/legacy/router_test.go
+++ b/routers/legacy/router_test.go
@@ -193,4 +193,21 @@ func TestRouter(t *testing.T) {
 		require.Nil(t, route)
 		require.Nil(t, pathParams)
 	}
+
+	schema := &openapi3.Schema{
+		Type:    "string",
+		Example: 3,
+	}
+	content := openapi3.NewContentWithJSONSchema(schema)
+	responses := openapi3.NewResponses()
+	responses["default"].Value.Content = content
+	doc.Paths["/withExamples"] = &openapi3.PathItem{
+		Get: &openapi3.Operation{Responses: responses},
+	}
+	err = doc.Validate(context.Background())
+	require.Error(t, err)
+	r, err = NewRouter(doc)
+	require.Error(t, err)
+	r, err = NewRouter(doc, openapi3.DisableExamplesValidation())
+	require.NoError(t, err)
 }