Merged in feature/openapifixes (pull request #106)
OpenAPI Middleware & Fixes * updates
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
type AuthenticationInput struct {
|
||||
RequestValidationInput *RequestValidationInput
|
||||
SecuritySchemeName string
|
||||
SecurityScheme *openapi3.SecurityScheme
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
func (input *AuthenticationInput) NewError(err error) error {
|
||||
if err == nil {
|
||||
if len(input.Scopes) == 0 {
|
||||
err = fmt.Errorf("security requirement %q failed", input.SecuritySchemeName)
|
||||
} else {
|
||||
err = fmt.Errorf("security requirement %q (scopes: %+v) failed", input.SecuritySchemeName, input.Scopes)
|
||||
}
|
||||
}
|
||||
return &RequestError{
|
||||
Input: input.RequestValidationInput,
|
||||
Reason: "authorization failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
var _ error = &RequestError{}
|
||||
|
||||
// RequestError is returned by ValidateRequest when request does not match OpenAPI spec
|
||||
type RequestError struct {
|
||||
Input *RequestValidationInput
|
||||
Parameter *openapi3.Parameter
|
||||
RequestBody *openapi3.RequestBody
|
||||
Reason string
|
||||
Err error
|
||||
}
|
||||
|
||||
var _ interface{ Unwrap() error } = RequestError{}
|
||||
|
||||
func (err *RequestError) Error() string {
|
||||
reason := err.Reason
|
||||
if e := err.Err; e != nil {
|
||||
if len(reason) == 0 || reason == e.Error() {
|
||||
reason = e.Error()
|
||||
} else {
|
||||
reason += ": " + e.Error()
|
||||
}
|
||||
}
|
||||
if v := err.Parameter; v != nil {
|
||||
return fmt.Sprintf("parameter %q in %s has an error: %s", v.Name, v.In, reason)
|
||||
} else if v := err.RequestBody; v != nil {
|
||||
return fmt.Sprintf("request body has an error: %s", reason)
|
||||
} else {
|
||||
return reason
|
||||
}
|
||||
}
|
||||
|
||||
func (err RequestError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
var _ error = &ResponseError{}
|
||||
|
||||
// ResponseError is returned by ValidateResponse when response does not match OpenAPI spec
|
||||
type ResponseError struct {
|
||||
Input *ResponseValidationInput
|
||||
Reason string
|
||||
Err error
|
||||
}
|
||||
|
||||
var _ interface{ Unwrap() error } = ResponseError{}
|
||||
|
||||
func (err *ResponseError) Error() string {
|
||||
reason := err.Reason
|
||||
if e := err.Err; e != nil {
|
||||
if len(reason) == 0 {
|
||||
reason = e.Error()
|
||||
} else {
|
||||
reason += ": " + e.Error()
|
||||
}
|
||||
}
|
||||
return reason
|
||||
}
|
||||
|
||||
func (err ResponseError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
var _ error = &SecurityRequirementsError{}
|
||||
|
||||
// SecurityRequirementsError is returned by ValidateSecurityRequirements
|
||||
// when no requirement is met.
|
||||
type SecurityRequirementsError struct {
|
||||
SecurityRequirements openapi3.SecurityRequirements
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (err *SecurityRequirementsError) Error() string {
|
||||
buff := bytes.NewBufferString("security requirements failed: ")
|
||||
for i, e := range err.Errors {
|
||||
buff.WriteString(e.Error())
|
||||
if i != len(err.Errors)-1 {
|
||||
buff.WriteString(" | ")
|
||||
}
|
||||
}
|
||||
|
||||
return buff.String()
|
||||
}
|
||||
|
||||
var _ interface{ Unwrap() []error } = SecurityRequirementsError{}
|
||||
|
||||
func (err SecurityRequirementsError) Unwrap() []error {
|
||||
return err.Errors
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseMediaType(contentType string) string {
|
||||
i := strings.IndexByte(contentType, ';')
|
||||
if i < 0 {
|
||||
return contentType
|
||||
}
|
||||
return contentType[:i]
|
||||
}
|
||||
|
||||
func isNilValue(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
switch reflect.TypeOf(value).Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
|
||||
return reflect.ValueOf(value).IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
// Validator provides HTTP request and response validation middleware.
|
||||
type Validator struct {
|
||||
router routers.Router
|
||||
errFunc ErrFunc
|
||||
logFunc LogFunc
|
||||
strict bool
|
||||
options Options
|
||||
}
|
||||
|
||||
// ErrFunc handles errors that may occur during validation.
|
||||
type ErrFunc func(ctx context.Context, w http.ResponseWriter, status int, code ErrCode, err error)
|
||||
|
||||
// LogFunc handles log messages that may occur during validation.
|
||||
type LogFunc func(ctx context.Context, message string, err error)
|
||||
|
||||
// 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 ErrCode int
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
func (e ErrCode) responseText() string {
|
||||
switch e {
|
||||
case ErrCodeOK:
|
||||
return "OK"
|
||||
case ErrCodeCannotFindRoute:
|
||||
return "not found"
|
||||
case ErrCodeRequestInvalid:
|
||||
return "bad request"
|
||||
default:
|
||||
return "server error"
|
||||
}
|
||||
}
|
||||
|
||||
// NewValidator returns a new response validation middleware, using the given
|
||||
// routes from an OpenAPI 3 specification.
|
||||
func NewValidator(router routers.Router, options ...ValidatorOption) *Validator {
|
||||
v := &Validator{
|
||||
router: router,
|
||||
errFunc: func(_ context.Context, w http.ResponseWriter, status int, code ErrCode, _ error) {
|
||||
http.Error(w, code.responseText(), status)
|
||||
},
|
||||
logFunc: func(_ context.Context, message string, err error) {
|
||||
log.Printf("%s: %v", message, err)
|
||||
},
|
||||
}
|
||||
for i := range options {
|
||||
options[i](v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ValidatorOption defines an option that may be specified when creating a
|
||||
// Validator.
|
||||
type ValidatorOption func(*Validator)
|
||||
|
||||
// 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 OnErr(f ErrFunc) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.errFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// 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 OnLog(f LogFunc) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.logFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Strict(strict bool) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.strict = strict
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationOptions sets request/response validation options on the validator.
|
||||
func ValidationOptions(options Options) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.options = options
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware returns an http.Handler which wraps the given handler with
|
||||
// request and response validation.
|
||||
func (v *Validator) Middleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
route, pathParams, err := v.router.FindRoute(r)
|
||||
if err != nil {
|
||||
v.logFunc(ctx, "validation error: failed to find route for "+r.URL.String(), err)
|
||||
v.errFunc(ctx, w, http.StatusNotFound, ErrCodeCannotFindRoute, err)
|
||||
return
|
||||
}
|
||||
requestValidationInput := &RequestValidationInput{
|
||||
Request: r,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
Options: &v.options,
|
||||
}
|
||||
if err = ValidateRequest(ctx, requestValidationInput); err != nil {
|
||||
v.logFunc(ctx, "invalid request", err)
|
||||
v.errFunc(ctx, w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
|
||||
return
|
||||
}
|
||||
|
||||
var wr responseWrapper
|
||||
if v.strict {
|
||||
wr = &strictResponseWrapper{w: w}
|
||||
} else {
|
||||
wr = newWarnResponseWrapper(w)
|
||||
}
|
||||
|
||||
h.ServeHTTP(wr, r)
|
||||
|
||||
if err = ValidateResponse(ctx, &ResponseValidationInput{
|
||||
RequestValidationInput: requestValidationInput,
|
||||
Status: wr.statusCode(),
|
||||
Header: wr.Header(),
|
||||
Body: io.NopCloser(bytes.NewBuffer(wr.bodyContents())),
|
||||
Options: &v.options,
|
||||
}); err != nil {
|
||||
v.logFunc(ctx, "invalid response", err)
|
||||
if v.strict {
|
||||
v.errFunc(ctx, w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = wr.flushBodyContents(); err != nil {
|
||||
v.logFunc(ctx, "failed to write response", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type responseWrapper interface {
|
||||
http.ResponseWriter
|
||||
|
||||
// flushBodyContents writes the buffered response to the client, if it has
|
||||
// not yet been written.
|
||||
flushBodyContents() error
|
||||
|
||||
// statusCode returns the response status code, 0 if not set yet.
|
||||
statusCode() int
|
||||
|
||||
// bodyContents returns the buffered
|
||||
bodyContents() []byte
|
||||
}
|
||||
|
||||
type warnResponseWrapper struct {
|
||||
w http.ResponseWriter
|
||||
headerWritten bool
|
||||
status int
|
||||
body bytes.Buffer
|
||||
tee io.Writer
|
||||
}
|
||||
|
||||
func newWarnResponseWrapper(w http.ResponseWriter) *warnResponseWrapper {
|
||||
wr := &warnResponseWrapper{
|
||||
w: w,
|
||||
}
|
||||
wr.tee = io.MultiWriter(w, &wr.body)
|
||||
return wr
|
||||
}
|
||||
|
||||
// Write implements http.ResponseWriter.
|
||||
func (wr *warnResponseWrapper) Write(b []byte) (int, error) {
|
||||
if !wr.headerWritten {
|
||||
wr.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return wr.tee.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (wr *warnResponseWrapper) WriteHeader(status int) {
|
||||
if !wr.headerWritten {
|
||||
// If the header hasn't been written, record the status for response
|
||||
// validation.
|
||||
wr.status = status
|
||||
wr.headerWritten = true
|
||||
}
|
||||
wr.w.WriteHeader(wr.status)
|
||||
}
|
||||
|
||||
// Header implements http.ResponseWriter.
|
||||
func (wr *warnResponseWrapper) Header() http.Header {
|
||||
return wr.w.Header()
|
||||
}
|
||||
|
||||
// Flush implements the optional http.Flusher interface.
|
||||
func (wr *warnResponseWrapper) Flush() {
|
||||
// If the wrapped http.ResponseWriter implements optional http.Flusher,
|
||||
// pass through.
|
||||
if fl, ok := wr.w.(http.Flusher); ok {
|
||||
fl.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (wr *warnResponseWrapper) flushBodyContents() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wr *warnResponseWrapper) statusCode() int {
|
||||
return wr.status
|
||||
}
|
||||
|
||||
func (wr *warnResponseWrapper) bodyContents() []byte {
|
||||
return wr.body.Bytes()
|
||||
}
|
||||
|
||||
type strictResponseWrapper struct {
|
||||
w http.ResponseWriter
|
||||
headerWritten bool
|
||||
status int
|
||||
body bytes.Buffer
|
||||
}
|
||||
|
||||
// Write implements http.ResponseWriter.
|
||||
func (wr *strictResponseWrapper) Write(b []byte) (int, error) {
|
||||
if !wr.headerWritten {
|
||||
wr.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return wr.body.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (wr *strictResponseWrapper) WriteHeader(status int) {
|
||||
if !wr.headerWritten {
|
||||
wr.status = status
|
||||
wr.headerWritten = true
|
||||
}
|
||||
}
|
||||
|
||||
// Header implements http.ResponseWriter.
|
||||
func (wr *strictResponseWrapper) Header() http.Header {
|
||||
return wr.w.Header()
|
||||
}
|
||||
|
||||
func (wr *strictResponseWrapper) flushBodyContents() error {
|
||||
wr.w.WriteHeader(wr.status)
|
||||
_, err := wr.w.Write(wr.body.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (wr *strictResponseWrapper) statusCode() int {
|
||||
return wr.status
|
||||
}
|
||||
|
||||
func (wr *strictResponseWrapper) bodyContents() []byte {
|
||||
return wr.body.Bytes()
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package openapi3filter
|
||||
|
||||
import "github.com/getkin/kin-openapi/openapi3"
|
||||
|
||||
// Options used by ValidateRequest and ValidateResponse
|
||||
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
|
||||
|
||||
// Set RegexCompiler to override the regex implementation
|
||||
RegexCompiler openapi3.RegexCompilerFunc
|
||||
|
||||
// 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
|
||||
|
||||
customSchemaErrorFunc CustomSchemaErrorFunc
|
||||
}
|
||||
|
||||
// CustomSchemaErrorFunc allows for custom the schema error message.
|
||||
type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string
|
||||
|
||||
// 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.
|
||||
func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc) {
|
||||
o.customSchemaErrorFunc = f
|
||||
}
|
||||
+1591
File diff suppressed because it is too large
Load Diff
+58
@@ -0,0 +1,58 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func encodeBody(body any, mediaType string) ([]byte, error) {
|
||||
if encoder := RegisteredBodyEncoder(mediaType); encoder != nil {
|
||||
return encoder(body)
|
||||
}
|
||||
return nil, &ParseError{
|
||||
Kind: KindUnsupportedFormat,
|
||||
Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType),
|
||||
}
|
||||
}
|
||||
|
||||
// BodyEncoder really is an (encoding/json).Marshaler
|
||||
type BodyEncoder func(body any) ([]byte, error)
|
||||
|
||||
var bodyEncodersM sync.RWMutex
|
||||
var bodyEncoders = map[string]BodyEncoder{
|
||||
"application/json": json.Marshal,
|
||||
}
|
||||
|
||||
// RegisterBodyEncoder enables package-wide decoding of contentType values
|
||||
func RegisterBodyEncoder(contentType string, encoder BodyEncoder) {
|
||||
if contentType == "" {
|
||||
panic("contentType is empty")
|
||||
}
|
||||
if encoder == nil {
|
||||
panic("encoder is not defined")
|
||||
}
|
||||
bodyEncodersM.Lock()
|
||||
bodyEncoders[contentType] = encoder
|
||||
bodyEncodersM.Unlock()
|
||||
}
|
||||
|
||||
// UnregisterBodyEncoder disables package-wide decoding of contentType values
|
||||
func UnregisterBodyEncoder(contentType string) {
|
||||
if contentType == "" {
|
||||
panic("contentType is empty")
|
||||
}
|
||||
bodyEncodersM.Lock()
|
||||
delete(bodyEncoders, contentType)
|
||||
bodyEncodersM.Unlock()
|
||||
}
|
||||
|
||||
// RegisteredBodyEncoder returns the registered body encoder for the given content type.
|
||||
//
|
||||
// If no encoder was registered for the given content type, nil is returned.
|
||||
func RegisteredBodyEncoder(contentType string) BodyEncoder {
|
||||
bodyEncodersM.RLock()
|
||||
mayBE := bodyEncoders[contentType]
|
||||
bodyEncodersM.RUnlock()
|
||||
return mayBE
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// ErrAuthenticationServiceMissing is returned when no authentication service
|
||||
// is defined for the request validator
|
||||
var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc")
|
||||
|
||||
// ErrInvalidRequired is returned when a required value of a parameter or request body is not defined.
|
||||
var ErrInvalidRequired = errors.New("value is required but missing")
|
||||
|
||||
// ErrInvalidEmptyValue is returned when a value of a parameter or request body is empty while it's not allowed.
|
||||
var ErrInvalidEmptyValue = errors.New("empty value is not allowed")
|
||||
|
||||
// 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 ValidateRequest(ctx context.Context, input *RequestValidationInput) error {
|
||||
var me openapi3.MultiError
|
||||
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
route := input.Route
|
||||
operation := route.Operation
|
||||
operationParameters := operation.Parameters
|
||||
pathItemParameters := route.PathItem.Parameters
|
||||
|
||||
// Security
|
||||
security := operation.Security
|
||||
// If there aren't any security requirements for the operation
|
||||
if security == nil {
|
||||
// Use the global security requirements.
|
||||
security = &route.Spec.Security
|
||||
}
|
||||
if security != nil {
|
||||
if err := ValidateSecurityRequirements(ctx, input, *security); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
// For each parameter of the PathItem
|
||||
for _, parameterRef := range pathItemParameters {
|
||||
parameter := parameterRef.Value
|
||||
if operationParameters != nil {
|
||||
if override := operationParameters.GetByInAndName(parameter.In, parameter.Name); override != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := ValidateParameter(ctx, input, parameter); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
// For each parameter of the Operation
|
||||
for _, parameter := range operationParameters {
|
||||
if options.ExcludeRequestQueryParams && parameter.Value.In == openapi3.ParameterInQuery {
|
||||
continue
|
||||
}
|
||||
if err := ValidateParameter(ctx, input, parameter.Value); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestBody
|
||||
requestBody := operation.RequestBody
|
||||
if requestBody != nil && !options.ExcludeRequestBody {
|
||||
if err := ValidateRequestBody(ctx, input, requestBody.Value); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(me) > 0 {
|
||||
return me
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendToQueryValues adds to query parameters each value in the provided slice
|
||||
func appendToQueryValues[T any](q url.Values, parameterName string, v []T) {
|
||||
for _, i := range v {
|
||||
q.Add(parameterName, fmt.Sprintf("%v", i))
|
||||
}
|
||||
}
|
||||
|
||||
// populateDefaultQueryParameters populates default values inside query parameters, while ensuring types are respected
|
||||
func populateDefaultQueryParameters(q url.Values, parameterName string, value any) {
|
||||
switch t := value.(type) {
|
||||
case []any:
|
||||
appendToQueryValues(q, parameterName, t)
|
||||
default:
|
||||
q.Add(parameterName, fmt.Sprintf("%v", value))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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 ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error {
|
||||
if parameter.Schema == nil && parameter.Content == nil {
|
||||
// We have no schema for the parameter. Assume that everything passes
|
||||
// a schema-less check, but this could also be an error. The OpenAPI
|
||||
// validation allows this to happen.
|
||||
return nil
|
||||
}
|
||||
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
var value any
|
||||
var err error
|
||||
var found bool
|
||||
var schema *openapi3.Schema
|
||||
|
||||
// Validation will ensure that we either have content or schema.
|
||||
if parameter.Content != nil {
|
||||
if value, schema, found, err = decodeContentParameter(parameter, input); err != nil {
|
||||
return &RequestError{Input: input, Parameter: parameter, Err: err}
|
||||
}
|
||||
} else {
|
||||
if value, found, err = decodeStyledParameter(parameter, input); err != nil {
|
||||
return &RequestError{Input: input, Parameter: parameter, Err: err}
|
||||
}
|
||||
schema = parameter.Schema.Value
|
||||
}
|
||||
|
||||
// Set default value if needed
|
||||
if !options.SkipSettingDefaults && value == nil && schema != nil {
|
||||
value = schema.Default
|
||||
for _, subSchema := range schema.AllOf {
|
||||
if subSchema.Value.Default != nil {
|
||||
value = subSchema.Value.Default
|
||||
break // This is not a validation of the schema itself, so use the first default value.
|
||||
}
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
req := input.Request
|
||||
switch parameter.In {
|
||||
case openapi3.ParameterInPath:
|
||||
// Path parameters are required.
|
||||
// Next check `parameter.Required && !found` will catch this.
|
||||
case openapi3.ParameterInQuery:
|
||||
q := req.URL.Query()
|
||||
populateDefaultQueryParameters(q, parameter.Name, value)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
case openapi3.ParameterInHeader:
|
||||
req.Header.Add(parameter.Name, fmt.Sprintf("%v", value))
|
||||
case openapi3.ParameterInCookie:
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: parameter.Name,
|
||||
Value: fmt.Sprintf("%v", value),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate a parameter's value and presence.
|
||||
if parameter.Required && !found {
|
||||
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired}
|
||||
}
|
||||
|
||||
if isNilValue(value) {
|
||||
if !parameter.AllowEmptyValue && found {
|
||||
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidEmptyValue.Error(), Err: ErrInvalidEmptyValue}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if schema == nil {
|
||||
// A parameter's schema is not defined so skip validation of a parameter's value.
|
||||
return nil
|
||||
}
|
||||
|
||||
var opts []openapi3.SchemaValidationOption
|
||||
if options.MultiError {
|
||||
opts = make([]openapi3.SchemaValidationOption, 0, 1)
|
||||
opts = append(opts, openapi3.MultiErrors())
|
||||
}
|
||||
if options.customSchemaErrorFunc != nil {
|
||||
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
|
||||
}
|
||||
if err = schema.VisitJSON(value, opts...); err != nil {
|
||||
return &RequestError{Input: input, Parameter: parameter, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const prefixInvalidCT = "header Content-Type has unexpected value"
|
||||
|
||||
// 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 ValidateRequestBody(ctx context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error {
|
||||
var (
|
||||
req = input.Request
|
||||
data []byte
|
||||
)
|
||||
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
if req.Body != http.NoBody && req.Body != nil {
|
||||
defer req.Body.Close()
|
||||
var err error
|
||||
if data, err = io.ReadAll(req.Body); err != nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: "reading failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// Put the data back into the input
|
||||
req.Body = nil
|
||||
if req.GetBody != nil {
|
||||
if req.Body, err = req.GetBody(); err != nil {
|
||||
req.Body = nil
|
||||
}
|
||||
}
|
||||
if req.Body == nil {
|
||||
req.ContentLength = int64(len(data))
|
||||
req.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
req.Body, _ = req.GetBody() // no error return
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
if requestBody.Required {
|
||||
return &RequestError{Input: input, RequestBody: requestBody, Err: ErrInvalidRequired}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
content := requestBody.Content
|
||||
if len(content) == 0 {
|
||||
// A request's body does not have declared content, so skip validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
inputMIME := req.Header.Get(headerCT)
|
||||
contentType := requestBody.Content.Get(inputMIME)
|
||||
if contentType == nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: fmt.Sprintf("%s %q", prefixInvalidCT, inputMIME),
|
||||
}
|
||||
}
|
||||
|
||||
if contentType.Schema == nil {
|
||||
// A JSON schema that describes the received data is not declared, so skip validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] }
|
||||
mediaType, value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn)
|
||||
if err != nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: "failed to decode request body",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
defaultsSet := false
|
||||
opts := make([]openapi3.SchemaValidationOption, 0, 4) // 4 potential opts here
|
||||
opts = append(opts, openapi3.VisitAsRequest())
|
||||
if !options.SkipSettingDefaults {
|
||||
opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
|
||||
}
|
||||
if options.MultiError {
|
||||
opts = append(opts, openapi3.MultiErrors())
|
||||
}
|
||||
if options.customSchemaErrorFunc != nil {
|
||||
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
|
||||
}
|
||||
if options.ExcludeReadOnlyValidations {
|
||||
opts = append(opts, openapi3.DisableReadOnlyValidation())
|
||||
}
|
||||
if options.RegexCompiler != nil {
|
||||
opts = append(opts, openapi3.SetSchemaRegexCompiler(options.RegexCompiler))
|
||||
}
|
||||
|
||||
// Validate JSON with the schema
|
||||
if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
|
||||
schemaId := getSchemaIdentifier(contentType.Schema)
|
||||
schemaId = prependSpaceIfNeeded(schemaId)
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: fmt.Sprintf("doesn't match schema%s", schemaId),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if defaultsSet {
|
||||
var err error
|
||||
if data, err = encodeBody(value, mediaType); err != nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: "rewriting failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// Put the data back into the input
|
||||
if req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
req.ContentLength = int64(len(data))
|
||||
req.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
req.Body, _ = req.GetBody() // no error return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error {
|
||||
if len(srs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
for _, sr := range srs {
|
||||
if err := validateSecurityRequirement(ctx, input, sr); err != nil {
|
||||
if len(errs) == 0 {
|
||||
errs = make([]error, 0, len(srs))
|
||||
}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return &SecurityRequirementsError{
|
||||
SecurityRequirements: srs,
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
|
||||
// validateSecurityRequirement validates a single OpenAPI 3 security requirement
|
||||
func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error {
|
||||
names := make([]string, 0, len(securityRequirement))
|
||||
for name := range securityRequirement {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
// Get authentication function
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
f := options.AuthenticationFunc
|
||||
if f == nil {
|
||||
return ErrAuthenticationServiceMissing
|
||||
}
|
||||
|
||||
var securitySchemes openapi3.SecuritySchemes
|
||||
if components := input.Route.Spec.Components; components != nil {
|
||||
securitySchemes = components.SecuritySchemes
|
||||
}
|
||||
|
||||
// For each scheme for the requirement
|
||||
for _, name := range names {
|
||||
var securityScheme *openapi3.SecurityScheme
|
||||
if securitySchemes != nil {
|
||||
if ref := securitySchemes[name]; ref != nil {
|
||||
securityScheme = ref.Value
|
||||
}
|
||||
}
|
||||
if securityScheme == nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
Err: fmt.Errorf("security scheme %q is not declared", name),
|
||||
}
|
||||
}
|
||||
scopes := securityRequirement[name]
|
||||
if err := f(ctx, &AuthenticationInput{
|
||||
RequestValidationInput: input,
|
||||
SecuritySchemeName: name,
|
||||
SecurityScheme: securityScheme,
|
||||
Scopes: scopes,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
// 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 ContentParameterDecoder func(param *openapi3.Parameter, values []string) (any, *openapi3.Schema, error)
|
||||
|
||||
type RequestValidationInput struct {
|
||||
Request *http.Request
|
||||
PathParams map[string]string
|
||||
QueryParams url.Values
|
||||
Route *routers.Route
|
||||
Options *Options
|
||||
ParamDecoder ContentParameterDecoder
|
||||
}
|
||||
|
||||
func (input *RequestValidationInput) GetQueryParams() url.Values {
|
||||
q := input.QueryParams
|
||||
if q == nil {
|
||||
q = input.Request.URL.Query()
|
||||
input.QueryParams = q
|
||||
}
|
||||
return q
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// Package openapi3filter validates that requests and inputs request an OpenAPI 3 specification file.
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// 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 ValidateResponse(ctx context.Context, input *ResponseValidationInput) error {
|
||||
req := input.RequestValidationInput.Request
|
||||
switch req.Method {
|
||||
case "HEAD":
|
||||
return nil
|
||||
}
|
||||
status := input.Status
|
||||
|
||||
// These status codes will never be validated.
|
||||
// TODO: The list is probably missing some.
|
||||
switch status {
|
||||
case http.StatusNotModified,
|
||||
http.StatusPermanentRedirect,
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusMovedPermanently:
|
||||
return nil
|
||||
}
|
||||
route := input.RequestValidationInput.Route
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
// Find input for the current status
|
||||
responses := route.Operation.Responses
|
||||
if responses.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
responseRef := responses.Status(status) // Response
|
||||
if responseRef == nil {
|
||||
responseRef = responses.Default() // Default input
|
||||
}
|
||||
if responseRef == nil {
|
||||
// By default, status that is not documented is allowed.
|
||||
if !options.IncludeResponseStatus {
|
||||
return nil
|
||||
}
|
||||
return &ResponseError{Input: input, Reason: "status is not supported"}
|
||||
}
|
||||
response := responseRef.Value
|
||||
if response == nil {
|
||||
return &ResponseError{Input: input, Reason: "response has not been resolved"}
|
||||
}
|
||||
|
||||
opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential options here
|
||||
if options.MultiError {
|
||||
opts = append(opts, openapi3.MultiErrors())
|
||||
}
|
||||
if options.customSchemaErrorFunc != nil {
|
||||
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
|
||||
}
|
||||
if options.ExcludeWriteOnlyValidations {
|
||||
opts = append(opts, openapi3.DisableWriteOnlyValidation())
|
||||
}
|
||||
|
||||
headers := make([]string, 0, len(response.Headers))
|
||||
for k := range response.Headers {
|
||||
if k != headerCT {
|
||||
headers = append(headers, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(headers)
|
||||
for _, headerName := range headers {
|
||||
headerRef := response.Headers[headerName]
|
||||
if err := validateResponseHeader(headerName, headerRef, input, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if options.ExcludeResponseBody {
|
||||
// A user turned off validation of a response's body.
|
||||
return nil
|
||||
}
|
||||
|
||||
content := response.Content
|
||||
if len(content) == 0 || options.ExcludeResponseBody {
|
||||
// An operation does not contains a validation schema for responses with this status code.
|
||||
return nil
|
||||
}
|
||||
|
||||
inputMIME := input.Header.Get(headerCT)
|
||||
contentType := content.Get(inputMIME)
|
||||
if contentType == nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response %s: %q", prefixInvalidCT, inputMIME),
|
||||
}
|
||||
}
|
||||
|
||||
if contentType.Schema == nil {
|
||||
// An operation does not contains a validation schema for responses with this status code.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read response's body.
|
||||
body := input.Body
|
||||
|
||||
// Response would contain partial or empty input body
|
||||
// after we begin reading.
|
||||
// Ensure that this doesn't happen.
|
||||
input.Body = nil
|
||||
|
||||
// Ensure we close the reader
|
||||
defer body.Close()
|
||||
|
||||
// Read all
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: "failed to read response body",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Put the data back into the response.
|
||||
input.SetBodyBytes(data)
|
||||
|
||||
encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] }
|
||||
_, value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn)
|
||||
if err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: "failed to decode response body",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate data with the schema.
|
||||
if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil {
|
||||
schemaId := getSchemaIdentifier(contentType.Schema)
|
||||
schemaId = prependSpaceIfNeeded(schemaId)
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response body doesn't match schema%s", schemaId),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error {
|
||||
var err error
|
||||
var decodedValue any
|
||||
var found bool
|
||||
var sm *openapi3.SerializationMethod
|
||||
dec := &headerParamDecoder{header: input.Header}
|
||||
|
||||
if sm, err = headerRef.Value.SerializationMethod(); err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("unable to get header %q serialization method", headerName),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if decodedValue, found, err = decodeValue(dec, headerName, sm, headerRef.Value.Schema, headerRef.Value.Required); err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("unable to decode header %q value", headerName),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if err = headerRef.Value.Schema.Value.VisitJSON(decodedValue, opts...); err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response header %q doesn't match schema", headerName),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
} else if headerRef.Value.Required {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response header %q missing", headerName),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSchemaIdentifier gets something by which a schema could be identified.
|
||||
// A schema by itself doesn't have a true identity field. This function makes
|
||||
// a best effort to get a value that can fill that void.
|
||||
func getSchemaIdentifier(schema *openapi3.SchemaRef) string {
|
||||
var id string
|
||||
|
||||
if schema != nil {
|
||||
id = strings.TrimSpace(schema.Ref)
|
||||
}
|
||||
if id == "" && schema.Value != nil {
|
||||
id = strings.TrimSpace(schema.Value.Title)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func prependSpaceIfNeeded(value string) string {
|
||||
if len(value) > 0 {
|
||||
value = " " + value
|
||||
}
|
||||
return value
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ResponseValidationInput struct {
|
||||
RequestValidationInput *RequestValidationInput
|
||||
Status int
|
||||
Header http.Header
|
||||
Body io.ReadCloser
|
||||
Options *Options
|
||||
}
|
||||
|
||||
func (input *ResponseValidationInput) SetBodyBytes(value []byte) *ResponseValidationInput {
|
||||
input.Body = io.NopCloser(bytes.NewReader(value))
|
||||
return input
|
||||
}
|
||||
|
||||
var JSONPrefixes = []string{
|
||||
")]}',\n",
|
||||
}
|
||||
|
||||
// TrimJSONPrefix trims one of the possible prefixes
|
||||
func TrimJSONPrefix(data []byte) []byte {
|
||||
search:
|
||||
for _, prefix := range JSONPrefixes {
|
||||
if len(data) < len(prefix) {
|
||||
continue
|
||||
}
|
||||
for i, b := range data[:len(prefix)] {
|
||||
if b != prefix[i] {
|
||||
continue search
|
||||
}
|
||||
}
|
||||
return data[len(prefix):]
|
||||
}
|
||||
return data
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ValidationError struct provides granular error information
|
||||
// useful for communicating issues back to end user and developer.
|
||||
// Based on https://jsonapi.org/format/#error-objects
|
||||
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"`
|
||||
}
|
||||
|
||||
// ValidationErrorSource struct
|
||||
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"`
|
||||
}
|
||||
|
||||
var _ error = &ValidationError{}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *ValidationError) Error() string {
|
||||
b := bytes.NewBufferString("[")
|
||||
if e.Status != 0 {
|
||||
b.WriteString(strconv.Itoa(e.Status))
|
||||
}
|
||||
b.WriteString("]")
|
||||
b.WriteString("[")
|
||||
if e.Code != "" {
|
||||
b.WriteString(e.Code)
|
||||
}
|
||||
b.WriteString("]")
|
||||
b.WriteString("[")
|
||||
if e.Id != "" {
|
||||
b.WriteString(e.Id)
|
||||
}
|
||||
b.WriteString("]")
|
||||
b.WriteString(" ")
|
||||
if e.Title != "" {
|
||||
b.WriteString(e.Title)
|
||||
b.WriteString(" ")
|
||||
}
|
||||
if e.Detail != "" {
|
||||
b.WriteString("| ")
|
||||
b.WriteString(e.Detail)
|
||||
b.WriteString(" ")
|
||||
}
|
||||
if e.Source != nil {
|
||||
b.WriteString("[source ")
|
||||
if e.Source.Parameter != "" {
|
||||
b.WriteString("parameter=")
|
||||
b.WriteString(e.Source.Parameter)
|
||||
} else if e.Source.Pointer != "" {
|
||||
b.WriteString("pointer=")
|
||||
b.WriteString(e.Source.Pointer)
|
||||
}
|
||||
b.WriteString("]")
|
||||
}
|
||||
|
||||
if b.Len() == 0 {
|
||||
return "no error"
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// StatusCode implements the StatusCoder interface for DefaultErrorEncoder
|
||||
func (e *ValidationError) StatusCode() int {
|
||||
return e.Status
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
// ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors
|
||||
type ValidationErrorEncoder struct {
|
||||
Encoder ErrorEncoder
|
||||
}
|
||||
|
||||
// Encode implements the ErrorEncoder interface for encoding ValidationErrors
|
||||
func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) {
|
||||
enc.Encoder(ctx, ConvertErrors(err), w)
|
||||
}
|
||||
|
||||
// ConvertErrors converts all errors to the appropriate error format.
|
||||
func ConvertErrors(err error) error {
|
||||
if e, ok := err.(*routers.RouteError); ok {
|
||||
return convertRouteError(e)
|
||||
}
|
||||
|
||||
e, ok := err.(*RequestError)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
var cErr *ValidationError
|
||||
if e.Err == nil {
|
||||
cErr = convertBasicRequestError(e)
|
||||
} else if e.Err == ErrInvalidRequired {
|
||||
cErr = convertErrInvalidRequired(e)
|
||||
} else if e.Err == ErrInvalidEmptyValue {
|
||||
cErr = convertErrInvalidEmptyValue(e)
|
||||
} else if innerErr, ok := e.Err.(*ParseError); ok {
|
||||
cErr = convertParseError(e, innerErr)
|
||||
} else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok {
|
||||
cErr = convertSchemaError(e, innerErr)
|
||||
}
|
||||
|
||||
if cErr != nil {
|
||||
return cErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func convertRouteError(e *routers.RouteError) *ValidationError {
|
||||
status := http.StatusNotFound
|
||||
if e.Error() == routers.ErrMethodNotAllowed.Error() {
|
||||
status = http.StatusMethodNotAllowed
|
||||
}
|
||||
return &ValidationError{Status: status, Title: e.Error()}
|
||||
}
|
||||
|
||||
func convertBasicRequestError(e *RequestError) *ValidationError {
|
||||
if strings.HasPrefix(e.Reason, prefixInvalidCT) {
|
||||
if strings.HasSuffix(e.Reason, `""`) {
|
||||
return &ValidationError{
|
||||
Status: http.StatusUnsupportedMediaType,
|
||||
Title: "header Content-Type is required",
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusUnsupportedMediaType,
|
||||
Title: prefixUnsupportedCT + strings.TrimPrefix(e.Reason, prefixInvalidCT),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertErrInvalidRequired(e *RequestError) *ValidationError {
|
||||
if e.Err == ErrInvalidRequired && e.Parameter != nil {
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertErrInvalidEmptyValue(e *RequestError) *ValidationError {
|
||||
if e.Err == ErrInvalidEmptyValue && e.Parameter != nil {
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("parameter %q in %s is not allowed to be empty", e.Parameter.Name, e.Parameter.In),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError {
|
||||
// We treat path params of the wrong type like a 404 instead of a 400
|
||||
if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" {
|
||||
return &ValidationError{
|
||||
Status: http.StatusNotFound,
|
||||
Title: fmt.Sprintf("resource not found with %q value: %v", e.Parameter.Name, innerErr.Value),
|
||||
}
|
||||
} else if strings.HasPrefix(innerErr.Reason, prefixUnsupportedCT) {
|
||||
return &ValidationError{
|
||||
Status: http.StatusUnsupportedMediaType,
|
||||
Title: innerErr.Reason,
|
||||
}
|
||||
} else if innerErr.RootCause() != nil {
|
||||
if rootErr, ok := innerErr.Cause.(*ParseError); ok &&
|
||||
rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" {
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("parameter %q in %s is invalid: %v is %s",
|
||||
e.Parameter.Name, e.Parameter.In, rootErr.Value, rootErr.Reason),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: innerErr.Reason,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError {
|
||||
cErr := &ValidationError{Title: innerErr.Reason}
|
||||
|
||||
// Handle "Origin" error
|
||||
if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok {
|
||||
cErr = convertSchemaError(e, originErr)
|
||||
}
|
||||
|
||||
// Add http status code
|
||||
if e.Parameter != nil {
|
||||
cErr.Status = http.StatusBadRequest
|
||||
} else if e.RequestBody != nil {
|
||||
cErr.Status = http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
// Add error source
|
||||
if e.Parameter != nil {
|
||||
// We have a JSONPointer in the query param too so need to
|
||||
// make sure 'Parameter' check takes priority over 'Pointer'
|
||||
cErr.Source = &ValidationErrorSource{Parameter: e.Parameter.Name}
|
||||
} else if ptr := innerErr.JSONPointer(); ptr != nil {
|
||||
cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)}
|
||||
}
|
||||
|
||||
// Add details on allowed values for enums
|
||||
if innerErr.SchemaField == "enum" {
|
||||
enums := make([]string, 0, len(innerErr.Schema.Enum))
|
||||
for _, enum := range innerErr.Schema.Enum {
|
||||
enums = append(enums, fmt.Sprintf("%v", enum))
|
||||
}
|
||||
cErr.Detail = fmt.Sprintf("value %v at %s must be one of: %s",
|
||||
innerErr.Value,
|
||||
toJSONPointer(innerErr.JSONPointer()),
|
||||
strings.Join(enums, ", "))
|
||||
value := fmt.Sprintf("%v", innerErr.Value)
|
||||
if e.Parameter != nil &&
|
||||
(e.Parameter.Explode == nil || *e.Parameter.Explode) &&
|
||||
(e.Parameter.Style == "" || e.Parameter.Style == "form") &&
|
||||
strings.Contains(value, ",") {
|
||||
parts := strings.Split(value, ",")
|
||||
cErr.Detail = fmt.Sprintf("%s; perhaps you intended '?%s=%s'",
|
||||
cErr.Detail,
|
||||
e.Parameter.Name,
|
||||
strings.Join(parts, "&"+e.Parameter.Name+"="))
|
||||
}
|
||||
}
|
||||
return cErr
|
||||
}
|
||||
|
||||
func toJSONPointer(reversePath []string) string {
|
||||
return "/" + strings.Join(reversePath, "/")
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
|
||||
)
|
||||
|
||||
// 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
|
||||
type AuthenticationFunc func(context.Context, *AuthenticationInput) error
|
||||
|
||||
// NoopAuthenticationFunc is an AuthenticationFunc
|
||||
func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil }
|
||||
|
||||
var _ AuthenticationFunc = NoopAuthenticationFunc
|
||||
|
||||
type ValidationHandler struct {
|
||||
Handler http.Handler
|
||||
AuthenticationFunc AuthenticationFunc
|
||||
File string
|
||||
ErrorEncoder ErrorEncoder
|
||||
router routers.Router
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) Load() error {
|
||||
loader := openapi3.NewLoader()
|
||||
doc, err := loader.LoadFromFile(h.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := doc.Validate(loader.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
if h.router, err = legacyrouter.NewRouter(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set defaults
|
||||
if h.Handler == nil {
|
||||
h.Handler = http.DefaultServeMux
|
||||
}
|
||||
if h.AuthenticationFunc == nil {
|
||||
h.AuthenticationFunc = NoopAuthenticationFunc
|
||||
}
|
||||
if h.ErrorEncoder == nil {
|
||||
h.ErrorEncoder = DefaultErrorEncoder
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if handled := h.before(w, r); handled {
|
||||
return
|
||||
}
|
||||
// TODO: validateResponse
|
||||
h.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Middleware implements gorilla/mux MiddlewareFunc
|
||||
func (h *ValidationHandler) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if handled := h.before(w, r); handled {
|
||||
return
|
||||
}
|
||||
// TODO: validateResponse
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (handled bool) {
|
||||
if err := h.validateRequest(r); err != nil {
|
||||
h.ErrorEncoder(r.Context(), err, w)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) validateRequest(r *http.Request) error {
|
||||
// Find route
|
||||
route, pathParams, err := h.router.FindRoute(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := &Options{
|
||||
AuthenticationFunc: h.AuthenticationFunc,
|
||||
}
|
||||
|
||||
// Validate request
|
||||
requestValidationInput := &RequestValidationInput{
|
||||
Request: r,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
Options: options,
|
||||
}
|
||||
if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// We didn't want to tie kin-openapi too tightly with go-kit.
|
||||
// This file contains the ErrorEncoder and DefaultErrorEncoder function
|
||||
// borrowed from this project.
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015 Peter Bourgon
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 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 ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)
|
||||
|
||||
// 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 StatusCoder interface {
|
||||
StatusCode() int
|
||||
}
|
||||
|
||||
// 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 Headerer interface {
|
||||
Headers() http.Header
|
||||
}
|
||||
|
||||
// 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 DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) {
|
||||
contentType, body := "text/plain; charset=utf-8", []byte(err.Error())
|
||||
if marshaler, ok := err.(json.Marshaler); ok {
|
||||
if jsonBody, marshalErr := marshaler.MarshalJSON(); marshalErr == nil {
|
||||
contentType, body = "application/json; charset=utf-8", jsonBody
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if headerer, ok := err.(Headerer); ok {
|
||||
for k, values := range headerer.Headers() {
|
||||
for _, v := range values {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
code := http.StatusInternalServerError
|
||||
if sc, ok := err.(StatusCoder); ok {
|
||||
code = sc.StatusCode()
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
w.Write(body)
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
// Package gorillamux implements a router.
|
||||
//
|
||||
// It differs from the legacy router:
|
||||
// * it provides somewhat granular errors: "path not found", "method not allowed".
|
||||
// * it handles matching routes with extensions (e.g. /books/{id}.json)
|
||||
// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z:.*})
|
||||
package gorillamux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
var _ routers.Router = &Router{}
|
||||
|
||||
// Router helps link http.Request.s and an OpenAPIv3 spec
|
||||
type Router struct {
|
||||
muxes []routeMux
|
||||
routes []*routers.Route
|
||||
}
|
||||
|
||||
type varsf func(vars map[string]string)
|
||||
|
||||
type routeMux struct {
|
||||
muxRoute *mux.Route
|
||||
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))
|
||||
|
||||
// NewRouter creates a gorilla/mux router.
|
||||
// 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) {
|
||||
servers, err := makeServers(doc.Servers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
muxRouter := mux.NewRouter().UseEncodedPath()
|
||||
r := &Router{}
|
||||
for _, path := range doc.Paths.InMatchingOrder() {
|
||||
pathItem := doc.Paths.Value(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))
|
||||
for method := range operations {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
sort.Strings(methods)
|
||||
|
||||
for _, s := range servers {
|
||||
muxRoute := muxRouter.Path(s.base + path).Methods(methods...)
|
||||
if schemes := s.schemes; len(schemes) != 0 {
|
||||
muxRoute.Schemes(schemes...)
|
||||
}
|
||||
if host := s.host; host != "" {
|
||||
muxRoute.Host(host)
|
||||
}
|
||||
if err := muxRoute.GetError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.muxes = append(r.muxes, routeMux{
|
||||
muxRoute: muxRoute,
|
||||
varsUpdater: s.varsUpdater,
|
||||
})
|
||||
r.routes = append(r.routes, &routers.Route{
|
||||
Spec: doc,
|
||||
Server: s.server,
|
||||
Path: path,
|
||||
PathItem: pathItem,
|
||||
Method: "",
|
||||
Operation: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// FindRoute extracts the route and parameters of an http.Request
|
||||
func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
|
||||
for i, m := range r.muxes {
|
||||
var match mux.RouteMatch
|
||||
if m.muxRoute.Match(req, &match) {
|
||||
if err := match.MatchErr; err != nil {
|
||||
// What then?
|
||||
}
|
||||
vars := match.Vars
|
||||
if f := m.varsUpdater; f != nil {
|
||||
f(vars)
|
||||
}
|
||||
route := *r.routes[i]
|
||||
route.Method = req.Method
|
||||
route.Operation = route.Spec.Paths.Value(route.Path).GetOperation(route.Method)
|
||||
return &route, vars, nil
|
||||
}
|
||||
switch match.MatchErr {
|
||||
case nil:
|
||||
case mux.ErrMethodMismatch:
|
||||
return nil, nil, routers.ErrMethodNotAllowed
|
||||
default: // What then?
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
svr, err := newSrv(serverURL, server, varsUpdater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
servers = append(servers, svr)
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
svr, err := newSrv(serverURL, server, varsUpdater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, svr)
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
servers = append(servers, srv{})
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func newSrv(serverURL string, server *openapi3.Server, varsUpdater varsf) (srv, error) {
|
||||
var schemes []string
|
||||
if strings.Contains(serverURL, "://") {
|
||||
scheme0 := strings.Split(serverURL, "://")[0]
|
||||
schemes = permutePart(scheme0, server)
|
||||
serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1)
|
||||
}
|
||||
|
||||
u, err := url.Parse(bEncode(serverURL))
|
||||
if err != nil {
|
||||
return srv{}, err
|
||||
}
|
||||
path := bDecode(u.EscapedPath())
|
||||
if len(path) > 0 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
svr := 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,
|
||||
}
|
||||
return svr, nil
|
||||
}
|
||||
|
||||
// Magic strings that temporarily replace "{}" so net/url.Parse() works
|
||||
var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50)
|
||||
|
||||
func bEncode(s string) string {
|
||||
s = strings.Replace(s, "{", blURL, -1)
|
||||
s = strings.Replace(s, "}", brURL, -1)
|
||||
return s
|
||||
}
|
||||
func bDecode(s string) string {
|
||||
s = strings.Replace(s, blURL, "{", -1)
|
||||
s = strings.Replace(s, brURL, "}", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
func permutePart(part0 string, srv *openapi3.Server) []string {
|
||||
type mapAndSlice struct {
|
||||
m map[string]struct{}
|
||||
s []string
|
||||
}
|
||||
var2val := make(map[string]mapAndSlice)
|
||||
max := 0
|
||||
for name0, v := range srv.Variables {
|
||||
name := "{" + name0 + "}"
|
||||
if !strings.Contains(part0, name) {
|
||||
continue
|
||||
}
|
||||
m := map[string]struct{}{v.Default: {}}
|
||||
for _, value := range v.Enum {
|
||||
m[value] = struct{}{}
|
||||
}
|
||||
if l := len(m); l > max {
|
||||
max = l
|
||||
}
|
||||
s := make([]string, 0, len(m))
|
||||
for value := range m {
|
||||
s = append(s, value)
|
||||
}
|
||||
var2val[name] = mapAndSlice{m: m, s: s}
|
||||
}
|
||||
if len(var2val) == 0 {
|
||||
return []string{part0}
|
||||
}
|
||||
|
||||
partsMap := make(map[string]struct{}, max*len(var2val))
|
||||
for i := 0; i < max; i++ {
|
||||
part := part0
|
||||
for name, mas := range var2val {
|
||||
part = strings.Replace(part, name, mas.s[i%len(mas.s)], -1)
|
||||
}
|
||||
partsMap[part] = struct{}{}
|
||||
}
|
||||
parts := make([]string, 0, len(partsMap))
|
||||
for part := range partsMap {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
sort.Strings(parts)
|
||||
return parts
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
// 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 root)
|
||||
// - "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions)
|
||||
package pathpattern
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DefaultOptions = &Options{
|
||||
SupportWildcard: true,
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
SupportWildcard bool
|
||||
SupportRegExp bool
|
||||
}
|
||||
|
||||
// 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"
|
||||
func PathFromHost(host string, specialDashes bool) string {
|
||||
buf := make([]byte, 0, len(host))
|
||||
end := len(host)
|
||||
|
||||
// Go from end to start
|
||||
for start := end - 1; start >= 0; start-- {
|
||||
switch host[start] {
|
||||
case '.':
|
||||
buf = append(buf, host[start+1:end]...)
|
||||
buf = append(buf, '/', '.', '/')
|
||||
end = start
|
||||
case '-':
|
||||
if specialDashes {
|
||||
buf = append(buf, host[start+1:end]...)
|
||||
buf = append(buf, '/', '-', '/')
|
||||
end = start
|
||||
}
|
||||
}
|
||||
}
|
||||
buf = append(buf, host[:end]...)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
VariableNames []string
|
||||
Value any
|
||||
Suffixes SuffixList
|
||||
}
|
||||
|
||||
func (currentNode *Node) String() string {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 255))
|
||||
currentNode.toBuffer(buf, "")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (currentNode *Node) toBuffer(buf *bytes.Buffer, linePrefix string) {
|
||||
if value := currentNode.Value; value != nil {
|
||||
buf.WriteString(linePrefix)
|
||||
buf.WriteString("VALUE: ")
|
||||
fmt.Fprint(buf, value)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
suffixes := currentNode.Suffixes
|
||||
if len(suffixes) > 0 {
|
||||
newLinePrefix := linePrefix + " "
|
||||
for _, suffix := range suffixes {
|
||||
buf.WriteString(linePrefix)
|
||||
buf.WriteString("PATTERN: ")
|
||||
buf.WriteString(suffix.String())
|
||||
buf.WriteString("\n")
|
||||
suffix.Node.toBuffer(buf, newLinePrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SuffixKind int
|
||||
|
||||
// Note that order is important!
|
||||
const (
|
||||
// SuffixKindConstant matches a constant string
|
||||
SuffixKindConstant = SuffixKind(iota)
|
||||
|
||||
// SuffixKindRegExp matches a regular expression
|
||||
SuffixKindRegExp
|
||||
|
||||
// SuffixKindVariable matches everything until '/'
|
||||
SuffixKindVariable
|
||||
|
||||
// SuffixKindEverything matches everything (until end-of-string)
|
||||
SuffixKindEverything
|
||||
)
|
||||
|
||||
// Suffix describes condition that
|
||||
type Suffix struct {
|
||||
Kind SuffixKind
|
||||
Pattern string
|
||||
|
||||
// compiled regular expression
|
||||
regExp *regexp.Regexp
|
||||
|
||||
// Next node
|
||||
Node *Node
|
||||
}
|
||||
|
||||
func EqualSuffix(a, b Suffix) bool {
|
||||
return a.Kind == b.Kind && a.Pattern == b.Pattern
|
||||
}
|
||||
|
||||
func (suffix Suffix) String() string {
|
||||
switch suffix.Kind {
|
||||
case SuffixKindConstant:
|
||||
return suffix.Pattern
|
||||
case SuffixKindVariable:
|
||||
return "{_}"
|
||||
case SuffixKindEverything:
|
||||
return "{_*}"
|
||||
default:
|
||||
return "{_|" + suffix.Pattern + "}"
|
||||
}
|
||||
}
|
||||
|
||||
type SuffixList []Suffix
|
||||
|
||||
func (list SuffixList) Less(i, j int) bool {
|
||||
a, b := list[i], list[j]
|
||||
ak, bk := a.Kind, b.Kind
|
||||
if ak < bk {
|
||||
return true
|
||||
} else if bk < ak {
|
||||
return false
|
||||
}
|
||||
return a.Pattern > b.Pattern
|
||||
}
|
||||
|
||||
func (list SuffixList) Len() int {
|
||||
return len(list)
|
||||
}
|
||||
|
||||
func (list SuffixList) Swap(i, j int) {
|
||||
a, b := list[i], list[j]
|
||||
list[i], list[j] = b, a
|
||||
}
|
||||
|
||||
func (currentNode *Node) MustAdd(path string, value any, options *Options) {
|
||||
node, err := currentNode.CreateNode(path, options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
node.Value = value
|
||||
}
|
||||
|
||||
func (currentNode *Node) Add(path string, value any, options *Options) error {
|
||||
node, err := currentNode.CreateNode(path, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node.Value = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (currentNode *Node) CreateNode(path string, options *Options) (*Node, error) {
|
||||
if options == nil {
|
||||
options = DefaultOptions
|
||||
}
|
||||
for strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
remaining := path
|
||||
var variableNames []string
|
||||
loop:
|
||||
for {
|
||||
//remaining = strings.TrimPrefix(remaining, "/")
|
||||
if len(remaining) == 0 {
|
||||
// This node is the right one
|
||||
// Check whether another route already leads to this node
|
||||
currentNode.VariableNames = variableNames
|
||||
return currentNode, nil
|
||||
}
|
||||
|
||||
suffix := Suffix{}
|
||||
var i int
|
||||
if strings.HasPrefix(remaining, "/") {
|
||||
remaining = remaining[1:]
|
||||
suffix.Kind = SuffixKindConstant
|
||||
suffix.Pattern = "/"
|
||||
} else {
|
||||
i = strings.IndexAny(remaining, "/{")
|
||||
if i < 0 {
|
||||
i = len(remaining)
|
||||
}
|
||||
if i > 0 {
|
||||
// Constant string pattern
|
||||
suffix.Kind = SuffixKindConstant
|
||||
suffix.Pattern = remaining[:i]
|
||||
remaining = remaining[i:]
|
||||
} else if remaining[0] == '{' {
|
||||
// This is probably a variable
|
||||
suffix.Kind = SuffixKindVariable
|
||||
|
||||
// Find variable name
|
||||
i := strings.IndexByte(remaining, '}')
|
||||
if i < 0 {
|
||||
return nil, fmt.Errorf("missing '}' in: %s", path)
|
||||
}
|
||||
variableName := strings.TrimSpace(remaining[1:i])
|
||||
remaining = remaining[i+1:]
|
||||
|
||||
if options.SupportRegExp {
|
||||
// See if it has regular expression
|
||||
i = strings.IndexByte(variableName, '|')
|
||||
if i >= 0 {
|
||||
suffix.Kind = SuffixKindRegExp
|
||||
suffix.Pattern = strings.TrimSpace(variableName[i+1:])
|
||||
variableName = strings.TrimSpace(variableName[:i])
|
||||
}
|
||||
}
|
||||
if suffix.Kind == SuffixKindVariable && options.SupportWildcard {
|
||||
if strings.HasSuffix(variableName, "*") {
|
||||
suffix.Kind = SuffixKindEverything
|
||||
}
|
||||
}
|
||||
variableNames = append(variableNames, variableName)
|
||||
}
|
||||
}
|
||||
|
||||
// Find existing matcher
|
||||
for _, existing := range currentNode.Suffixes {
|
||||
if EqualSuffix(existing, suffix) {
|
||||
currentNode = existing.Node
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
// Compile regular expression
|
||||
if suffix.Kind == SuffixKindRegExp {
|
||||
regExp, err := regexp.Compile(suffix.Pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid regular expression in: %s", path)
|
||||
}
|
||||
suffix.regExp = regExp
|
||||
}
|
||||
|
||||
// Create new node
|
||||
newNode := &Node{}
|
||||
suffix.Node = newNode
|
||||
currentNode.Suffixes = append(currentNode.Suffixes, suffix)
|
||||
sort.Sort(currentNode.Suffixes)
|
||||
currentNode = newNode
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
func (currentNode *Node) Match(path string) (*Node, []string) {
|
||||
for strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
variableValues := make([]string, 0, 8)
|
||||
return currentNode.matchRemaining(path, variableValues)
|
||||
}
|
||||
|
||||
func (currentNode *Node) matchRemaining(remaining string, paramValues []string) (*Node, []string) {
|
||||
// Check if this node matches
|
||||
if len(remaining) == 0 && currentNode.Value != nil {
|
||||
return currentNode, paramValues
|
||||
}
|
||||
|
||||
// See if any suffix matches
|
||||
for _, suffix := range currentNode.Suffixes {
|
||||
var resultNode *Node
|
||||
var resultValues []string
|
||||
switch suffix.Kind {
|
||||
case SuffixKindConstant:
|
||||
pattern := suffix.Pattern
|
||||
if strings.HasPrefix(remaining, pattern) {
|
||||
newRemaining := remaining[len(pattern):]
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, paramValues)
|
||||
} else if len(remaining) == 0 && pattern == "/" {
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(remaining, paramValues)
|
||||
}
|
||||
case SuffixKindVariable:
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i < 0 {
|
||||
i = len(remaining)
|
||||
}
|
||||
newParamValues := append(paramValues, remaining[:i])
|
||||
newRemaining := remaining[i:]
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, newParamValues)
|
||||
case SuffixKindEverything:
|
||||
newParamValues := append(paramValues, remaining)
|
||||
resultNode, resultValues = suffix.Node, newParamValues
|
||||
case SuffixKindRegExp:
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i < 0 {
|
||||
i = len(remaining)
|
||||
}
|
||||
paramValue := remaining[:i]
|
||||
regExp := suffix.regExp
|
||||
if regExp.MatchString(paramValue) {
|
||||
matches := regExp.FindStringSubmatch(paramValue)
|
||||
if len(matches) > 1 {
|
||||
paramValue = matches[1]
|
||||
}
|
||||
newParamValues := append(paramValues, paramValue)
|
||||
newRemaining := remaining[i:]
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, newParamValues)
|
||||
}
|
||||
}
|
||||
if resultNode != nil && resultNode.Value != nil {
|
||||
// This suffix matched
|
||||
return resultNode, resultValues
|
||||
}
|
||||
}
|
||||
|
||||
// No suffix matched
|
||||
return nil, nil
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Package legacy implements a router.
|
||||
//
|
||||
// It differs from the gorilla/mux router:
|
||||
// * it provides granular errors: "path not found", "method not allowed", "variable missing from path"
|
||||
// * it does not handle matching routes with extensions (e.g. /books/{id}.json)
|
||||
// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z.*})
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
"github.com/getkin/kin-openapi/routers/legacy/pathpattern"
|
||||
)
|
||||
|
||||
// Routers maps a HTTP request to a Router.
|
||||
type Routers []*Router
|
||||
|
||||
// FindRoute extracts the route and parameters of an http.Request
|
||||
func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, map[string]string, error) {
|
||||
for _, router := range rs {
|
||||
// Skip routers that have DO NOT have servers
|
||||
if len(router.doc.Servers) == 0 {
|
||||
continue
|
||||
}
|
||||
route, pathParams, err := router.FindRoute(req)
|
||||
if err == nil {
|
||||
return router, route, pathParams, nil
|
||||
}
|
||||
}
|
||||
for _, router := range rs {
|
||||
// Skip routers that DO have servers
|
||||
if len(router.doc.Servers) > 0 {
|
||||
continue
|
||||
}
|
||||
route, pathParams, err := router.FindRoute(req)
|
||||
if err == nil {
|
||||
return router, route, pathParams, nil
|
||||
}
|
||||
}
|
||||
return nil, nil, nil, &routers.RouteError{
|
||||
Reason: "none of the routers match",
|
||||
}
|
||||
}
|
||||
|
||||
// Router maps a HTTP request to an OpenAPI operation.
|
||||
type Router struct {
|
||||
doc *openapi3.T
|
||||
pathNode *pathpattern.Node
|
||||
}
|
||||
|
||||
// NewRouter creates a new router.
|
||||
//
|
||||
// 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, 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}
|
||||
root := router.node()
|
||||
for path, pathItem := range doc.Paths.Map() {
|
||||
for method, operation := range pathItem.Operations() {
|
||||
method = strings.ToUpper(method)
|
||||
if err := root.Add(method+" "+path, &routers.Route{
|
||||
Spec: doc,
|
||||
Path: path,
|
||||
PathItem: pathItem,
|
||||
Method: method,
|
||||
Operation: operation,
|
||||
}, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return router, nil
|
||||
}
|
||||
|
||||
// AddRoute adds a route in the router.
|
||||
func (router *Router) AddRoute(route *routers.Route) error {
|
||||
method := route.Method
|
||||
if method == "" {
|
||||
return errors.New("route is missing method")
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
path := route.Path
|
||||
if path == "" {
|
||||
return errors.New("route is missing path")
|
||||
}
|
||||
return router.node().Add(method+" "+path, router, nil)
|
||||
}
|
||||
|
||||
func (router *Router) node() *pathpattern.Node {
|
||||
root := router.pathNode
|
||||
if root == nil {
|
||||
root = &pathpattern.Node{}
|
||||
router.pathNode = root
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// FindRoute extracts the route and parameters of an http.Request
|
||||
func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
|
||||
method, url := req.Method, req.URL
|
||||
doc := router.doc
|
||||
|
||||
// Get server
|
||||
servers := doc.Servers
|
||||
var server *openapi3.Server
|
||||
var remainingPath string
|
||||
var pathParams map[string]string
|
||||
if len(servers) == 0 {
|
||||
remainingPath = url.Path
|
||||
} else {
|
||||
var paramValues []string
|
||||
server, paramValues, remainingPath = servers.MatchURL(url)
|
||||
if server == nil {
|
||||
return nil, nil, &routers.RouteError{
|
||||
Reason: routers.ErrPathNotFound.Error(),
|
||||
}
|
||||
}
|
||||
pathParams = make(map[string]string)
|
||||
paramNames, err := server.ParameterNames()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i, value := range paramValues {
|
||||
name := paramNames[i]
|
||||
pathParams[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Get PathItem
|
||||
root := router.node()
|
||||
var route *routers.Route
|
||||
node, paramValues := root.Match(method + " " + remainingPath)
|
||||
if node != nil {
|
||||
route, _ = node.Value.(*routers.Route)
|
||||
}
|
||||
if route == nil {
|
||||
pathItem := doc.Paths.Value(remainingPath)
|
||||
if pathItem == nil {
|
||||
return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()}
|
||||
}
|
||||
if pathItem.GetOperation(method) == nil {
|
||||
return nil, nil, &routers.RouteError{Reason: routers.ErrMethodNotAllowed.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
if pathParams == nil {
|
||||
pathParams = make(map[string]string, len(paramValues))
|
||||
}
|
||||
paramKeys := node.VariableNames
|
||||
for i, value := range paramValues {
|
||||
key := strings.TrimSuffix(paramKeys[i], "*")
|
||||
pathParams[key] = value
|
||||
}
|
||||
return route, pathParams, nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// Router helps link http.Request.s and an OpenAPIv3 spec
|
||||
type Router interface {
|
||||
// FindRoute matches an HTTP request with the operation it resolves to.
|
||||
// Hosts are matched from the OpenAPIv3 servers key.
|
||||
//
|
||||
// If you experience ErrPathNotFound and have localhost hosts specified as your servers,
|
||||
// turning these server URLs as relative (leaving only the path) should resolve this.
|
||||
//
|
||||
// See openapi3filter for example uses with request and response validation.
|
||||
FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error)
|
||||
}
|
||||
|
||||
// Route describes the operation an http.Request can match
|
||||
type Route struct {
|
||||
Spec *openapi3.T
|
||||
Server *openapi3.Server
|
||||
Path string
|
||||
PathItem *openapi3.PathItem
|
||||
Method string
|
||||
Operation *openapi3.Operation
|
||||
}
|
||||
|
||||
// ErrPathNotFound is returned when no route match is found
|
||||
var ErrPathNotFound error = &RouteError{"no matching operation was found"}
|
||||
|
||||
// ErrMethodNotAllowed is returned when no method of the matched route matches
|
||||
var ErrMethodNotAllowed error = &RouteError{"method not allowed"}
|
||||
|
||||
// RouteError describes Router errors
|
||||
type RouteError struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *RouteError) Error() string { return e.Reason }
|
||||
Reference in New Issue
Block a user