Repository: getkin/kin-openapi (go)

Issue:
Header type should have the same fields as Parameter
Hi,

I'm dinning in OpenAPIv3 spec and just figured out that Header object fields should be the same as in Parameter.

[Header Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#headerObject) :
```
Header Object

The Header Object follows the structure of the Parameter Object with the following changes:

    name MUST NOT be specified, it is given in the corresponding headers map.
    in MUST NOT be specified, it is implicitly in header.
    All traits that are affected by the location MUST be applicable to a location of header (for example, style).
```


Pull request description:
Have Header Object follow the structure of the Parameter Object
Fixes #46

@pruser @danielvladco can you confirm this meets your needs? I'm asking specifically about the awkward way to construct a `Header.Value`:
```go
			Headers: map[string]*HeaderRef{
				"someHeader": {
					Ref: "#/components/headers/otherHeader",
				},
				"otherHeader": {
					Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}},
				},
			},
```

Patch:
diff --git a/openapi3/header.go b/openapi3/header.go
index 7cf61f8..5fdc317 100644
--- a/openapi3/header.go
+++ b/openapi3/header.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
 	"github.com/getkin/kin-openapi/jsoninfo"
@@ -24,17 +25,10 @@ func (h Headers) JSONLookup(token string) (interface{}, error) {
 	return ref.Value, nil
 }
 
+// Header is specified by OpenAPI/Swagger 3.0 standard.
+// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#headerObject
 type Header struct {
-	ExtensionProps
-
-	// Optional description. Should use CommonMark syntax.
-	Description string      `json:"description,omitempty" yaml:"description,omitempty"`
-	Deprecated  bool        `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
-	Required    bool        `json:"required,omitempty" yaml:"required,omitempty"`
-	Schema      *SchemaRef  `json:"schema,omitempty" yaml:"schema,omitempty"`
-	Example     interface{} `json:"example,omitempty" yaml:"example,omitempty"`
-	Examples    Examples    `json:"examples,omitempty" yaml:"examples,omitempty"`
-	Content     Content     `json:"content,omitempty" yaml:"content,omitempty"`
+	Parameter
 }
 
 var _ jsonpointer.JSONPointable = (*Header)(nil)
@@ -43,10 +37,52 @@ func (value *Header) UnmarshalJSON(data []byte) error {
 	return jsoninfo.UnmarshalStrictStruct(data, value)
 }
 
+// SerializationMethod returns a header's serialization method.
+func (value *Header) SerializationMethod() (*SerializationMethod, error) {
+	style := value.Style
+	if style == "" {
+		style = SerializationSimple
+	}
+	explode := false
+	if value.Explode != nil {
+		explode = *value.Explode
+	}
+	return &SerializationMethod{Style: style, Explode: explode}, nil
+}
+
 func (value *Header) Validate(ctx context.Context) error {
-	if v := value.Schema; v != nil {
-		if err := v.Validate(ctx); err != nil {
-			return err
+	if value.Name != "" {
+		return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map")
+	}
+	if value.In != "" {
+		return errors.New("header 'in' MUST NOT be specified, it is implicitly in header")
+	}
+
+	// Validate a parameter's serialization method.
+	sm, err := value.SerializationMethod()
+	if err != nil {
+		return err
+	}
+	if smSupported := false ||
+		sm.Style == SerializationSimple && !sm.Explode ||
+		sm.Style == SerializationSimple && sm.Explode; !smSupported {
+		e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a header parameter", sm.Style, sm.Explode)
+		return fmt.Errorf("header schema is invalid: %v", e)
+	}
+
+	if (value.Schema == nil) == (value.Content == nil) {
+		e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", value)
+		return fmt.Errorf("header schema is invalid: %v", e)
+	}
+	if schema := value.Schema; schema != nil {
+		if err := schema.Validate(ctx); err != nil {
+			return fmt.Errorf("header schema is invalid: %v", err)
+		}
+	}
+
+	if content := value.Content; content != nil {
+		if err := content.Validate(ctx); err != nil {
+			return fmt.Errorf("header content is invalid: %v", err)
 		}
 	}
 	return nil
@@ -61,8 +97,20 @@ func (value Header) JSONLookup(token string) (interface{}, error) {
 			}
 			return value.Schema.Value, nil
 		}
+	case "name":
+		return value.Name, nil
+	case "in":
+		return value.In, nil
 	case "description":
 		return value.Description, nil
+	case "style":
+		return value.Style, nil
+	case "explode":
+		return value.Explode, nil
+	case "allowEmptyValue":
+		return value.AllowEmptyValue, nil
+	case "allowReserved":
+		return value.AllowReserved, nil
 	case "deprecated":
 		return value.Deprecated, nil
 	case "required":
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index f4b91ad..2081e4e 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -83,6 +83,7 @@ func (value Parameters) Validate(ctx context.Context) error {
 }
 
 // Parameter is specified by OpenAPI/Swagger 3.0 standard.
+// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#parameterObject
 type Parameter struct {
 	ExtensionProps
 	Name            string      `json:"name,omitempty" yaml:"name,omitempty"`
@@ -167,42 +168,42 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
 	return jsoninfo.UnmarshalStrictStruct(data, parameter)
 }
 
-func (parameter Parameter) JSONLookup(token string) (interface{}, error) {
+func (value Parameter) JSONLookup(token string) (interface{}, error) {
 	switch token {
 	case "schema":
-		if parameter.Schema != nil {
-			if parameter.Schema.Ref != "" {
-				return &Ref{Ref: parameter.Schema.Ref}, nil
+		if value.Schema != nil {
+			if value.Schema.Ref != "" {
+				return &Ref{Ref: value.Schema.Ref}, nil
 			}
-			return parameter.Schema.Value, nil
+			return value.Schema.Value, nil
 		}
 	case "name":
-		return parameter.Name, nil
+		return value.Name, nil
 	case "in":
-		return parameter.In, nil
+		return value.In, nil
 	case "description":
-		return parameter.Description, nil
+		return value.Description, nil
 	case "style":
-		return parameter.Style, nil
+		return value.Style, nil
 	case "explode":
-		return parameter.Explode, nil
+		return value.Explode, nil
 	case "allowEmptyValue":
-		return parameter.AllowEmptyValue, nil
+		return value.AllowEmptyValue, nil
 	case "allowReserved":
-		return parameter.AllowReserved, nil
+		return value.AllowReserved, nil
 	case "deprecated":
-		return parameter.Deprecated, nil
+		return value.Deprecated, nil
 	case "required":
-		return parameter.Required, nil
+		return value.Required, nil
 	case "example":
-		return parameter.Example, nil
+		return value.Example, nil
 	case "examples":
-		return parameter.Examples, nil
+		return value.Examples, nil
 	case "content":
-		return parameter.Content, nil
+		return value.Content, nil
 	}
 
-	v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token)
+	v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token)
 	return v, err
 }
 
@@ -294,6 +295,7 @@ func (value *Parameter) Validate(ctx context.Context) error {
 			return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err)
 		}
 	}
+
 	if content := value.Content; content != nil {
 		if err := content.Validate(ctx); err != nil {
 			return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err)


Test patch:
diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go
index 4c6bfe3..38d488d 100644
--- a/openapi3/openapi3_test.go
+++ b/openapi3/openapi3_test.go
@@ -130,7 +130,8 @@ components:
     someSchema:
       description: Some schema
   headers:
-    otherHeader: {}
+    otherHeader:
+      schema: {type: string}
     someHeader:
       "$ref": "#/components/headers/otherHeader"
   examples:
@@ -207,7 +208,11 @@ var specJSON = []byte(`
       }
     },
     "headers": {
-      "otherHeader": {},
+      "otherHeader": {
+        "schema": {
+          "type": "string"
+      	}
+      },
       "someHeader": {
         "$ref": "#/components/headers/otherHeader"
       }
@@ -317,7 +322,7 @@ func spec() *T {
 					Ref: "#/components/headers/otherHeader",
 				},
 				"otherHeader": {
-					Value: &Header{},
+					Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}},
 				},
 			},
 			Examples: map[string]*ExampleRef{
diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml
new file mode 100644
index 0000000..e3b61bf
--- /dev/null
+++ b/openapi3filter/testdata/petstore.yaml
@@ -0,0 +1,100 @@
+openapi: "3.0.0"
+info:
+  description: "This is a sample server Petstore server.  You can find out more about     Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).      For this sample, you can use the api key `special-key` to test the authorization     filters."
+  version: "1.0.0"
+  title: "Swagger Petstore"
+  termsOfService: "http://swagger.io/terms/"
+  contact:
+    email: "apiteam@swagger.io"
+  license:
+    name: "Apache 2.0"
+    url: "http://www.apache.org/licenses/LICENSE-2.0.html"
+tags:
+- name: "pet"
+  description: "Everything about your Pets"
+  externalDocs:
+    description: "Find out more"
+    url: "http://swagger.io"
+- name: "store"
+  description: "Access to Petstore orders"
+- name: "user"
+  description: "Operations about user"
+  externalDocs:
+    description: "Find out more about our store"
+    url: "http://swagger.io"
+paths:
+  /pet:
+    post:
+      tags:
+      - "pet"
+      summary: "Add a new pet to the store"
+      description: ""
+      operationId: "addPet"
+      requestBody:
+        required: true
+        content: 
+          'application/json':
+            schema:
+              $ref: '#/components/schemas/Pet'
+      responses:
+        "405":
+          description: "Invalid input"
+components:
+  schemas:
+    Category:
+      type: "object"
+      properties:
+        id:
+          type: "integer"
+          format: "int64"
+        name:
+          type: "string"
+      xml:
+        name: "Category"
+    Tag:
+      type: "object"
+      properties:
+        id:
+          type: "integer"
+          format: "int64"
+        name:
+          type: "string"
+      xml:
+        name: "Tag"
+    Pet:
+      type: "object"
+      required:
+      - "name"
+      - "photoUrls"
+      properties:
+        id:
+          type: "integer"
+          format: "int64"
+        category:
+          $ref: "#/components/schemas/Category"
+        name:
+          type: "string"
+          example: "doggie"
+        photoUrls:
+          type: "array"
+          xml:
+            name: "photoUrl"
+            wrapped: true
+          items:
+            type: "string"
+        tags:
+          type: "array"
+          xml:
+            name: "tag"
+            wrapped: true
+          items:
+            $ref: "#/components/schemas/Tag"
+        status:
+          type: "string"
+          description: "pet status in the store"
+          enum:
+          - "available"
+          - "pending"
+          - "sold"
+      xml:
+        name: "Pet"
diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go
new file mode 100644
index 0000000..4242177
--- /dev/null
+++ b/openapi3filter/unpack_errors_test.go
@@ -0,0 +1,173 @@
+package openapi3filter_test
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"sort"
+	"strings"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func Example() {
+	doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml")
+	if err != nil {
+		panic(err)
+	}
+
+	router, err := gorillamux.NewRouter(doc)
+	if err != nil {
+		panic(err)
+	}
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		route, pathParams, err := router.FindRoute(r)
+		if err != nil {
+			fmt.Println(err.Error())
+			w.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+
+		err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{
+			Request:    r,
+			PathParams: pathParams,
+			Route:      route,
+			Options: &openapi3filter.Options{
+				MultiError: true,
+			},
+		})
+		switch err := err.(type) {
+		case nil:
+		case openapi3.MultiError:
+			issues := convertError(err)
+			names := make([]string, 0, len(issues))
+			for k := range issues {
+				names = append(names, k)
+			}
+			sort.Strings(names)
+			for _, k := range names {
+				msgs := issues[k]
+				fmt.Println("===== Start New Error =====")
+				fmt.Println(k + ":")
+				for _, msg := range msgs {
+					fmt.Printf("\t%s\n", msg)
+				}
+			}
+			w.WriteHeader(http.StatusBadRequest)
+		default:
+			fmt.Println(err.Error())
+			w.WriteHeader(http.StatusBadRequest)
+		}
+	}))
+	defer ts.Close()
+
+	// (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)
+	if err != nil {
+		panic(err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		panic(err)
+	}
+	defer resp.Body.Close()
+	fmt.Printf("response: %d %s\n", resp.StatusCode, resp.Body)
+
+	// Output:
+	// ===== Start New Error =====
+	// @body.name:
+	// 	Error at "/name": Field must be set to string or not be present
+	// Schema:
+	//   {
+	//     "example": "doggie",
+	//     "type": "string"
+	//   }
+	//
+	// Value:
+	//   "number, integer"
+	//
+	// ===== Start New Error =====
+	// @body.status:
+	// 	Error at "/status": value is not one of the allowed values
+	// Schema:
+	//   {
+	//     "description": "pet status in the store",
+	//     "enum": [
+	//       "available",
+	//       "pending",
+	//       "sold"
+	//     ],
+	//     "type": "string"
+	//   }
+	//
+	// Value:
+	//   "invalidStatus"
+	//
+	// response: 400 {}
+}
+
+const (
+	prefixBody = "@body"
+	unknown    = "@unknown"
+)
+
+func convertError(me openapi3.MultiError) map[string][]string {
+	issues := make(map[string][]string)
+	for _, err := range me {
+		switch err := err.(type) {
+		case *openapi3.SchemaError:
+			// Can inspect schema validation errors here, e.g. err.Value
+			field := prefixBody
+			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
+			}
+
+			// 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
+		}
+	}
+	return issues
+}
diff --git a/openapi3filter/validate_request_test.go b/routers/legacy/validate_request_test.go
similarity index 95%
rename from openapi3filter/validate_request_test.go
rename to routers/legacy/validate_request_test.go
index 60e3f2e..7737f50 100644
--- a/openapi3filter/validate_request_test.go
+++ b/routers/legacy/validate_request_test.go
@@ -1,4 +1,4 @@
-package openapi3filter_test
+package legacy_test
 
 import (
 	"bytes"
@@ -8,7 +8,7 @@ import (
 
 	"github.com/getkin/kin-openapi/openapi3"
 	"github.com/getkin/kin-openapi/openapi3filter"
-	legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
+	"github.com/getkin/kin-openapi/routers/legacy"
 )
 
 const spec = `
@@ -73,7 +73,7 @@ func Example() {
 		panic(err)
 	}
 
-	router, err := legacyrouter.NewRouter(doc)
+	router, err := legacy.NewRouter(doc)
 	if err != nil {
 		panic(err)
 	}