openapi3filter

package
v0.0.0-...-41f97e3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2024 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package openapi3filter validates that requests and inputs request an OpenAPI 3 specification file.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"sort"
	"strings"

	"github.com/LianYun/openapi3/openapi3"
	"github.com/LianYun/openapi3/openapi3filter"
	"github.com/LianYun/openapi3/routers/gorillamux"
)

func main() {
	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromFile("./testdata/petstore.yaml")
	if err != nil {
		panic(err)
	}

	if err = doc.Validate(loader.Context); 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?num=0", 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)

}

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
			field := prefixBody
			if path := err.JSONPointer(); len(path) > 0 {
				field = fmt.Sprintf("%s.%s", field, strings.Join(path, "."))
			}
			issues[field] = append(issues[field], err.Error())
		case *openapi3filter.RequestError: // possible there were multiple issues that failed validation

			// check if invalid HTTP parameter
			if err.Parameter != nil {
				prefix := err.Parameter.In
				name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name)
				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 {
				issues[prefixBody] = append(issues[prefixBody], err.Error())
				continue
			}
		default:
			const unknown = "@unknown"
			issues[unknown] = append(issues[unknown], err.Error())
		}
	}
	return issues
}
Output:

===== Start New Error =====
@body.name:
	Error at "/name": value must be a string
Schema:
  {
    "example": "doggie",
    "type": "string"
  }

Value:
  100

===== Start New Error =====
@body.status:
	Error at "/status": value is not one of the allowed values ["available","pending","sold"]
Schema:
  {
    "description": "pet status in the store",
    "enum": [
      "available",
      "pending",
      "sold"
    ],
    "type": "string"
  }

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 {}
Example (ValidateMultipartFormData)
package main

import (
	"bytes"
	"context"
	"io"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"strings"

	"github.com/LianYun/openapi3/openapi3"
	"github.com/LianYun/openapi3/openapi3filter"
	"github.com/LianYun/openapi3/routers/gorillamux"
)

func main() {
	const spec = `
openapi: 3.0.0
info:
  title: 'Validator'
  version: 0.0.1
paths:
  /test:
    post:
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                categories:
                  type: array
                  items:
                    $ref: "#/components/schemas/Category"
      responses:
        '200':
          description: Created

components:
  schemas:
    Category:
      type: object
      properties:
        name:
          type: string
      required:
        - name
`

	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromData([]byte(spec))
	if err != nil {
		panic(err)
	}
	if err = doc.Validate(loader.Context); err != nil {
		panic(err)
	}

	router, err := gorillamux.NewRouter(doc)
	if err != nil {
		panic(err)
	}

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	{ // Add a single "categories" item as part data
		h := make(textproto.MIMEHeader)
		h.Set("Content-Disposition", `form-data; name="categories"`)
		h.Set("Content-Type", "application/json")
		fw, err := writer.CreatePart(h)
		if err != nil {
			panic(err)
		}
		if _, err = io.Copy(fw, strings.NewReader(`{"name": "foo"}`)); err != nil {
			panic(err)
		}
	}

	{ // Add a single "categories" item as part data, again
		h := make(textproto.MIMEHeader)
		h.Set("Content-Disposition", `form-data; name="categories"`)
		h.Set("Content-Type", "application/json")
		fw, err := writer.CreatePart(h)
		if err != nil {
			panic(err)
		}
		if _, err = io.Copy(fw, strings.NewReader(`{"name": "bar"}`)); err != nil {
			panic(err)
		}
	}

	{ // Add file data
		fw, err := writer.CreateFormFile("file", "hello.txt")
		if err != nil {
			panic(err)
		}
		if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil {
			panic(err)
		}
	}

	writer.Close()

	req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())

	route, pathParams, err := router.FindRoute(req)
	if err != nil {
		panic(err)
	}

	if err = openapi3filter.ValidateRequestBody(
		context.Background(),
		&openapi3filter.RequestValidationInput{
			Request:    req,
			PathParams: pathParams,
			Route:      route,
		},
		route.Operation.RequestBody.Value,
	); err != nil {
		panic(err)
	}
}
Output:

Index

Examples

Constants

View Source
const (
	// ErrCodeOK indicates no error. It is also the default value.
	ErrCodeOK = 0
	// ErrCodeCannotFindRoute happens when the validator fails to resolve the
	// request to a defined OpenAPI route.
	ErrCodeCannotFindRoute = iota
	// ErrCodeRequestInvalid happens when the inbound request does not conform
	// to the OpenAPI 3 specification.
	ErrCodeRequestInvalid = iota
	// ErrCodeResponseInvalid happens when the wrapped handler response does
	// not conform to the OpenAPI 3 specification.
	ErrCodeResponseInvalid = iota
)

Variables

View Source
var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc")

ErrAuthenticationServiceMissing is returned when no authentication service is defined for the request validator

View Source
var ErrInvalidEmptyValue = errors.New("empty value is not allowed")

ErrInvalidEmptyValue is returned when a value of a parameter or request body is empty while it's not allowed.

View Source
var ErrInvalidRequired = errors.New("value is required but missing")

ErrInvalidRequired is returned when a required value of a parameter or request body is not defined.

View Source
var JSONPrefixes = []string{
	")]}',\n",
}

Functions

func ConvertErrors

func ConvertErrors(err error) error

ConvertErrors converts all errors to the appropriate error format.

func DefaultErrorEncoder

func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter)

DefaultErrorEncoder writes the error to the ResponseWriter, by default a content type of text/plain, a body of the plain text of the error, and a status code of 500. If the error implements Headerer, the provided headers will be applied to the response. If the error implements json.Marshaler, and the marshaling succeeds, a content type of application/json and the JSON encoded form of the error will be used. If the error implements StatusCoder, the provided StatusCode will be used instead of 500.

func FileBodyDecoder

func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error)

FileBodyDecoder is a body decoder that decodes a file body to a string.

func JSONBodyDecoder

func JSONBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error)

JSONBodyDecoder decodes a JSON formatted body. It is public so that is easy to register additional JSON based formats.

func NoopAuthenticationFunc

func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error

NoopAuthenticationFunc is an AuthenticationFunc

func RegisterBodyDecoder

func RegisterBodyDecoder(contentType string, decoder BodyDecoder)

RegisterBodyDecoder registers a request body's decoder for a content type.

If a decoder for the specified content type already exists, the function replaces it with the specified decoder. This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.

func RegisterBodyEncoder

func RegisterBodyEncoder(contentType string, encoder BodyEncoder)

RegisterBodyEncoder enables package-wide decoding of contentType values

func TrimJSONPrefix

func TrimJSONPrefix(data []byte) []byte

TrimJSONPrefix trims one of the possible prefixes

func UnregisterBodyDecoder

func UnregisterBodyDecoder(contentType string)

UnregisterBodyDecoder dissociates a body decoder from a content type.

Decoding this content type will result in an error. This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.

func UnregisterBodyEncoder

func UnregisterBodyEncoder(contentType string)

UnregisterBodyEncoder disables package-wide decoding of contentType values

func ValidateParameter

func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error

ValidateParameter validates a parameter's value by JSON schema. The function returns RequestError with a ParseError cause when unable to parse a value. The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined. The function returns RequestError with ErrInvalidEmptyValue cause when a value of a required parameter is not defined. The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.

func ValidateRequest

func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error)

ValidateRequest is used to validate the given input according to previous loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a non-nil error will be returned.

Note: One can tune the behavior of uniqueItems: true verification by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker

func ValidateRequestBody

func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error

ValidateRequestBody validates data of a request's body.

The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined. The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.

func ValidateResponse

func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error

ValidateResponse is used to validate the given input according to previous loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a non-nil error will be returned.

Note: One can tune the behavior of uniqueItems: true verification by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker

func ValidateSecurityRequirements

func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error

ValidateSecurityRequirements goes through multiple OpenAPI 3 security requirements in order and returns nil on the first valid requirement. If no requirement is met, errors are returned in order.

Types

type AuthenticationFunc

type AuthenticationFunc func(context.Context, *AuthenticationInput) error

AuthenticationFunc allows for custom security requirement validation. A non-nil error fails authentication according to https://spec.openapis.org/oas/v3.1.0#security-requirement-object See ValidateSecurityRequirements

Example
const spec = `
openapi: 3.0.0
info:
  title: 'Validator'
  version: 0.0.1
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: /oauth2/token
          scopes:
            secrets.read: Ability to read secrets
            secrets.write: Ability to write secrets
paths:
  /secret:
    post:
      security:
        - OAuth2:
          - secrets.write
      responses:
        '200':
          description: Ok
        '401':
          description: Unauthorized
`
var (
	errUnauthenticated = errors.New("login required")
	errForbidden       = errors.New("permission denied")
)

userScopes := map[string][]string{
	"Alice": {"secrets.read"},
	"Bob":   {"secrets.read", "secrets.write"},
}

authenticationFunc := func(_ context.Context, ai *AuthenticationInput) error {
	user := ai.RequestValidationInput.Request.Header.Get("X-User")
	if user == "" {
		return errUnauthenticated
	}

	for _, requiredScope := range ai.Scopes {
		var allowed bool
		for _, scope := range userScopes[user] {
			if scope == requiredScope {
				allowed = true
				break
			}
		}
		if !allowed {
			return errForbidden
		}
	}

	return nil
}

loader := openapi3.NewLoader()
doc, _ := loader.LoadFromData([]byte(spec))
router, _ := gorillamux.NewRouter(doc)

validateRequest := func(req *http.Request) {
	route, pathParams, _ := router.FindRoute(req)

	validationInput := &RequestValidationInput{
		Request:    req,
		PathParams: pathParams,
		Route:      route,
		Options: &Options{
			AuthenticationFunc: authenticationFunc,
		},
	}
	err := ValidateRequest(context.TODO(), validationInput)
	switch {
	case errors.Is(err, errUnauthenticated):
		fmt.Println("username is required")
	case errors.Is(err, errForbidden):
		fmt.Println("user is not allowed to perform this action")
	case err == nil:
		fmt.Println("ok")
	default:
		log.Fatal(err)
	}
}

req1, _ := http.NewRequest(http.MethodPost, "/secret", nil)
req1.Header.Set("X-User", "Alice")

req2, _ := http.NewRequest(http.MethodPost, "/secret", nil)
req2.Header.Set("X-User", "Bob")

req3, _ := http.NewRequest(http.MethodPost, "/secret", nil)

validateRequest(req1)
validateRequest(req2)
validateRequest(req3)
Output:

user is not allowed to perform this action
ok
username is required

type AuthenticationInput

type AuthenticationInput struct {
	RequestValidationInput *RequestValidationInput
	SecuritySchemeName     string
	SecurityScheme         *openapi3.SecurityScheme
	Scopes                 []string
}

func (*AuthenticationInput) NewError

func (input *AuthenticationInput) NewError(err error) error

type BodyDecoder

type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error)

BodyDecoder is an interface to decode a body of a request or response. An implementation must return a value that is a primitive, []interface{}, or map[string]interface{}.

func RegisteredBodyDecoder

func RegisteredBodyDecoder(contentType string) BodyDecoder

RegisteredBodyDecoder returns the registered body decoder for the given content type.

If no decoder was registered for the given content type, nil is returned. This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.

type BodyEncoder

type BodyEncoder func(body interface{}) ([]byte, error)

BodyEncoder really is an (encoding/json).Marshaler

func RegisteredBodyEncoder

func RegisteredBodyEncoder(contentType string) BodyEncoder

RegisteredBodyEncoder returns the registered body encoder for the given content type.

If no encoder was registered for the given content type, nil is returned.

type ContentParameterDecoder

type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error)

A ContentParameterDecoder takes a parameter definition from the OpenAPI spec, and the value which we received for it. It is expected to return the value unmarshaled into an interface which can be traversed for validation, it should also return the schema to be used for validating the object, since there can be more than one in the content spec.

If a query parameter appears multiple times, values[] will have more than one value, but for all other parameter types it should have just one.

type CustomSchemaErrorFunc

type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string

CustomSchemaErrorFunc allows for custom the schema error message.

type EncodingFn

type EncodingFn func(partName string) *openapi3.Encoding

EncodingFn is a function that returns an encoding of a request body's part.

type ErrCode

type ErrCode int

ErrCode is used for classification of different types of errors that may occur during validation. These may be used to write an appropriate response in ErrFunc.

type ErrFunc

type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error)

ErrFunc handles errors that may occur during validation.

type ErrorEncoder

type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)

ErrorEncoder is responsible for encoding an error to the ResponseWriter. Users are encouraged to use custom ErrorEncoders to encode HTTP errors to their clients, and will likely want to pass and check for their own error types. See the example shipping/handling service.

type Headerer

type Headerer interface {
	Headers() http.Header
}

Headerer is checked by DefaultErrorEncoder. If an error value implements Headerer, the provided headers will be applied to the response writer, after the Content-Type is set.

type LogFunc

type LogFunc func(message string, err error)

LogFunc handles log messages that may occur during validation.

type Options

type Options struct {
	// Set ExcludeRequestBody so ValidateRequest skips request body validation
	ExcludeRequestBody bool

	// Set ExcludeRequestQueryParams so ValidateRequest skips request query params validation
	ExcludeRequestQueryParams bool

	// Set ExcludeResponseBody so ValidateResponse skips response body validation
	ExcludeResponseBody bool

	// Set ExcludeReadOnlyValidations so ValidateRequest skips read-only validations
	ExcludeReadOnlyValidations bool

	// Set ExcludeWriteOnlyValidations so ValidateResponse skips write-only validations
	ExcludeWriteOnlyValidations bool

	// Set IncludeResponseStatus so ValidateResponse fails on response
	// status not defined in OpenAPI spec
	IncludeResponseStatus bool

	MultiError bool

	// A document with security schemes defined will not pass validation
	// unless an AuthenticationFunc is defined.
	// See NoopAuthenticationFunc
	AuthenticationFunc AuthenticationFunc

	// Indicates whether default values are set in the
	// request. If true, then they are not set
	SkipSettingDefaults bool
	// contains filtered or unexported fields
}

Options used by ValidateRequest and ValidateResponse

func (*Options) WithCustomSchemaErrorFunc

func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc)

WithCustomSchemaErrorFunc sets a function to override the schema error message. If the passed function returns an empty string, it returns to the previous Error() implementation.

Example
package main

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/LianYun/openapi3/openapi3"
	"github.com/LianYun/openapi3/openapi3filter"
	"github.com/LianYun/openapi3/routers/gorillamux"
)

func main() {
	const spec = `
openapi: 3.0.0
info:
  title: 'Validator'
  version: 0.0.1
paths:
  /some:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                field:
                  title: Some field
                  type: integer
      responses:
        '200':
          description: Created
`

	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromData([]byte(spec))
	if err != nil {
		panic(err)
	}

	if err = doc.Validate(loader.Context); err != nil {
		panic(err)
	}

	router, err := gorillamux.NewRouter(doc)
	if err != nil {
		panic(err)
	}

	opts := &openapi3filter.Options{}

	opts.WithCustomSchemaErrorFunc(func(err *openapi3.SchemaError) string {
		return fmt.Sprintf(`field "%s" must be an integer`, err.Schema.Title)
	})

	req, err := http.NewRequest(http.MethodPost, "/some", strings.NewReader(`{"field":"not integer"}`))
	if err != nil {
		panic(err)
	}

	req.Header.Add("Content-Type", "application/json")

	route, pathParams, err := router.FindRoute(req)
	if err != nil {
		panic(err)
	}

	validationInput := &openapi3filter.RequestValidationInput{
		Request:    req,
		PathParams: pathParams,
		Route:      route,
		Options:    opts,
	}
	err = openapi3filter.ValidateRequest(context.Background(), validationInput)

	fmt.Println(err.Error())

}
Output:

request body has an error: doesn't match schema: field "Some field" must be an integer

type ParseError

type ParseError struct {
	Kind   ParseErrorKind
	Value  interface{}
	Reason string
	Cause  error
	// contains filtered or unexported fields
}

ParseError describes errors which happens while parse operation's parameters, requestBody, or response.

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Path

func (e *ParseError) Path() []interface{}

Path returns a path to the root cause.

func (*ParseError) RootCause

func (e *ParseError) RootCause() error

RootCause returns a root cause of ParseError.

func (ParseError) Unwrap

func (e ParseError) Unwrap() error

type ParseErrorKind

type ParseErrorKind int

ParseErrorKind describes a kind of ParseError. The type simplifies comparison of errors.

const (
	// KindOther describes an untyped parsing error.
	KindOther ParseErrorKind = iota
	// KindUnsupportedFormat describes an error that happens when a value has an unsupported format.
	KindUnsupportedFormat
	// KindInvalidFormat describes an error that happens when a value does not conform a format
	// that is required by a serialization method.
	KindInvalidFormat
)

type RequestError

type RequestError struct {
	Input       *RequestValidationInput
	Parameter   *openapi3.Parameter
	RequestBody *openapi3.RequestBody
	Reason      string
	Err         error
}

RequestError is returned by ValidateRequest when request does not match OpenAPI spec

func (*RequestError) Error

func (err *RequestError) Error() string

func (RequestError) Unwrap

func (err RequestError) Unwrap() error

type RequestValidationInput

type RequestValidationInput struct {
	Request      *http.Request
	PathParams   map[string]string
	QueryParams  url.Values
	Route        *routers.Route
	Options      *Options
	ParamDecoder ContentParameterDecoder
}

func (*RequestValidationInput) GetQueryParams

func (input *RequestValidationInput) GetQueryParams() url.Values

type ResponseError

type ResponseError struct {
	Input  *ResponseValidationInput
	Reason string
	Err    error
}

ResponseError is returned by ValidateResponse when response does not match OpenAPI spec

func (*ResponseError) Error

func (err *ResponseError) Error() string

func (ResponseError) Unwrap

func (err ResponseError) Unwrap() error

type ResponseValidationInput

type ResponseValidationInput struct {
	RequestValidationInput *RequestValidationInput
	Status                 int
	Header                 http.Header
	Body                   io.ReadCloser
	Options                *Options
}

func (*ResponseValidationInput) SetBodyBytes

func (input *ResponseValidationInput) SetBodyBytes(value []byte) *ResponseValidationInput

type SecurityRequirementsError

type SecurityRequirementsError struct {
	SecurityRequirements openapi3.SecurityRequirements
	Errors               []error
}

SecurityRequirementsError is returned by ValidateSecurityRequirements when no requirement is met.

func (*SecurityRequirementsError) Error

func (err *SecurityRequirementsError) Error() string

func (SecurityRequirementsError) Unwrap

func (err SecurityRequirementsError) Unwrap() []error

type StatusCoder

type StatusCoder interface {
	StatusCode() int
}

StatusCoder is checked by DefaultErrorEncoder. If an error value implements StatusCoder, the StatusCode will be used when encoding the error. By default, StatusInternalServerError (500) is used.

type ValidationError

type ValidationError struct {
	// A unique identifier for this particular occurrence of the problem.
	Id string `json:"id,omitempty" yaml:"id,omitempty"`
	// The HTTP status code applicable to this problem.
	Status int `json:"status,omitempty" yaml:"status,omitempty"`
	// An application-specific error code, expressed as a string value.
	Code string `json:"code,omitempty" yaml:"code,omitempty"`
	// A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.
	Title string `json:"title,omitempty" yaml:"title,omitempty"`
	// A human-readable explanation specific to this occurrence of the problem.
	Detail string `json:"detail,omitempty" yaml:"detail,omitempty"`
	// An object containing references to the source of the error
	Source *ValidationErrorSource `json:"source,omitempty" yaml:"source,omitempty"`
}

ValidationError struct provides granular error information useful for communicating issues back to end user and developer. Based on https://jsonapi.org/format/#error-objects

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface.

func (*ValidationError) StatusCode

func (e *ValidationError) StatusCode() int

StatusCode implements the StatusCoder interface for DefaultErrorEncoder

type ValidationErrorEncoder

type ValidationErrorEncoder struct {
	Encoder ErrorEncoder
}

ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors

func (*ValidationErrorEncoder) Encode

Encode implements the ErrorEncoder interface for encoding ValidationErrors

type ValidationErrorSource

type ValidationErrorSource struct {
	// A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].
	Pointer string `json:"pointer,omitempty" yaml:"pointer,omitempty"`
	// A string indicating which query parameter caused the error.
	Parameter string `json:"parameter,omitempty" yaml:"parameter,omitempty"`
}

ValidationErrorSource struct

type ValidationHandler

type ValidationHandler struct {
	Handler            http.Handler
	AuthenticationFunc AuthenticationFunc
	File               string
	ErrorEncoder       ErrorEncoder
	// contains filtered or unexported fields
}

func (*ValidationHandler) Load

func (h *ValidationHandler) Load() error

func (*ValidationHandler) Middleware

func (h *ValidationHandler) Middleware(next http.Handler) http.Handler

Middleware implements gorilla/mux MiddlewareFunc

func (*ValidationHandler) ServeHTTP

func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator provides HTTP request and response validation middleware.

Example
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"path"
	"strconv"
	"strings"

	"github.com/LianYun/openapi3/openapi3"
	"github.com/LianYun/openapi3/openapi3filter"
	"github.com/LianYun/openapi3/routers/gorillamux"
)

func main() {
	// OpenAPI specification for a simple service that squares integers, with
	// some limitations.
	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromData([]byte(`
openapi: 3.0.0
info:
  title: 'Validator - square example'
  version: '0.0.0'
paths:
  /square/{x}:
    get:
      description: square an integer
      parameters:
        - name: x
          in: path
          schema:
            type: integer
          required: true
      responses:
        '200':
          description: squared integer response
          content:
            "application/json":
              schema:
                type: object
                properties:
                  result:
                    type: integer
                    minimum: 0
                    maximum: 1000000
                required: [result]
                additionalProperties: false
`[1:]))
	if err != nil {
		panic(err)
	}

	// Make sure that OpenAPI document is correct
	if err = doc.Validate(loader.Context); err != nil {
		panic(err)
	}

	// Square service handler sanity checks inputs, but just crashes on invalid
	// requests.
	squareHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		xParam := path.Base(r.URL.Path)
		x, err := strconv.ParseInt(xParam, 10, 64)
		if err != nil {
			panic(err)
		}
		w.Header().Set("Content-Type", "application/json")
		result := map[string]interface{}{"result": x * x}
		if x == 42 {
			// An easter egg. Unfortunately, the spec does not allow additional properties...
			result["comment"] = "the answer to the ultimate question of life, the universe, and everything"
		}
		if err = json.NewEncoder(w).Encode(&result); err != nil {
			panic(err)
		}
	})

	// Start an http server.
	var mainHandler http.Handler
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Why are we wrapping the main server handler with a closure here?
		// Validation matches request Host: to server URLs in the spec. With an
		// httptest.Server, the URL is dynamic and we have to create it first!
		// In a real configured service, this is less likely to be needed.
		mainHandler.ServeHTTP(w, r)
	}))
	defer srv.Close()

	// Patch the OpenAPI spec to match the httptest.Server.URL. Only needed
	// because the server URL is dynamic here.
	doc.Servers = []*openapi3.Server{{URL: srv.URL}}
	if err := doc.Validate(loader.Context); err != nil { // Assert our OpenAPI is valid!
		panic(err)
	}
	// This router is used by the validator to match requests with the OpenAPI
	// spec. It does not place restrictions on how the wrapped handler routes
	// requests; use of gorilla/mux is just a validator implementation detail.
	router, err := gorillamux.NewRouter(doc)
	if err != nil {
		panic(err)
	}
	// Strict validation will respond HTTP 500 if the service tries to emit a
	// response that does not conform to the OpenAPI spec. Very useful for
	// testing a service against its spec in development and CI. In production,
	// availability may be more important than strictness.
	v := openapi3filter.NewValidator(router, openapi3filter.Strict(true),
		openapi3filter.OnErr(func(w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) {
			// Customize validation error responses to use JSON
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(status)
			json.NewEncoder(w).Encode(map[string]interface{}{
				"status":  status,
				"message": http.StatusText(status),
			})
		}))
	// Now we can finally set the main server handler.
	mainHandler = v.Middleware(squareHandler)

	printResp := func(resp *http.Response, err error) {
		if err != nil {
			panic(err)
		}
		defer resp.Body.Close()
		contents, err := io.ReadAll(resp.Body)
		if err != nil {
			panic(err)
		}
		fmt.Println(resp.StatusCode, strings.TrimSpace(string(contents)))
	}
	// Valid requests to our sum service
	printResp(srv.Client().Get(srv.URL + "/square/2"))
	printResp(srv.Client().Get(srv.URL + "/square/789"))
	// 404 Not found requests - method or path not found
	printResp(srv.Client().Post(srv.URL+"/square/2", "application/json", bytes.NewBufferString(`{"result": 5}`)))
	printResp(srv.Client().Get(srv.URL + "/sum/2"))
	printResp(srv.Client().Get(srv.URL + "/square/circle/4")) // Handler would process this; validation rejects it
	printResp(srv.Client().Get(srv.URL + "/square"))
	// 400 Bad requests - note they never reach the wrapped square handler (which would panic)
	printResp(srv.Client().Get(srv.URL + "/square/five"))
	// 500 Invalid responses
	printResp(srv.Client().Get(srv.URL + "/square/42"))    // Our "easter egg" added a property which is not allowed
	printResp(srv.Client().Get(srv.URL + "/square/65536")) // Answer overflows the maximum allowed value (1000000)
}
Output:

200 {"result":4}
200 {"result":622521}
404 {"message":"Not Found","status":404}
404 {"message":"Not Found","status":404}
404 {"message":"Not Found","status":404}
404 {"message":"Not Found","status":404}
400 {"message":"Bad Request","status":400}
500 {"message":"Internal Server Error","status":500}
500 {"message":"Internal Server Error","status":500}

func NewValidator

func NewValidator(router routers.Router, options ...ValidatorOption) *Validator

NewValidator returns a new response validation middleware, using the given routes from an OpenAPI 3 specification.

func (*Validator) Middleware

func (v *Validator) Middleware(h http.Handler) http.Handler

Middleware returns an http.Handler which wraps the given handler with request and response validation.

type ValidatorOption

type ValidatorOption func(*Validator)

ValidatorOption defines an option that may be specified when creating a Validator.

func OnErr

func OnErr(f ErrFunc) ValidatorOption

OnErr provides a callback that handles writing an HTTP response on a validation error. This allows customization of error responses without prescribing a particular form. This callback is only called on response validator errors in Strict mode.

func OnLog

func OnLog(f LogFunc) ValidatorOption

OnLog provides a callback that handles logging in the Validator. This allows the validator to integrate with a services' existing logging system without prescribing a particular one.

func Strict

func Strict(strict bool) ValidatorOption

Strict, if set, causes an internal server error to be sent if the wrapped handler response fails response validation. If not set, the response is sent and the error is only logged.

func ValidationOptions

func ValidationOptions(options Options) ValidatorOption

ValidationOptions sets request/response validation options on the validator.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL