add functions/vendor files

This commit is contained in:
Reed Allman
2017-06-11 02:05:36 -07:00
parent 6ee9c1fa0a
commit f2c7aa5ee6
7294 changed files with 1629834 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
package middleware
import (
"io"
"net/http"
"path"
"testing"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/stretchr/testify/assert"
)
type eofReader struct {
}
func (r *eofReader) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (r *eofReader) Close() error {
return nil
}
type rbn func(*http.Request, *MatchedRoute) error
func (b rbn) BindRequest(r *http.Request, rr *MatchedRoute) error {
return b(r, rr)
}
func TestBindRequest_BodyValidation(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
api.DefaultConsumes = runtime.JSONMime
ctx.router = DefaultRouter(spec, ctx.api)
req, err := http.NewRequest("GET", path.Join(spec.BasePath(), "/pets"), new(eofReader))
if assert.NoError(t, err) {
req.Header.Set("Content-Type", runtime.JSONMime)
ri, ok := ctx.RouteInfo(req)
if assert.True(t, ok) {
err := ctx.BindValidRequest(req, ri, rbn(func(r *http.Request, rr *MatchedRoute) error {
defer r.Body.Close()
var data interface{}
err := runtime.JSONConsumer().Consume(r.Body, &data)
_ = data
return err
}))
assert.Error(t, err)
assert.Equal(t, io.EOF, err)
}
}
}
func TestBindRequest_DeleteNoBody(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
api.DefaultConsumes = runtime.JSONMime
ctx.router = DefaultRouter(spec, ctx.api)
req, err := http.NewRequest("DELETE", path.Join(spec.BasePath(), "/pets/123"), new(eofReader))
if assert.NoError(t, err) {
req.Header.Set("Accept", "*/*")
ri, ok := ctx.RouteInfo(req)
if assert.True(t, ok) {
err := ctx.BindValidRequest(req, ri, rbn(func(r *http.Request, rr *MatchedRoute) error {
return nil
}))
assert.NoError(t, err)
//assert.Equal(t, io.EOF, err)
}
}
req, err = http.NewRequest("DELETE", path.Join(spec.BasePath(), "/pets/123"), new(eofReader))
if assert.NoError(t, err) {
req.Header.Set("Accept", "*/*")
req.Header.Set("Content-Type", runtime.JSONMime)
req.ContentLength = 1
ri, ok := ctx.RouteInfo(req)
if assert.True(t, ok) {
err := ctx.BindValidRequest(req, ri, rbn(func(r *http.Request, rr *MatchedRoute) error {
defer r.Body.Close()
var data interface{}
err := runtime.JSONConsumer().Consume(r.Body, &data)
_ = data
return err
}))
assert.Error(t, err)
assert.Equal(t, io.EOF, err)
}
}
}

View File

@@ -0,0 +1,526 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"log"
"net/http"
"os"
"strings"
"sync"
"github.com/go-openapi/analysis"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware/untyped"
"github.com/go-openapi/runtime/security"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/gorilla/context"
)
// Debug when true turns on verbose logging
var Debug = os.Getenv("SWAGGER_DEBUG") != "" || os.Getenv("DEBUG") != ""
func debugLog(format string, args ...interface{}) {
if Debug {
log.Printf(format, args...)
}
}
// A Builder can create middlewares
type Builder func(http.Handler) http.Handler
// PassthroughBuilder returns the handler, aka the builder identity function
func PassthroughBuilder(handler http.Handler) http.Handler { return handler }
// RequestBinder is an interface for types to implement
// when they want to be able to bind from a request
type RequestBinder interface {
BindRequest(*http.Request, *MatchedRoute) error
}
// Responder is an interface for types to implement
// when they want to be considered for writing HTTP responses
type Responder interface {
WriteResponse(http.ResponseWriter, runtime.Producer)
}
// ResponderFunc wraps a func as a Responder interface
type ResponderFunc func(http.ResponseWriter, runtime.Producer)
// WriteResponse writes to the response
func (fn ResponderFunc) WriteResponse(rw http.ResponseWriter, pr runtime.Producer) {
fn(rw, pr)
}
// Context is a type safe wrapper around an untyped request context
// used throughout to store request context with the gorilla context module
type Context struct {
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
router Router
formats strfmt.Registry
}
type routableUntypedAPI struct {
api *untyped.API
hlock *sync.Mutex
handlers map[string]map[string]http.Handler
defaultConsumes string
defaultProduces string
}
func newRoutableUntypedAPI(spec *loads.Document, api *untyped.API, context *Context) *routableUntypedAPI {
var handlers map[string]map[string]http.Handler
if spec == nil || api == nil {
return nil
}
analyzer := analysis.New(spec.Spec())
for method, hls := range analyzer.Operations() {
um := strings.ToUpper(method)
for path, op := range hls {
schemes := analyzer.SecurityDefinitionsFor(op)
if oh, ok := api.OperationHandlerFor(method, path); ok {
if handlers == nil {
handlers = make(map[string]map[string]http.Handler)
}
if b, ok := handlers[um]; !ok || b == nil {
handlers[um] = make(map[string]http.Handler)
}
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// lookup route info in the context
route, _ := context.RouteInfo(r)
// bind and validate the request using reflection
bound, validation := context.BindAndValidate(r, route)
if validation != nil {
context.Respond(w, r, route.Produces, route, validation)
return
}
// actually handle the request
result, err := oh.Handle(bound)
if err != nil {
// respond with failure
context.Respond(w, r, route.Produces, route, err)
return
}
// respond with success
context.Respond(w, r, route.Produces, route, result)
})
if len(schemes) > 0 {
handler = newSecureAPI(context, handler)
}
handlers[um][path] = handler
}
}
}
return &routableUntypedAPI{
api: api,
hlock: new(sync.Mutex),
handlers: handlers,
defaultProduces: api.DefaultProduces,
defaultConsumes: api.DefaultConsumes,
}
}
func (r *routableUntypedAPI) HandlerFor(method, path string) (http.Handler, bool) {
r.hlock.Lock()
paths, ok := r.handlers[strings.ToUpper(method)]
if !ok {
r.hlock.Unlock()
return nil, false
}
handler, ok := paths[path]
r.hlock.Unlock()
return handler, ok
}
func (r *routableUntypedAPI) ServeErrorFor(operationID string) func(http.ResponseWriter, *http.Request, error) {
return r.api.ServeError
}
func (r *routableUntypedAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer {
return r.api.ConsumersFor(mediaTypes)
}
func (r *routableUntypedAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer {
return r.api.ProducersFor(mediaTypes)
}
func (r *routableUntypedAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator {
return r.api.AuthenticatorsFor(schemes)
}
func (r *routableUntypedAPI) Formats() strfmt.Registry {
return r.api.Formats()
}
func (r *routableUntypedAPI) DefaultProduces() string {
return r.defaultProduces
}
func (r *routableUntypedAPI) DefaultConsumes() string {
return r.defaultConsumes
}
// NewRoutableContext creates a new context for a routable API
func NewRoutableContext(spec *loads.Document, routableAPI RoutableAPI, routes Router) *Context {
var an *analysis.Spec
if spec != nil {
an = analysis.New(spec.Spec())
}
ctx := &Context{spec: spec, api: routableAPI, analyzer: an}
return ctx
}
// NewContext creates a new context wrapper
func NewContext(spec *loads.Document, api *untyped.API, routes Router) *Context {
var an *analysis.Spec
if spec != nil {
an = analysis.New(spec.Spec())
}
ctx := &Context{spec: spec, analyzer: an}
ctx.api = newRoutableUntypedAPI(spec, api, ctx)
return ctx
}
// Serve serves the specified spec with the specified api registrations as a http.Handler
func Serve(spec *loads.Document, api *untyped.API) http.Handler {
return ServeWithBuilder(spec, api, PassthroughBuilder)
}
// ServeWithBuilder serves the specified spec with the specified api registrations as a http.Handler that is decorated
// by the Builder
func ServeWithBuilder(spec *loads.Document, api *untyped.API, builder Builder) http.Handler {
context := NewContext(spec, api, nil)
return context.APIHandler(builder)
}
type contextKey int8
const (
_ contextKey = iota
ctxContentType
ctxResponseFormat
ctxMatchedRoute
ctxAllowedMethods
ctxBoundParams
ctxSecurityPrincipal
ctxSecurityScopes
ctxConsumer
)
type contentTypeValue struct {
MediaType string
Charset string
}
// BasePath returns the base path for this API
func (c *Context) BasePath() string {
return c.spec.BasePath()
}
// RequiredProduces returns the accepted content types for responses
func (c *Context) RequiredProduces() []string {
return c.analyzer.RequiredProduces()
}
// BindValidRequest binds a params object to a request but only when the request is valid
// if the request is not valid an error will be returned
func (c *Context) BindValidRequest(request *http.Request, route *MatchedRoute, binder RequestBinder) error {
var res []error
requestContentType := "*/*"
// check and validate content type, select consumer
if runtime.HasBody(request) {
ct, _, err := runtime.ContentType(request.Header)
if err != nil {
res = append(res, err)
} else {
if err := validateContentType(route.Consumes, ct); err != nil {
res = append(res, err)
}
if len(res) == 0 {
cons, ok := route.Consumers[ct]
if !ok {
res = append(res, errors.New(500, "no consumer registered for %s", ct))
} else {
route.Consumer = cons
requestContentType = ct
}
}
}
}
// check and validate the response format
if len(res) == 0 && runtime.HasBody(request) {
if str := NegotiateContentType(request, route.Produces, requestContentType); str == "" {
res = append(res, errors.InvalidResponseFormat(request.Header.Get(runtime.HeaderAccept), route.Produces))
}
}
// now bind the request with the provided binder
// it's assumed the binder will also validate the request and return an error if the
// request is invalid
if binder != nil && len(res) == 0 {
if err := binder.BindRequest(request, route); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// ContentType gets the parsed value of a content type
func (c *Context) ContentType(request *http.Request) (string, string, error) {
if v, ok := context.GetOk(request, ctxContentType); ok {
if val, ok := v.(*contentTypeValue); ok {
return val.MediaType, val.Charset, nil
}
}
mt, cs, err := runtime.ContentType(request.Header)
if err != nil {
return "", "", err
}
context.Set(request, ctxContentType, &contentTypeValue{mt, cs})
return mt, cs, nil
}
// LookupRoute looks a route up and returns true when it is found
func (c *Context) LookupRoute(request *http.Request) (*MatchedRoute, bool) {
if route, ok := c.router.Lookup(request.Method, request.URL.EscapedPath()); ok {
return route, ok
}
return nil, false
}
// RouteInfo tries to match a route for this request
func (c *Context) RouteInfo(request *http.Request) (*MatchedRoute, bool) {
if v, ok := context.GetOk(request, ctxMatchedRoute); ok {
if val, ok := v.(*MatchedRoute); ok {
return val, ok
}
}
if route, ok := c.LookupRoute(request); ok {
context.Set(request, ctxMatchedRoute, route)
return route, ok
}
return nil, false
}
// ResponseFormat negotiates the response content type
func (c *Context) ResponseFormat(r *http.Request, offers []string) string {
if v, ok := context.GetOk(r, ctxResponseFormat); ok {
if val, ok := v.(string); ok {
return val
}
}
format := NegotiateContentType(r, offers, "")
if format != "" {
context.Set(r, ctxResponseFormat, format)
}
return format
}
// AllowedMethods gets the allowed methods for the path of this request
func (c *Context) AllowedMethods(request *http.Request) []string {
return c.router.OtherMethods(request.Method, request.URL.EscapedPath())
}
// Authorize authorizes the request
func (c *Context) Authorize(request *http.Request, route *MatchedRoute) (interface{}, error) {
if route == nil || len(route.Authenticators) == 0 {
return nil, nil
}
if v, ok := context.GetOk(request, ctxSecurityPrincipal); ok {
return v, nil
}
var lastError error
for scheme, authenticator := range route.Authenticators {
applies, usr, err := authenticator.Authenticate(&security.ScopedAuthRequest{
Request: request,
RequiredScopes: route.Scopes[scheme],
})
if !applies || err != nil || usr == nil {
if err != nil {
lastError = err
}
continue
}
context.Set(request, ctxSecurityPrincipal, usr)
context.Set(request, ctxSecurityScopes, route.Scopes[scheme])
return usr, nil
}
if lastError != nil {
return nil, lastError
}
return nil, errors.Unauthenticated("invalid credentials")
}
// BindAndValidate binds and validates the request
func (c *Context) BindAndValidate(request *http.Request, matched *MatchedRoute) (interface{}, error) {
if v, ok := context.GetOk(request, ctxBoundParams); ok {
if val, ok := v.(*validation); ok {
debugLog("got cached validation (valid: %t)", len(val.result) == 0)
if len(val.result) > 0 {
return val.bound, errors.CompositeValidationError(val.result...)
}
return val.bound, nil
}
}
result := validateRequest(c, request, matched)
if result != nil {
context.Set(request, ctxBoundParams, result)
}
if len(result.result) > 0 {
return result.bound, errors.CompositeValidationError(result.result...)
}
debugLog("no validation errors found")
return result.bound, nil
}
// NotFound the default not found responder for when no route has been matched yet
func (c *Context) NotFound(rw http.ResponseWriter, r *http.Request) {
c.Respond(rw, r, []string{c.api.DefaultProduces()}, nil, errors.NotFound("not found"))
}
// Respond renders the response after doing some content negotiation
func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, data interface{}) {
offers := []string{}
for _, mt := range produces {
if mt != c.api.DefaultProduces() {
offers = append(offers, mt)
}
}
// the default producer is last so more specific producers take precedence
offers = append(offers, c.api.DefaultProduces())
format := c.ResponseFormat(r, offers)
rw.Header().Set(runtime.HeaderContentType, format)
if resp, ok := data.(Responder); ok {
producers := route.Producers
prod, ok := producers[format]
if !ok {
prods := c.api.ProducersFor([]string{c.api.DefaultProduces()})
pr, ok := prods[c.api.DefaultProduces()]
if !ok {
panic(errors.New(http.StatusInternalServerError, "can't find a producer for "+format))
}
prod = pr
}
resp.WriteResponse(rw, prod)
return
}
if err, ok := data.(error); ok {
if format == "" {
rw.Header().Set(runtime.HeaderContentType, runtime.JSONMime)
}
if route == nil || route.Operation == nil {
c.api.ServeErrorFor("")(rw, r, err)
return
}
c.api.ServeErrorFor(route.Operation.ID)(rw, r, err)
return
}
if route == nil || route.Operation == nil {
rw.WriteHeader(200)
if r.Method == "HEAD" {
return
}
producers := c.api.ProducersFor(offers)
prod, ok := producers[format]
if !ok {
panic(errors.New(http.StatusInternalServerError, "can't find a producer for "+format))
}
if err := prod.Produce(rw, data); err != nil {
panic(err) // let the recovery middleware deal with this
}
return
}
if _, code, ok := route.Operation.SuccessResponse(); ok {
rw.WriteHeader(code)
if code == 204 || r.Method == "HEAD" {
return
}
producers := route.Producers
prod, ok := producers[format]
if !ok {
if !ok {
prods := c.api.ProducersFor([]string{c.api.DefaultProduces()})
pr, ok := prods[c.api.DefaultProduces()]
if !ok {
panic(errors.New(http.StatusInternalServerError, "can't find a producer for "+format))
}
prod = pr
}
}
if err := prod.Produce(rw, data); err != nil {
panic(err) // let the recovery middleware deal with this
}
return
}
c.api.ServeErrorFor(route.Operation.ID)(rw, r, errors.New(http.StatusInternalServerError, "can't produce response"))
}
// APIHandler returns a handler to serve the API, this includes a swagger spec, router and the contract defined in the swagger spec
func (c *Context) APIHandler(builder Builder) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
var title string
sp := c.spec.Spec()
if sp != nil && sp.Info != nil && sp.Info.Title != "" {
title = sp.Info.Title
}
redocOpts := RedocOpts{
BasePath: c.BasePath(),
Title: title,
}
return Spec("", c.spec.Raw(), Redoc(redocOpts, c.RoutesHandler(builder)))
}
// RoutesHandler returns a handler to serve the API, just the routes and the contract defined in the swagger spec
func (c *Context) RoutesHandler(builder Builder) http.Handler {
b := builder
if b == nil {
b = PassthroughBuilder
}
return NewRouter(c, b(NewOperationExecutor(c)))
}

View File

@@ -0,0 +1,395 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-openapi/loads"
"github.com/go-openapi/loads/fmts"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/go-openapi/runtime/middleware/untyped"
"github.com/gorilla/context"
"github.com/stretchr/testify/assert"
)
type stubOperationHandler struct {
}
func (s *stubOperationHandler) ParameterModel() interface{} {
return nil
}
func (s *stubOperationHandler) Handle(params interface{}) (interface{}, error) {
return nil, nil
}
type testBinder struct {
}
func (t *testBinder) BindRequest(r *http.Request, m *MatchedRoute) error {
return nil
}
func init() {
loads.AddLoader(fmts.YAMLMatcher, fmts.YAMLDoc)
}
func TestContentType_Issue264(t *testing.T) {
swspec, err := loads.Spec("../fixtures/bugs/264/swagger.yml")
if assert.NoError(t, err) {
api := untyped.NewAPI(swspec)
api.RegisterConsumer("application/json", runtime.JSONConsumer())
api.RegisterProducer("application/json", runtime.JSONProducer())
api.RegisterOperation("delete", "/key/{id}", new(stubOperationHandler))
handler := Serve(swspec, api)
request, _ := http.NewRequest("DELETE", "/key/1", nil)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
}
}
func TestServe(t *testing.T) {
spec, api := petstore.NewAPI(t)
handler := Serve(spec, api)
// serve spec document
request, _ := http.NewRequest("GET", "http://localhost:8080/swagger.json", nil)
request.Header.Add("Content-Type", runtime.JSONMime)
request.Header.Add("Accept", runtime.JSONMime)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
request, _ = http.NewRequest("GET", "http://localhost:8080/swagger-ui", nil)
recorder = httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
assert.Equal(t, 404, recorder.Code)
}
func TestContextAuthorize(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := runtime.JSONRequest("GET", "/api/pets", nil)
v, ok := context.GetOk(request, ctxSecurityPrincipal)
assert.False(t, ok)
assert.Nil(t, v)
ri, ok := ctx.RouteInfo(request)
assert.True(t, ok)
p, err := ctx.Authorize(request, ri)
assert.Error(t, err)
assert.Nil(t, p)
v, ok = context.GetOk(request, ctxSecurityPrincipal)
assert.False(t, ok)
assert.Nil(t, v)
request.SetBasicAuth("wrong", "wrong")
p, err = ctx.Authorize(request, ri)
assert.Error(t, err)
assert.Nil(t, p)
v, ok = context.GetOk(request, ctxSecurityPrincipal)
assert.False(t, ok)
assert.Nil(t, v)
request.SetBasicAuth("admin", "admin")
p, err = ctx.Authorize(request, ri)
assert.NoError(t, err)
assert.Equal(t, "admin", p)
v, ok = context.GetOk(request, ctxSecurityPrincipal)
assert.True(t, ok)
assert.Equal(t, "admin", v)
request.SetBasicAuth("doesn't matter", "doesn't")
pp, rr := ctx.Authorize(request, ri)
assert.Equal(t, p, pp)
assert.Equal(t, err, rr)
}
func TestContextNegotiateContentType(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("POST", "/api/pets", nil)
// request.Header.Add("Accept", "*/*")
request.Header.Add("content-type", "text/html")
v, ok := context.GetOk(request, ctxBoundParams)
assert.False(t, ok)
assert.Nil(t, v)
ri, _ := ctx.RouteInfo(request)
res := NegotiateContentType(request, ri.Produces, "")
assert.Equal(t, "", res)
res2 := NegotiateContentType(request, ri.Produces, "text/plain")
assert.Equal(t, "text/plain", res2)
}
func TestContextBindAndValidate(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("POST", "/api/pets", nil)
request.Header.Add("Accept", "*/*")
request.Header.Add("content-type", "text/html")
request.ContentLength = 1
v, ok := context.GetOk(request, ctxBoundParams)
assert.False(t, ok)
assert.Nil(t, v)
ri, _ := ctx.RouteInfo(request)
data, result := ctx.BindAndValidate(request, ri) // this requires a much more thorough test
assert.NotNil(t, data)
assert.NotNil(t, result)
v, ok = context.GetOk(request, ctxBoundParams)
assert.True(t, ok)
assert.NotNil(t, v)
dd, rr := ctx.BindAndValidate(request, ri)
assert.Equal(t, data, dd)
assert.Equal(t, result, rr)
}
func TestContextRender(t *testing.T) {
ct := runtime.JSONMime
spec, api := petstore.NewAPI(t)
assert.NotNil(t, spec)
assert.NotNil(t, api)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("GET", "pets", nil)
request.Header.Set(runtime.HeaderAccept, ct)
ri, _ := ctx.RouteInfo(request)
recorder := httptest.NewRecorder()
ctx.Respond(recorder, request, []string{ct}, ri, map[string]interface{}{"name": "hello"})
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "{\"name\":\"hello\"}\n", recorder.Body.String())
recorder = httptest.NewRecorder()
ctx.Respond(recorder, request, []string{ct}, ri, errors.New("this went wrong"))
assert.Equal(t, 500, recorder.Code)
// recorder = httptest.NewRecorder()
// assert.Panics(t, func() { ctx.Respond(recorder, request, []string{ct}, ri, map[int]interface{}{1: "hello"}) })
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "pets", nil)
assert.Panics(t, func() { ctx.Respond(recorder, request, []string{}, ri, map[string]interface{}{"name": "hello"}) })
request, _ = http.NewRequest("GET", "/pets", nil)
request.Header.Set(runtime.HeaderAccept, ct)
ri, _ = ctx.RouteInfo(request)
recorder = httptest.NewRecorder()
ctx.Respond(recorder, request, []string{ct}, ri, map[string]interface{}{"name": "hello"})
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "{\"name\":\"hello\"}\n", recorder.Body.String())
recorder = httptest.NewRecorder()
ctx.Respond(recorder, request, []string{ct}, ri, errors.New("this went wrong"))
assert.Equal(t, 500, recorder.Code)
// recorder = httptest.NewRecorder()
// assert.Panics(t, func() { ctx.Respond(recorder, request, []string{ct}, ri, map[int]interface{}{1: "hello"}) })
// recorder = httptest.NewRecorder()
// request, _ = http.NewRequest("GET", "/pets", nil)
// assert.Panics(t, func() { ctx.Respond(recorder, request, []string{}, ri, map[string]interface{}{"name": "hello"}) })
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("DELETE", "/api/pets/1", nil)
ri, _ = ctx.RouteInfo(request)
ctx.Respond(recorder, request, ri.Produces, ri, nil)
assert.Equal(t, 204, recorder.Code)
}
func TestContextValidResponseFormat(t *testing.T) {
ct := "application/json"
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("GET", "http://localhost:8080", nil)
request.Header.Set(runtime.HeaderAccept, ct)
// check there's nothing there
cached, ok := context.GetOk(request, ctxResponseFormat)
assert.False(t, ok)
assert.Empty(t, cached)
// trigger the parse
mt := ctx.ResponseFormat(request, []string{ct})
assert.Equal(t, ct, mt)
// check it was cached
cached, ok = context.GetOk(request, ctxResponseFormat)
assert.True(t, ok)
assert.Equal(t, ct, cached)
// check if the cast works and fetch from cache too
mt = ctx.ResponseFormat(request, []string{ct})
assert.Equal(t, ct, mt)
}
func TestContextInvalidResponseFormat(t *testing.T) {
ct := "application/x-yaml"
other := "application/sgml"
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("GET", "http://localhost:8080", nil)
request.Header.Set(runtime.HeaderAccept, ct)
// check there's nothing there
cached, ok := context.GetOk(request, ctxResponseFormat)
assert.False(t, ok)
assert.Empty(t, cached)
// trigger the parse
mt := ctx.ResponseFormat(request, []string{other})
assert.Empty(t, mt)
// check it was cached
cached, ok = context.GetOk(request, ctxResponseFormat)
assert.False(t, ok)
assert.Empty(t, cached)
// check if the cast works and fetch from cache too
mt = ctx.ResponseFormat(request, []string{other})
assert.Empty(t, mt)
}
func TestContextValidRoute(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("GET", "/api/pets", nil)
// check there's nothing there
_, ok := context.GetOk(request, ctxMatchedRoute)
assert.False(t, ok)
matched, ok := ctx.RouteInfo(request)
assert.True(t, ok)
assert.NotNil(t, matched)
// check it was cached
_, ok = context.GetOk(request, ctxMatchedRoute)
assert.True(t, ok)
matched, ok = ctx.RouteInfo(request)
assert.True(t, ok)
assert.NotNil(t, matched)
}
func TestContextInvalidRoute(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
ctx.router = DefaultRouter(spec, ctx.api)
request, _ := http.NewRequest("DELETE", "pets", nil)
// check there's nothing there
_, ok := context.GetOk(request, ctxMatchedRoute)
assert.False(t, ok)
matched, ok := ctx.RouteInfo(request)
assert.False(t, ok)
assert.Nil(t, matched)
// check it was cached
_, ok = context.GetOk(request, ctxMatchedRoute)
assert.False(t, ok)
matched, ok = ctx.RouteInfo(request)
assert.False(t, ok)
assert.Nil(t, matched)
}
func TestContextValidContentType(t *testing.T) {
ct := "application/json"
ctx := NewContext(nil, nil, nil)
request, _ := http.NewRequest("GET", "http://localhost:8080", nil)
request.Header.Set(runtime.HeaderContentType, ct)
// check there's nothing there
_, ok := context.GetOk(request, ctxContentType)
assert.False(t, ok)
// trigger the parse
mt, _, err := ctx.ContentType(request)
assert.NoError(t, err)
assert.Equal(t, ct, mt)
// check it was cached
_, ok = context.GetOk(request, ctxContentType)
assert.True(t, ok)
// check if the cast works and fetch from cache too
mt, _, err = ctx.ContentType(request)
assert.NoError(t, err)
assert.Equal(t, ct, mt)
}
func TestContextInvalidContentType(t *testing.T) {
ct := "application("
ctx := NewContext(nil, nil, nil)
request, _ := http.NewRequest("GET", "http://localhost:8080", nil)
request.Header.Set(runtime.HeaderContentType, ct)
// check there's nothing there
_, ok := context.GetOk(request, ctxContentType)
assert.False(t, ok)
// trigger the parse
mt, _, err := ctx.ContentType(request)
assert.Error(t, err)
assert.Empty(t, mt)
// check it was not cached
_, ok = context.GetOk(request, ctxContentType)
assert.False(t, ok)
// check if the failure continues
_, _, err = ctx.ContentType(request)
assert.Error(t, err)
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2014 Naoya Inada <naoina@kuune.org>
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.

View File

@@ -0,0 +1,180 @@
# Denco [![Build Status](https://travis-ci.org/naoina/denco.png?branch=master)](https://travis-ci.org/naoina/denco)
The fast and flexible HTTP request router for [Go](http://golang.org).
Denco is based on Double-Array implementation of [Kocha-urlrouter](https://github.com/naoina/kocha-urlrouter).
However, Denco is optimized and some features added.
## Features
* Fast (See [go-http-routing-benchmark](https://github.com/naoina/go-http-routing-benchmark))
* [URL patterns](#url-patterns) (`/foo/:bar` and `/foo/*wildcard`)
* Small (but enough) URL router API
* HTTP request multiplexer like `http.ServeMux`
## Installation
go get -u github.com/go-openapi/runtime/middleware/denco
## Using as HTTP request multiplexer
```go
package main
import (
"fmt"
"log"
"net/http"
"github.com/go-openapi/runtime/middleware/denco"
)
func Index(w http.ResponseWriter, r *http.Request, params denco.Params) {
fmt.Fprintf(w, "Welcome to Denco!\n")
}
func User(w http.ResponseWriter, r *http.Request, params denco.Params) {
fmt.Fprintf(w, "Hello %s!\n", params.Get("name"))
}
func main() {
mux := denco.NewMux()
handler, err := mux.Build([]denco.Handler{
mux.GET("/", Index),
mux.GET("/user/:name", User),
mux.POST("/user/:name", User),
})
if err != nil {
panic(err)
}
log.Fatal(http.ListenAndServe(":8080", handler))
}
```
## Using as URL router
```go
package main
import (
"fmt"
"github.com/go-openapi/runtime/middleware/denco"
)
type route struct {
name string
}
func main() {
router := denco.New()
router.Build([]denco.Record{
{"/", &route{"root"}},
{"/user/:id", &route{"user"}},
{"/user/:name/:id", &route{"username"}},
{"/static/*filepath", &route{"static"}},
})
data, params, found := router.Lookup("/")
// print `&main.route{name:"root"}, denco.Params(nil), true`.
fmt.Printf("%#v, %#v, %#v\n", data, params, found)
data, params, found = router.Lookup("/user/hoge")
// print `&main.route{name:"user"}, denco.Params{denco.Param{Name:"id", Value:"hoge"}}, true`.
fmt.Printf("%#v, %#v, %#v\n", data, params, found)
data, params, found = router.Lookup("/user/hoge/7")
// print `&main.route{name:"username"}, denco.Params{denco.Param{Name:"name", Value:"hoge"}, denco.Param{Name:"id", Value:"7"}}, true`.
fmt.Printf("%#v, %#v, %#v\n", data, params, found)
data, params, found = router.Lookup("/static/path/to/file")
// print `&main.route{name:"static"}, denco.Params{denco.Param{Name:"filepath", Value:"path/to/file"}}, true`.
fmt.Printf("%#v, %#v, %#v\n", data, params, found)
}
```
See [Godoc](http://godoc.org/github.com/go-openapi/runtime/middleware/denco) for more details.
## Getting the value of path parameter
You can get the value of path parameter by 2 ways.
1. Using [`denco.Params.Get`](http://godoc.org/github.com/go-openapi/runtime/middleware/denco#Params.Get) method
2. Find by loop
```go
package main
import (
"fmt"
"github.com/go-openapi/runtime/middleware/denco"
)
func main() {
router := denco.New()
if err := router.Build([]denco.Record{
{"/user/:name/:id", "route1"},
}); err != nil {
panic(err)
}
// 1. Using denco.Params.Get method.
_, params, _ := router.Lookup("/user/alice/1")
name := params.Get("name")
if name != "" {
fmt.Printf("Hello %s.\n", name) // prints "Hello alice.".
}
// 2. Find by loop.
for _, param := range params {
if param.Name == "name" {
fmt.Printf("Hello %s.\n", name) // prints "Hello alice.".
}
}
}
```
## URL patterns
Denco's route matching strategy is "most nearly matching".
When routes `/:name` and `/alice` have been built, URI `/alice` matches the route `/alice`, not `/:name`.
Because URI `/alice` is more match with the route `/alice` than `/:name`.
For more example, when routes below have been built:
```
/user/alice
/user/:name
/user/:name/:id
/user/alice/:id
/user/:id/bob
```
Routes matching are:
```
/user/alice => "/user/alice" (no match with "/user/:name")
/user/bob => "/user/:name"
/user/naoina/1 => "/user/:name/1"
/user/alice/1 => "/user/alice/:id" (no match with "/user/:name/:id")
/user/1/bob => "/user/:id/bob" (no match with "/user/:name/:id")
/user/alice/bob => "/user/alice/:id" (no match with "/user/:name/:id" and "/user/:id/bob")
```
## Limitation
Denco has some limitations below.
* Number of param records (such as `/:name`) must be less than 2^22
* Number of elements of internal slice must be less than 2^22
## Benchmarks
cd $GOPATH/github.com/go-openapi/runtime/middleware/denco
go test -bench . -benchmem
## License
Denco is licensed under the MIT License.

View File

@@ -0,0 +1,452 @@
// Package denco provides fast URL router.
package denco
import (
"fmt"
"sort"
"strings"
)
const (
// ParamCharacter is a special character for path parameter.
ParamCharacter = ':'
// WildcardCharacter is a special character for wildcard path parameter.
WildcardCharacter = '*'
// TerminationCharacter is a special character for end of path.
TerminationCharacter = '#'
// MaxSize is max size of records and internal slice.
MaxSize = (1 << 22) - 1
)
// Router represents a URL router.
type Router struct {
// SizeHint expects the maximum number of path parameters in records to Build.
// SizeHint will be used to determine the capacity of the memory to allocate.
// By default, SizeHint will be determined from given records to Build.
SizeHint int
static map[string]interface{}
param *doubleArray
}
// New returns a new Router.
func New() *Router {
return &Router{
SizeHint: -1,
static: make(map[string]interface{}),
param: newDoubleArray(),
}
}
// Lookup returns data and path parameters that associated with path.
// params is a slice of the Param that arranged in the order in which parameters appeared.
// e.g. when built routing path is "/path/to/:id/:name" and given path is "/path/to/1/alice". params order is [{"id": "1"}, {"name": "alice"}], not [{"name": "alice"}, {"id": "1"}].
func (rt *Router) Lookup(path string) (data interface{}, params Params, found bool) {
if data, found := rt.static[path]; found {
return data, nil, true
}
if len(rt.param.node) == 1 {
return nil, nil, false
}
nd, params, found := rt.param.lookup(path, make([]Param, 0, rt.SizeHint), 1)
if !found {
return nil, nil, false
}
for i := 0; i < len(params); i++ {
params[i].Name = nd.paramNames[i]
}
return nd.data, params, true
}
// Build builds URL router from records.
func (rt *Router) Build(records []Record) error {
statics, params := makeRecords(records)
if len(params) > MaxSize {
return fmt.Errorf("denco: too many records")
}
if rt.SizeHint < 0 {
rt.SizeHint = 0
for _, p := range params {
size := 0
for _, k := range p.Key {
if k == ParamCharacter || k == WildcardCharacter {
size++
}
}
if size > rt.SizeHint {
rt.SizeHint = size
}
}
}
for _, r := range statics {
rt.static[r.Key] = r.Value
}
if err := rt.param.build(params, 1, 0, make(map[int]struct{})); err != nil {
return err
}
return nil
}
// Param represents name and value of path parameter.
type Param struct {
Name string
Value string
}
// Params represents the name and value of path parameters.
type Params []Param
// Get gets the first value associated with the given name.
// If there are no values associated with the key, Get returns "".
func (ps Params) Get(name string) string {
for _, p := range ps {
if p.Name == name {
return p.Value
}
}
return ""
}
type doubleArray struct {
bc []baseCheck
node []*node
}
func newDoubleArray() *doubleArray {
return &doubleArray{
bc: []baseCheck{0},
node: []*node{nil}, // A start index is adjusting to 1 because 0 will be used as a mark of non-existent node.
}
}
// baseCheck contains BASE, CHECK and Extra flags.
// From the top, 22bits of BASE, 2bits of Extra flags and 8bits of CHECK.
//
// BASE (22bit) | Extra flags (2bit) | CHECK (8bit)
// |----------------------|--|--------|
// 32 10 8 0
type baseCheck uint32
func (bc baseCheck) Base() int {
return int(bc >> 10)
}
func (bc *baseCheck) SetBase(base int) {
*bc |= baseCheck(base) << 10
}
func (bc baseCheck) Check() byte {
return byte(bc)
}
func (bc *baseCheck) SetCheck(check byte) {
*bc |= baseCheck(check)
}
func (bc baseCheck) IsEmpty() bool {
return bc&0xfffffcff == 0
}
func (bc baseCheck) IsSingleParam() bool {
return bc&paramTypeSingle == paramTypeSingle
}
func (bc baseCheck) IsWildcardParam() bool {
return bc&paramTypeWildcard == paramTypeWildcard
}
func (bc baseCheck) IsAnyParam() bool {
return bc&paramTypeAny != 0
}
func (bc *baseCheck) SetSingleParam() {
*bc |= (1 << 8)
}
func (bc *baseCheck) SetWildcardParam() {
*bc |= (1 << 9)
}
const (
paramTypeSingle = 0x0100
paramTypeWildcard = 0x0200
paramTypeAny = 0x0300
)
func (da *doubleArray) lookup(path string, params []Param, idx int) (*node, []Param, bool) {
indices := make([]uint64, 0, 1)
for i := 0; i < len(path); i++ {
if da.bc[idx].IsAnyParam() {
indices = append(indices, (uint64(i)<<32)|(uint64(idx)&0xffffffff))
}
c := path[i]
if idx = nextIndex(da.bc[idx].Base(), c); idx >= len(da.bc) || da.bc[idx].Check() != c {
goto BACKTRACKING
}
}
if next := nextIndex(da.bc[idx].Base(), TerminationCharacter); next < len(da.bc) && da.bc[next].Check() == TerminationCharacter {
return da.node[da.bc[next].Base()], params, true
}
BACKTRACKING:
for j := len(indices) - 1; j >= 0; j-- {
i, idx := int(indices[j]>>32), int(indices[j]&0xffffffff)
if da.bc[idx].IsSingleParam() {
idx := nextIndex(da.bc[idx].Base(), ParamCharacter)
if idx >= len(da.bc) {
break
}
next := NextSeparator(path, i)
params := append(params, Param{Value: path[i:next]})
if nd, params, found := da.lookup(path[next:], params, idx); found {
return nd, params, true
}
}
if da.bc[idx].IsWildcardParam() {
idx := nextIndex(da.bc[idx].Base(), WildcardCharacter)
params := append(params, Param{Value: path[i:]})
return da.node[da.bc[idx].Base()], params, true
}
}
return nil, nil, false
}
// build builds double-array from records.
func (da *doubleArray) build(srcs []*record, idx, depth int, usedBase map[int]struct{}) error {
sort.Stable(recordSlice(srcs))
base, siblings, leaf, err := da.arrange(srcs, idx, depth, usedBase)
if err != nil {
return err
}
if leaf != nil {
nd, err := makeNode(leaf)
if err != nil {
return err
}
da.bc[idx].SetBase(len(da.node))
da.node = append(da.node, nd)
}
for _, sib := range siblings {
da.setCheck(nextIndex(base, sib.c), sib.c)
}
for _, sib := range siblings {
records := srcs[sib.start:sib.end]
switch sib.c {
case ParamCharacter:
for _, r := range records {
next := NextSeparator(r.Key, depth+1)
name := r.Key[depth+1 : next]
r.paramNames = append(r.paramNames, name)
r.Key = r.Key[next:]
}
da.bc[idx].SetSingleParam()
if err := da.build(records, nextIndex(base, sib.c), 0, usedBase); err != nil {
return err
}
case WildcardCharacter:
r := records[0]
name := r.Key[depth+1 : len(r.Key)-1]
r.paramNames = append(r.paramNames, name)
r.Key = ""
da.bc[idx].SetWildcardParam()
if err := da.build(records, nextIndex(base, sib.c), 0, usedBase); err != nil {
return err
}
default:
if err := da.build(records, nextIndex(base, sib.c), depth+1, usedBase); err != nil {
return err
}
}
}
return nil
}
// setBase sets BASE.
func (da *doubleArray) setBase(i, base int) {
da.bc[i].SetBase(base)
}
// setCheck sets CHECK.
func (da *doubleArray) setCheck(i int, check byte) {
da.bc[i].SetCheck(check)
}
// findEmptyIndex returns an index of unused BASE/CHECK node.
func (da *doubleArray) findEmptyIndex(start int) int {
i := start
for ; i < len(da.bc); i++ {
if da.bc[i].IsEmpty() {
break
}
}
return i
}
// findBase returns good BASE.
func (da *doubleArray) findBase(siblings []sibling, start int, usedBase map[int]struct{}) (base int) {
for idx, firstChar := start+1, siblings[0].c; ; idx = da.findEmptyIndex(idx + 1) {
base = nextIndex(idx, firstChar)
if _, used := usedBase[base]; used {
continue
}
i := 0
for ; i < len(siblings); i++ {
next := nextIndex(base, siblings[i].c)
if len(da.bc) <= next {
da.bc = append(da.bc, make([]baseCheck, next-len(da.bc)+1)...)
}
if !da.bc[next].IsEmpty() {
break
}
}
if i == len(siblings) {
break
}
}
usedBase[base] = struct{}{}
return base
}
func (da *doubleArray) arrange(records []*record, idx, depth int, usedBase map[int]struct{}) (base int, siblings []sibling, leaf *record, err error) {
siblings, leaf, err = makeSiblings(records, depth)
if err != nil {
return -1, nil, nil, err
}
if len(siblings) < 1 {
return -1, nil, leaf, nil
}
base = da.findBase(siblings, idx, usedBase)
if base > MaxSize {
return -1, nil, nil, fmt.Errorf("denco: too many elements of internal slice")
}
da.setBase(idx, base)
return base, siblings, leaf, err
}
// node represents a node of Double-Array.
type node struct {
data interface{}
// Names of path parameters.
paramNames []string
}
// makeNode returns a new node from record.
func makeNode(r *record) (*node, error) {
dups := make(map[string]bool)
for _, name := range r.paramNames {
if dups[name] {
return nil, fmt.Errorf("denco: path parameter `%v' is duplicated in the key `%v'", name, r.Key)
}
dups[name] = true
}
return &node{data: r.Value, paramNames: r.paramNames}, nil
}
// sibling represents an intermediate data of build for Double-Array.
type sibling struct {
// An index of start of duplicated characters.
start int
// An index of end of duplicated characters.
end int
// A character of sibling.
c byte
}
// nextIndex returns a next index of array of BASE/CHECK.
func nextIndex(base int, c byte) int {
return base ^ int(c)
}
// makeSiblings returns slice of sibling.
func makeSiblings(records []*record, depth int) (sib []sibling, leaf *record, err error) {
var (
pc byte
n int
)
for i, r := range records {
if len(r.Key) <= depth {
leaf = r
continue
}
c := r.Key[depth]
switch {
case pc < c:
sib = append(sib, sibling{start: i, c: c})
case pc == c:
continue
default:
return nil, nil, fmt.Errorf("denco: BUG: routing table hasn't been sorted")
}
if n > 0 {
sib[n-1].end = i
}
pc = c
n++
}
if n == 0 {
return nil, leaf, nil
}
sib[n-1].end = len(records)
return sib, leaf, nil
}
// Record represents a record data for router construction.
type Record struct {
// Key for router construction.
Key string
// Result value for Key.
Value interface{}
}
// NewRecord returns a new Record.
func NewRecord(key string, value interface{}) Record {
return Record{
Key: key,
Value: value,
}
}
// record represents a record that use to build the Double-Array.
type record struct {
Record
paramNames []string
}
// makeRecords returns the records that use to build Double-Arrays.
func makeRecords(srcs []Record) (statics, params []*record) {
spChars := string([]byte{ParamCharacter, WildcardCharacter})
termChar := string(TerminationCharacter)
for _, r := range srcs {
if strings.ContainsAny(r.Key, spChars) {
r.Key += termChar
params = append(params, &record{Record: r})
} else {
statics = append(statics, &record{Record: r})
}
}
return statics, params
}
// recordSlice represents a slice of Record for sort and implements the sort.Interface.
type recordSlice []*record
// Len implements the sort.Interface.Len.
func (rs recordSlice) Len() int {
return len(rs)
}
// Less implements the sort.Interface.Less.
func (rs recordSlice) Less(i, j int) bool {
return rs[i].Key < rs[j].Key
}
// Swap implements the sort.Interface.Swap.
func (rs recordSlice) Swap(i, j int) {
rs[i], rs[j] = rs[j], rs[i]
}

View File

@@ -0,0 +1,178 @@
package denco_test
import (
"bytes"
"crypto/rand"
"fmt"
"math/big"
"testing"
"github.com/go-openapi/runtime/middleware/denco"
)
func BenchmarkRouterLookupStatic100(b *testing.B) {
benchmarkRouterLookupStatic(b, 100)
}
func BenchmarkRouterLookupStatic300(b *testing.B) {
benchmarkRouterLookupStatic(b, 300)
}
func BenchmarkRouterLookupStatic700(b *testing.B) {
benchmarkRouterLookupStatic(b, 700)
}
func BenchmarkRouterLookupSingleParam100(b *testing.B) {
records := makeTestSingleParamRecords(100)
benchmarkRouterLookupSingleParam(b, records)
}
func BenchmarkRouterLookupSingleParam300(b *testing.B) {
records := makeTestSingleParamRecords(300)
benchmarkRouterLookupSingleParam(b, records)
}
func BenchmarkRouterLookupSingleParam700(b *testing.B) {
records := makeTestSingleParamRecords(700)
benchmarkRouterLookupSingleParam(b, records)
}
func BenchmarkRouterLookupSingle2Param100(b *testing.B) {
records := makeTestSingle2ParamRecords(100)
benchmarkRouterLookupSingleParam(b, records)
}
func BenchmarkRouterLookupSingle2Param300(b *testing.B) {
records := makeTestSingle2ParamRecords(300)
benchmarkRouterLookupSingleParam(b, records)
}
func BenchmarkRouterLookupSingle2Param700(b *testing.B) {
records := makeTestSingle2ParamRecords(700)
benchmarkRouterLookupSingleParam(b, records)
}
func BenchmarkRouterBuildStatic100(b *testing.B) {
records := makeTestStaticRecords(100)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildStatic300(b *testing.B) {
records := makeTestStaticRecords(300)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildStatic700(b *testing.B) {
records := makeTestStaticRecords(700)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildSingleParam100(b *testing.B) {
records := makeTestSingleParamRecords(100)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildSingleParam300(b *testing.B) {
records := makeTestSingleParamRecords(300)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildSingleParam700(b *testing.B) {
records := makeTestSingleParamRecords(700)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildSingle2Param100(b *testing.B) {
records := makeTestSingle2ParamRecords(100)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildSingle2Param300(b *testing.B) {
records := makeTestSingle2ParamRecords(300)
benchmarkRouterBuild(b, records)
}
func BenchmarkRouterBuildSingle2Param700(b *testing.B) {
records := makeTestSingle2ParamRecords(700)
benchmarkRouterBuild(b, records)
}
func benchmarkRouterLookupStatic(b *testing.B, n int) {
b.StopTimer()
router := denco.New()
records := makeTestStaticRecords(n)
if err := router.Build(records); err != nil {
b.Fatal(err)
}
record := pickTestRecord(records)
b.StartTimer()
for i := 0; i < b.N; i++ {
if r, _, _ := router.Lookup(record.Key); r != record.Value {
b.Fail()
}
}
}
func benchmarkRouterLookupSingleParam(b *testing.B, records []denco.Record) {
router := denco.New()
if err := router.Build(records); err != nil {
b.Fatal(err)
}
record := pickTestRecord(records)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, _, found := router.Lookup(record.Key); !found {
b.Fail()
}
}
}
func benchmarkRouterBuild(b *testing.B, records []denco.Record) {
for i := 0; i < b.N; i++ {
router := denco.New()
if err := router.Build(records); err != nil {
b.Fatal(err)
}
}
}
func makeTestStaticRecords(n int) []denco.Record {
records := make([]denco.Record, n)
for i := 0; i < n; i++ {
records[i] = denco.NewRecord("/"+randomString(50), fmt.Sprintf("testroute%d", i))
}
return records
}
func makeTestSingleParamRecords(n int) []denco.Record {
records := make([]denco.Record, n)
for i := 0; i < len(records); i++ {
records[i] = denco.NewRecord(fmt.Sprintf("/user%d/:name", i), fmt.Sprintf("testroute%d", i))
}
return records
}
func makeTestSingle2ParamRecords(n int) []denco.Record {
records := make([]denco.Record, n)
for i := 0; i < len(records); i++ {
records[i] = denco.NewRecord(fmt.Sprintf("/user%d/:name/comment/:id", i), fmt.Sprintf("testroute%d", i))
}
return records
}
func pickTestRecord(records []denco.Record) denco.Record {
return records[len(records)/2]
}
func randomString(n int) string {
const srcStrings = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/"
var buf bytes.Buffer
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(srcStrings)-1)))
if err != nil {
panic(err)
}
buf.WriteByte(srcStrings[num.Int64()])
}
return buf.String()
}

View File

@@ -0,0 +1,526 @@
package denco_test
import (
"fmt"
"math/rand"
"reflect"
"testing"
"time"
"github.com/go-openapi/runtime/middleware/denco"
)
func routes() []denco.Record {
return []denco.Record{
{"/", "testroute0"},
{"/path/to/route", "testroute1"},
{"/path/to/other", "testroute2"},
{"/path/to/route/a", "testroute3"},
{"/path/to/:param", "testroute4"},
{"/gists/:param1/foo/:param2", "testroute12"},
{"/gists/:param1/foo/bar", "testroute11"},
{"/:param1/:param2/foo/:param3", "testroute13"},
{"/path/to/wildcard/*routepath", "testroute5"},
{"/path/to/:param1/:param2", "testroute6"},
{"/path/to/:param1/sep/:param2", "testroute7"},
{"/:year/:month/:day", "testroute8"},
{"/user/:id", "testroute9"},
{"/a/to/b/:param/*routepath", "testroute10"},
}
}
var realURIs = []denco.Record{
{"/authorizations", "/authorizations"},
{"/authorizations/:id", "/authorizations/:id"},
{"/applications/:client_id/tokens/:access_token", "/applications/:client_id/tokens/:access_token"},
{"/events", "/events"},
{"/repos/:owner/:repo/events", "/repos/:owner/:repo/events"},
{"/networks/:owner/:repo/events", "/networks/:owner/:repo/events"},
{"/orgs/:org/events", "/orgs/:org/events"},
{"/users/:user/received_events", "/users/:user/received_events"},
{"/users/:user/received_events/public", "/users/:user/received_events/public"},
{"/users/:user/events", "/users/:user/events"},
{"/users/:user/events/public", "/users/:user/events/public"},
{"/users/:user/events/orgs/:org", "/users/:user/events/orgs/:org"},
{"/feeds", "/feeds"},
{"/notifications", "/notifications"},
{"/repos/:owner/:repo/notifications", "/repos/:owner/:repo/notifications"},
{"/notifications/threads/:id", "/notifications/threads/:id"},
{"/notifications/threads/:id/subscription", "/notifications/threads/:id/subscription"},
{"/repos/:owner/:repo/stargazers", "/repos/:owner/:repo/stargazers"},
{"/users/:user/starred", "/users/:user/starred"},
{"/user/starred", "/user/starred"},
{"/user/starred/:owner/:repo", "/user/starred/:owner/:repo"},
{"/repos/:owner/:repo/subscribers", "/repos/:owner/:repo/subscribers"},
{"/users/:user/subscriptions", "/users/:user/subscriptions"},
{"/user/subscriptions", "/user/subscriptions"},
{"/repos/:owner/:repo/subscription", "/repos/:owner/:repo/subscription"},
{"/user/subscriptions/:owner/:repo", "/user/subscriptions/:owner/:repo"},
{"/users/:user/gists", "/users/:user/gists"},
{"/gists", "/gists"},
{"/gists/:id", "/gists/:id"},
{"/gists/:id/star", "/gists/:id/star"},
{"/repos/:owner/:repo/git/blobs/:sha", "/repos/:owner/:repo/git/blobs/:sha"},
{"/repos/:owner/:repo/git/commits/:sha", "/repos/:owner/:repo/git/commits/:sha"},
{"/repos/:owner/:repo/git/refs", "/repos/:owner/:repo/git/refs"},
{"/repos/:owner/:repo/git/tags/:sha", "/repos/:owner/:repo/git/tags/:sha"},
{"/repos/:owner/:repo/git/trees/:sha", "/repos/:owner/:repo/git/trees/:sha"},
{"/issues", "/issues"},
{"/user/issues", "/user/issues"},
{"/orgs/:org/issues", "/orgs/:org/issues"},
{"/repos/:owner/:repo/issues", "/repos/:owner/:repo/issues"},
{"/repos/:owner/:repo/issues/:number", "/repos/:owner/:repo/issues/:number"},
{"/repos/:owner/:repo/assignees", "/repos/:owner/:repo/assignees"},
{"/repos/:owner/:repo/assignees/:assignee", "/repos/:owner/:repo/assignees/:assignee"},
{"/repos/:owner/:repo/issues/:number/comments", "/repos/:owner/:repo/issues/:number/comments"},
{"/repos/:owner/:repo/issues/:number/events", "/repos/:owner/:repo/issues/:number/events"},
{"/repos/:owner/:repo/labels", "/repos/:owner/:repo/labels"},
{"/repos/:owner/:repo/labels/:name", "/repos/:owner/:repo/labels/:name"},
{"/repos/:owner/:repo/issues/:number/labels", "/repos/:owner/:repo/issues/:number/labels"},
{"/repos/:owner/:repo/milestones/:number/labels", "/repos/:owner/:repo/milestones/:number/labels"},
{"/repos/:owner/:repo/milestones", "/repos/:owner/:repo/milestones"},
{"/repos/:owner/:repo/milestones/:number", "/repos/:owner/:repo/milestones/:number"},
{"/emojis", "/emojis"},
{"/gitignore/templates", "/gitignore/templates"},
{"/gitignore/templates/:name", "/gitignore/templates/:name"},
{"/meta", "/meta"},
{"/rate_limit", "/rate_limit"},
{"/users/:user/orgs", "/users/:user/orgs"},
{"/user/orgs", "/user/orgs"},
{"/orgs/:org", "/orgs/:org"},
{"/orgs/:org/members", "/orgs/:org/members"},
{"/orgs/:org/members/:user", "/orgs/:org/members/:user"},
{"/orgs/:org/public_members", "/orgs/:org/public_members"},
{"/orgs/:org/public_members/:user", "/orgs/:org/public_members/:user"},
{"/orgs/:org/teams", "/orgs/:org/teams"},
{"/teams/:id", "/teams/:id"},
{"/teams/:id/members", "/teams/:id/members"},
{"/teams/:id/members/:user", "/teams/:id/members/:user"},
{"/teams/:id/repos", "/teams/:id/repos"},
{"/teams/:id/repos/:owner/:repo", "/teams/:id/repos/:owner/:repo"},
{"/user/teams", "/user/teams"},
{"/repos/:owner/:repo/pulls", "/repos/:owner/:repo/pulls"},
{"/repos/:owner/:repo/pulls/:number", "/repos/:owner/:repo/pulls/:number"},
{"/repos/:owner/:repo/pulls/:number/commits", "/repos/:owner/:repo/pulls/:number/commits"},
{"/repos/:owner/:repo/pulls/:number/files", "/repos/:owner/:repo/pulls/:number/files"},
{"/repos/:owner/:repo/pulls/:number/merge", "/repos/:owner/:repo/pulls/:number/merge"},
{"/repos/:owner/:repo/pulls/:number/comments", "/repos/:owner/:repo/pulls/:number/comments"},
{"/user/repos", "/user/repos"},
{"/users/:user/repos", "/users/:user/repos"},
{"/orgs/:org/repos", "/orgs/:org/repos"},
{"/repositories", "/repositories"},
{"/repos/:owner/:repo", "/repos/:owner/:repo"},
{"/repos/:owner/:repo/contributors", "/repos/:owner/:repo/contributors"},
{"/repos/:owner/:repo/languages", "/repos/:owner/:repo/languages"},
{"/repos/:owner/:repo/teams", "/repos/:owner/:repo/teams"},
{"/repos/:owner/:repo/tags", "/repos/:owner/:repo/tags"},
{"/repos/:owner/:repo/branches", "/repos/:owner/:repo/branches"},
{"/repos/:owner/:repo/branches/:branch", "/repos/:owner/:repo/branches/:branch"},
{"/repos/:owner/:repo/collaborators", "/repos/:owner/:repo/collaborators"},
{"/repos/:owner/:repo/collaborators/:user", "/repos/:owner/:repo/collaborators/:user"},
{"/repos/:owner/:repo/comments", "/repos/:owner/:repo/comments"},
{"/repos/:owner/:repo/commits/:sha/comments", "/repos/:owner/:repo/commits/:sha/comments"},
{"/repos/:owner/:repo/comments/:id", "/repos/:owner/:repo/comments/:id"},
{"/repos/:owner/:repo/commits", "/repos/:owner/:repo/commits"},
{"/repos/:owner/:repo/commits/:sha", "/repos/:owner/:repo/commits/:sha"},
{"/repos/:owner/:repo/readme", "/repos/:owner/:repo/readme"},
{"/repos/:owner/:repo/keys", "/repos/:owner/:repo/keys"},
{"/repos/:owner/:repo/keys/:id", "/repos/:owner/:repo/keys/:id"},
{"/repos/:owner/:repo/downloads", "/repos/:owner/:repo/downloads"},
{"/repos/:owner/:repo/downloads/:id", "/repos/:owner/:repo/downloads/:id"},
{"/repos/:owner/:repo/forks", "/repos/:owner/:repo/forks"},
{"/repos/:owner/:repo/hooks", "/repos/:owner/:repo/hooks"},
{"/repos/:owner/:repo/hooks/:id", "/repos/:owner/:repo/hooks/:id"},
{"/repos/:owner/:repo/releases", "/repos/:owner/:repo/releases"},
{"/repos/:owner/:repo/releases/:id", "/repos/:owner/:repo/releases/:id"},
{"/repos/:owner/:repo/releases/:id/assets", "/repos/:owner/:repo/releases/:id/assets"},
{"/repos/:owner/:repo/stats/contributors", "/repos/:owner/:repo/stats/contributors"},
{"/repos/:owner/:repo/stats/commit_activity", "/repos/:owner/:repo/stats/commit_activity"},
{"/repos/:owner/:repo/stats/code_frequency", "/repos/:owner/:repo/stats/code_frequency"},
{"/repos/:owner/:repo/stats/participation", "/repos/:owner/:repo/stats/participation"},
{"/repos/:owner/:repo/stats/punch_card", "/repos/:owner/:repo/stats/punch_card"},
{"/repos/:owner/:repo/statuses/:ref", "/repos/:owner/:repo/statuses/:ref"},
{"/search/repositories", "/search/repositories"},
{"/search/code", "/search/code"},
{"/search/issues", "/search/issues"},
{"/search/users", "/search/users"},
{"/legacy/issues/search/:owner/:repository/:state/:keyword", "/legacy/issues/search/:owner/:repository/:state/:keyword"},
{"/legacy/repos/search/:keyword", "/legacy/repos/search/:keyword"},
{"/legacy/user/search/:keyword", "/legacy/user/search/:keyword"},
{"/legacy/user/email/:email", "/legacy/user/email/:email"},
{"/users/:user", "/users/:user"},
{"/user", "/user"},
{"/users", "/users"},
{"/user/emails", "/user/emails"},
{"/users/:user/followers", "/users/:user/followers"},
{"/user/followers", "/user/followers"},
{"/users/:user/following", "/users/:user/following"},
{"/user/following", "/user/following"},
{"/user/following/:user", "/user/following/:user"},
{"/users/:user/following/:target_user", "/users/:user/following/:target_user"},
{"/users/:user/keys", "/users/:user/keys"},
{"/user/keys", "/user/keys"},
{"/user/keys/:id", "/user/keys/:id"},
{"/people/:userId", "/people/:userId"},
{"/people", "/people"},
{"/activities/:activityId/people/:collection", "/activities/:activityId/people/:collection"},
{"/people/:userId/people/:collection", "/people/:userId/people/:collection"},
{"/people/:userId/openIdConnect", "/people/:userId/openIdConnect"},
{"/people/:userId/activities/:collection", "/people/:userId/activities/:collection"},
{"/activities/:activityId", "/activities/:activityId"},
{"/activities", "/activities"},
{"/activities/:activityId/comments", "/activities/:activityId/comments"},
{"/comments/:commentId", "/comments/:commentId"},
{"/people/:userId/moments/:collection", "/people/:userId/moments/:collection"},
}
type testcase struct {
path string
value interface{}
params []denco.Param
found bool
}
func runLookupTest(t *testing.T, records []denco.Record, testcases []testcase) {
r := denco.New()
if err := r.Build(records); err != nil {
t.Fatal(err)
}
for _, testcase := range testcases {
data, params, found := r.Lookup(testcase.path)
if !reflect.DeepEqual(data, testcase.value) || !reflect.DeepEqual(params, denco.Params(testcase.params)) || !reflect.DeepEqual(found, testcase.found) {
t.Errorf("Router.Lookup(%q) => (%#v, %#v, %#v), want (%#v, %#v, %#v)", testcase.path, data, params, found, testcase.value, denco.Params(testcase.params), testcase.found)
}
}
}
func TestRouter_Lookup(t *testing.T) {
testcases := []testcase{
{"/", "testroute0", nil, true},
{"/gists/1323/foo/bar", "testroute11", []denco.Param{{"param1", "1323"}}, true},
{"/gists/1323/foo/133", "testroute12", []denco.Param{{"param1", "1323"}, {"param2", "133"}}, true},
{"/234/1323/foo/133", "testroute13", []denco.Param{{"param1", "234"}, {"param2", "1323"}, {"param3", "133"}}, true},
{"/path/to/route", "testroute1", nil, true},
{"/path/to/other", "testroute2", nil, true},
{"/path/to/route/a", "testroute3", nil, true},
{"/path/to/hoge", "testroute4", []denco.Param{{"param", "hoge"}}, true},
{"/path/to/wildcard/some/params", "testroute5", []denco.Param{{"routepath", "some/params"}}, true},
{"/path/to/o1/o2", "testroute6", []denco.Param{{"param1", "o1"}, {"param2", "o2"}}, true},
{"/path/to/p1/sep/p2", "testroute7", []denco.Param{{"param1", "p1"}, {"param2", "p2"}}, true},
{"/2014/01/06", "testroute8", []denco.Param{{"year", "2014"}, {"month", "01"}, {"day", "06"}}, true},
{"/user/777", "testroute9", []denco.Param{{"id", "777"}}, true},
{"/a/to/b/p1/some/wildcard/params", "testroute10", []denco.Param{{"param", "p1"}, {"routepath", "some/wildcard/params"}}, true},
{"/missing", nil, nil, false},
}
runLookupTest(t, routes(), testcases)
records := []denco.Record{
{"/", "testroute0"},
{"/:b", "testroute1"},
{"/*wildcard", "testroute2"},
}
testcases = []testcase{
{"/", "testroute0", nil, true},
{"/true", "testroute1", []denco.Param{{"b", "true"}}, true},
{"/foo/bar", "testroute2", []denco.Param{{"wildcard", "foo/bar"}}, true},
}
runLookupTest(t, records, testcases)
records = []denco.Record{
{"/networks/:owner/:repo/events", "testroute0"},
{"/orgs/:org/events", "testroute1"},
{"/notifications/threads/:id", "testroute2"},
{"/mypathisgreat/:thing-id", "testroute3"},
}
testcases = []testcase{
{"/networks/:owner/:repo/events", "testroute0", []denco.Param{{"owner", ":owner"}, {"repo", ":repo"}}, true},
{"/orgs/:org/events", "testroute1", []denco.Param{{"org", ":org"}}, true},
{"/notifications/threads/:id", "testroute2", []denco.Param{{"id", ":id"}}, true},
{"/mypathisgreat/:thing-id", "testroute3", []denco.Param{{"thing-id", ":thing-id"}}, true},
}
runLookupTest(t, records, testcases)
runLookupTest(t, []denco.Record{
{"/", "route2"},
}, []testcase{
{"/user/alice", nil, nil, false},
})
runLookupTest(t, []denco.Record{
{"/user/:name", "route1"},
}, []testcase{
{"/", nil, nil, false},
})
runLookupTest(t, []denco.Record{
{"/*wildcard", "testroute0"},
{"/a/:b", "testroute1"},
}, []testcase{
{"/a", "testroute0", []denco.Param{{"wildcard", "a"}}, true},
})
}
func TestRouter_Lookup_withManyRoutes(t *testing.T) {
n := 1000
rand.Seed(time.Now().UnixNano())
records := make([]denco.Record, n)
for i := 0; i < n; i++ {
records[i] = denco.Record{Key: "/" + randomString(rand.Intn(50)+10), Value: fmt.Sprintf("route%d", i)}
}
router := denco.New()
if err := router.Build(records); err != nil {
t.Fatal(err)
}
for _, r := range records {
data, params, found := router.Lookup(r.Key)
if !reflect.DeepEqual(data, r.Value) || len(params) != 0 || !reflect.DeepEqual(found, true) {
t.Errorf("Router.Lookup(%q) => (%#v, %#v, %#v), want (%#v, %#v, %#v)", r.Key, data, len(params), found, r.Value, 0, true)
}
}
}
func TestRouter_Lookup_realURIs(t *testing.T) {
testcases := []testcase{
{"/authorizations", "/authorizations", nil, true},
{"/authorizations/1", "/authorizations/:id", []denco.Param{{"id", "1"}}, true},
{"/applications/1/tokens/zohRoo7e", "/applications/:client_id/tokens/:access_token", []denco.Param{{"client_id", "1"}, {"access_token", "zohRoo7e"}}, true},
{"/events", "/events", nil, true},
{"/repos/naoina/denco/events", "/repos/:owner/:repo/events", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/networks/naoina/denco/events", "/networks/:owner/:repo/events", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/orgs/something/events", "/orgs/:org/events", []denco.Param{{"org", "something"}}, true},
{"/users/naoina/received_events", "/users/:user/received_events", []denco.Param{{"user", "naoina"}}, true},
{"/users/naoina/received_events/public", "/users/:user/received_events/public", []denco.Param{{"user", "naoina"}}, true},
{"/users/naoina/events", "/users/:user/events", []denco.Param{{"user", "naoina"}}, true},
{"/users/naoina/events/public", "/users/:user/events/public", []denco.Param{{"user", "naoina"}}, true},
{"/users/naoina/events/orgs/something", "/users/:user/events/orgs/:org", []denco.Param{{"user", "naoina"}, {"org", "something"}}, true},
{"/feeds", "/feeds", nil, true},
{"/notifications", "/notifications", nil, true},
{"/repos/naoina/denco/notifications", "/repos/:owner/:repo/notifications", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/notifications/threads/1", "/notifications/threads/:id", []denco.Param{{"id", "1"}}, true},
{"/notifications/threads/2/subscription", "/notifications/threads/:id/subscription", []denco.Param{{"id", "2"}}, true},
{"/repos/naoina/denco/stargazers", "/repos/:owner/:repo/stargazers", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/users/naoina/starred", "/users/:user/starred", []denco.Param{{"user", "naoina"}}, true},
{"/user/starred", "/user/starred", nil, true},
{"/user/starred/naoina/denco", "/user/starred/:owner/:repo", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/subscribers", "/repos/:owner/:repo/subscribers", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/users/naoina/subscriptions", "/users/:user/subscriptions", []denco.Param{{"user", "naoina"}}, true},
{"/user/subscriptions", "/user/subscriptions", nil, true},
{"/repos/naoina/denco/subscription", "/repos/:owner/:repo/subscription", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/user/subscriptions/naoina/denco", "/user/subscriptions/:owner/:repo", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/users/naoina/gists", "/users/:user/gists", []denco.Param{{"user", "naoina"}}, true},
{"/gists", "/gists", nil, true},
{"/gists/1", "/gists/:id", []denco.Param{{"id", "1"}}, true},
{"/gists/2/star", "/gists/:id/star", []denco.Param{{"id", "2"}}, true},
{"/repos/naoina/denco/git/blobs/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/blobs/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true},
{"/repos/naoina/denco/git/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/commits/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true},
{"/repos/naoina/denco/git/refs", "/repos/:owner/:repo/git/refs", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/git/tags/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/tags/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true},
{"/repos/naoina/denco/git/trees/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/trees/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true},
{"/issues", "/issues", nil, true},
{"/user/issues", "/user/issues", nil, true},
{"/orgs/something/issues", "/orgs/:org/issues", []denco.Param{{"org", "something"}}, true},
{"/repos/naoina/denco/issues", "/repos/:owner/:repo/issues", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/issues/1", "/repos/:owner/:repo/issues/:number", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/assignees", "/repos/:owner/:repo/assignees", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/assignees/foo", "/repos/:owner/:repo/assignees/:assignee", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"assignee", "foo"}}, true},
{"/repos/naoina/denco/issues/1/comments", "/repos/:owner/:repo/issues/:number/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/issues/1/events", "/repos/:owner/:repo/issues/:number/events", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/labels", "/repos/:owner/:repo/labels", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/labels/bug", "/repos/:owner/:repo/labels/:name", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"name", "bug"}}, true},
{"/repos/naoina/denco/issues/1/labels", "/repos/:owner/:repo/issues/:number/labels", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/milestones/1/labels", "/repos/:owner/:repo/milestones/:number/labels", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/milestones", "/repos/:owner/:repo/milestones", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/milestones/1", "/repos/:owner/:repo/milestones/:number", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/emojis", "/emojis", nil, true},
{"/gitignore/templates", "/gitignore/templates", nil, true},
{"/gitignore/templates/Go", "/gitignore/templates/:name", []denco.Param{{"name", "Go"}}, true},
{"/meta", "/meta", nil, true},
{"/rate_limit", "/rate_limit", nil, true},
{"/users/naoina/orgs", "/users/:user/orgs", []denco.Param{{"user", "naoina"}}, true},
{"/user/orgs", "/user/orgs", nil, true},
{"/orgs/something", "/orgs/:org", []denco.Param{{"org", "something"}}, true},
{"/orgs/something/members", "/orgs/:org/members", []denco.Param{{"org", "something"}}, true},
{"/orgs/something/members/naoina", "/orgs/:org/members/:user", []denco.Param{{"org", "something"}, {"user", "naoina"}}, true},
{"/orgs/something/public_members", "/orgs/:org/public_members", []denco.Param{{"org", "something"}}, true},
{"/orgs/something/public_members/naoina", "/orgs/:org/public_members/:user", []denco.Param{{"org", "something"}, {"user", "naoina"}}, true},
{"/orgs/something/teams", "/orgs/:org/teams", []denco.Param{{"org", "something"}}, true},
{"/teams/1", "/teams/:id", []denco.Param{{"id", "1"}}, true},
{"/teams/2/members", "/teams/:id/members", []denco.Param{{"id", "2"}}, true},
{"/teams/3/members/naoina", "/teams/:id/members/:user", []denco.Param{{"id", "3"}, {"user", "naoina"}}, true},
{"/teams/4/repos", "/teams/:id/repos", []denco.Param{{"id", "4"}}, true},
{"/teams/5/repos/naoina/denco", "/teams/:id/repos/:owner/:repo", []denco.Param{{"id", "5"}, {"owner", "naoina"}, {"repo", "denco"}}, true},
{"/user/teams", "/user/teams", nil, true},
{"/repos/naoina/denco/pulls", "/repos/:owner/:repo/pulls", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/pulls/1", "/repos/:owner/:repo/pulls/:number", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/pulls/1/commits", "/repos/:owner/:repo/pulls/:number/commits", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/pulls/1/files", "/repos/:owner/:repo/pulls/:number/files", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/pulls/1/merge", "/repos/:owner/:repo/pulls/:number/merge", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/repos/naoina/denco/pulls/1/comments", "/repos/:owner/:repo/pulls/:number/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true},
{"/user/repos", "/user/repos", nil, true},
{"/users/naoina/repos", "/users/:user/repos", []denco.Param{{"user", "naoina"}}, true},
{"/orgs/something/repos", "/orgs/:org/repos", []denco.Param{{"org", "something"}}, true},
{"/repositories", "/repositories", nil, true},
{"/repos/naoina/denco", "/repos/:owner/:repo", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/contributors", "/repos/:owner/:repo/contributors", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/languages", "/repos/:owner/:repo/languages", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/teams", "/repos/:owner/:repo/teams", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/tags", "/repos/:owner/:repo/tags", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/branches", "/repos/:owner/:repo/branches", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/branches/master", "/repos/:owner/:repo/branches/:branch", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"branch", "master"}}, true},
{"/repos/naoina/denco/collaborators", "/repos/:owner/:repo/collaborators", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/collaborators/something", "/repos/:owner/:repo/collaborators/:user", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"user", "something"}}, true},
{"/repos/naoina/denco/comments", "/repos/:owner/:repo/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9/comments", "/repos/:owner/:repo/commits/:sha/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true},
{"/repos/naoina/denco/comments/1", "/repos/:owner/:repo/comments/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true},
{"/repos/naoina/denco/commits", "/repos/:owner/:repo/commits", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/commits/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true},
{"/repos/naoina/denco/readme", "/repos/:owner/:repo/readme", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/keys", "/repos/:owner/:repo/keys", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/keys/1", "/repos/:owner/:repo/keys/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true},
{"/repos/naoina/denco/downloads", "/repos/:owner/:repo/downloads", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/downloads/2", "/repos/:owner/:repo/downloads/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "2"}}, true},
{"/repos/naoina/denco/forks", "/repos/:owner/:repo/forks", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/hooks", "/repos/:owner/:repo/hooks", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/hooks/2", "/repos/:owner/:repo/hooks/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "2"}}, true},
{"/repos/naoina/denco/releases", "/repos/:owner/:repo/releases", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/releases/1", "/repos/:owner/:repo/releases/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true},
{"/repos/naoina/denco/releases/1/assets", "/repos/:owner/:repo/releases/:id/assets", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true},
{"/repos/naoina/denco/stats/contributors", "/repos/:owner/:repo/stats/contributors", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/stats/commit_activity", "/repos/:owner/:repo/stats/commit_activity", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/stats/code_frequency", "/repos/:owner/:repo/stats/code_frequency", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/stats/participation", "/repos/:owner/:repo/stats/participation", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/stats/punch_card", "/repos/:owner/:repo/stats/punch_card", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true},
{"/repos/naoina/denco/statuses/master", "/repos/:owner/:repo/statuses/:ref", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"ref", "master"}}, true},
{"/search/repositories", "/search/repositories", nil, true},
{"/search/code", "/search/code", nil, true},
{"/search/issues", "/search/issues", nil, true},
{"/search/users", "/search/users", nil, true},
{"/legacy/issues/search/naoina/denco/closed/test", "/legacy/issues/search/:owner/:repository/:state/:keyword", []denco.Param{{"owner", "naoina"}, {"repository", "denco"}, {"state", "closed"}, {"keyword", "test"}}, true},
{"/legacy/repos/search/test", "/legacy/repos/search/:keyword", []denco.Param{{"keyword", "test"}}, true},
{"/legacy/user/search/test", "/legacy/user/search/:keyword", []denco.Param{{"keyword", "test"}}, true},
{"/legacy/user/email/naoina@kuune.org", "/legacy/user/email/:email", []denco.Param{{"email", "naoina@kuune.org"}}, true},
{"/users/naoina", "/users/:user", []denco.Param{{"user", "naoina"}}, true},
{"/user", "/user", nil, true},
{"/users", "/users", nil, true},
{"/user/emails", "/user/emails", nil, true},
{"/users/naoina/followers", "/users/:user/followers", []denco.Param{{"user", "naoina"}}, true},
{"/user/followers", "/user/followers", nil, true},
{"/users/naoina/following", "/users/:user/following", []denco.Param{{"user", "naoina"}}, true},
{"/user/following", "/user/following", nil, true},
{"/user/following/naoina", "/user/following/:user", []denco.Param{{"user", "naoina"}}, true},
{"/users/naoina/following/target", "/users/:user/following/:target_user", []denco.Param{{"user", "naoina"}, {"target_user", "target"}}, true},
{"/users/naoina/keys", "/users/:user/keys", []denco.Param{{"user", "naoina"}}, true},
{"/user/keys", "/user/keys", nil, true},
{"/user/keys/1", "/user/keys/:id", []denco.Param{{"id", "1"}}, true},
{"/people/me", "/people/:userId", []denco.Param{{"userId", "me"}}, true},
{"/people", "/people", nil, true},
{"/activities/foo/people/vault", "/activities/:activityId/people/:collection", []denco.Param{{"activityId", "foo"}, {"collection", "vault"}}, true},
{"/people/me/people/vault", "/people/:userId/people/:collection", []denco.Param{{"userId", "me"}, {"collection", "vault"}}, true},
{"/people/me/openIdConnect", "/people/:userId/openIdConnect", []denco.Param{{"userId", "me"}}, true},
{"/people/me/activities/vault", "/people/:userId/activities/:collection", []denco.Param{{"userId", "me"}, {"collection", "vault"}}, true},
{"/activities/foo", "/activities/:activityId", []denco.Param{{"activityId", "foo"}}, true},
{"/activities", "/activities", nil, true},
{"/activities/foo/comments", "/activities/:activityId/comments", []denco.Param{{"activityId", "foo"}}, true},
{"/comments/hoge", "/comments/:commentId", []denco.Param{{"commentId", "hoge"}}, true},
{"/people/me/moments/vault", "/people/:userId/moments/:collection", []denco.Param{{"userId", "me"}, {"collection", "vault"}}, true},
}
runLookupTest(t, realURIs, testcases)
}
func TestRouter_Build(t *testing.T) {
// test for duplicate name of path parameters.
func() {
r := denco.New()
if err := r.Build([]denco.Record{
{"/:user/:id/:id", "testroute0"},
{"/:user/:user/:id", "testroute0"},
}); err == nil {
t.Errorf("no error returned by duplicate name of path parameters")
}
}()
}
func TestRouter_Build_withoutSizeHint(t *testing.T) {
for _, v := range []struct {
keys []string
sizeHint int
}{
{[]string{"/user"}, 0},
{[]string{"/user/:id"}, 1},
{[]string{"/user/:id/post"}, 1},
{[]string{"/user/:id/:group"}, 2},
{[]string{"/user/:id/post/:cid"}, 2},
{[]string{"/user/:id/post/:cid", "/admin/:id/post/:cid"}, 2},
{[]string{"/user/:id", "/admin/:id/post/:cid"}, 2},
{[]string{"/user/:id/post/:cid", "/admin/:id/post/:cid/:type"}, 3},
} {
r := denco.New()
actual := r.SizeHint
expect := -1
if !reflect.DeepEqual(actual, expect) {
t.Errorf(`before Build; Router.SizeHint => (%[1]T=%#[1]v); want (%[2]T=%#[2]v)`, actual, expect)
}
records := make([]denco.Record, len(v.keys))
for i, k := range v.keys {
records[i] = denco.Record{Key: k, Value: "value"}
}
if err := r.Build(records); err != nil {
t.Fatal(err)
}
actual = r.SizeHint
expect = v.sizeHint
if !reflect.DeepEqual(actual, expect) {
t.Errorf(`Router.Build(%#v); Router.SizeHint => (%[2]T=%#[2]v); want (%[3]T=%#[3]v)`, records, actual, expect)
}
}
}
func TestRouter_Build_withSizeHint(t *testing.T) {
for _, v := range []struct {
key string
sizeHint int
expect int
}{
{"/user", 0, 0},
{"/user", 1, 1},
{"/user", 2, 2},
{"/user/:id", 3, 3},
{"/user/:id/:group", 0, 0},
{"/user/:id/:group", 1, 1},
} {
r := denco.New()
r.SizeHint = v.sizeHint
records := []denco.Record{
{v.key, "value"},
}
if err := r.Build(records); err != nil {
t.Fatal(err)
}
actual := r.SizeHint
expect := v.expect
if !reflect.DeepEqual(actual, expect) {
t.Errorf(`Router.Build(%#v); Router.SizeHint => (%[2]T=%#[2]v); want (%[3]T=%#[3]v)`, records, actual, expect)
}
}
}
func TestParams_Get(t *testing.T) {
params := denco.Params([]denco.Param{
{"name1", "value1"},
{"name2", "value2"},
{"name3", "value3"},
{"name1", "value4"},
})
for _, v := range []struct{ value, expected string }{
{"name1", "value1"},
{"name2", "value2"},
{"name3", "value3"},
{"name4", ""},
} {
actual := params.Get(v.value)
expected := v.expected
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Params.Get(%q) => %#v, want %#v", v.value, actual, expected)
}
}
}

View File

@@ -0,0 +1,106 @@
package denco
import (
"net/http"
)
// Mux represents a multiplexer for HTTP request.
type Mux struct{}
// NewMux returns a new Mux.
func NewMux() *Mux {
return &Mux{}
}
// GET is shorthand of Mux.Handler("GET", path, handler).
func (m *Mux) GET(path string, handler HandlerFunc) Handler {
return m.Handler("GET", path, handler)
}
// POST is shorthand of Mux.Handler("POST", path, handler).
func (m *Mux) POST(path string, handler HandlerFunc) Handler {
return m.Handler("POST", path, handler)
}
// PUT is shorthand of Mux.Handler("PUT", path, handler).
func (m *Mux) PUT(path string, handler HandlerFunc) Handler {
return m.Handler("PUT", path, handler)
}
// HEAD is shorthand of Mux.Handler("HEAD", path, handler).
func (m *Mux) HEAD(path string, handler HandlerFunc) Handler {
return m.Handler("HEAD", path, handler)
}
// Handler returns a handler for HTTP method.
func (m *Mux) Handler(method, path string, handler HandlerFunc) Handler {
return Handler{
Method: method,
Path: path,
Func: handler,
}
}
// Build builds a http.Handler.
func (m *Mux) Build(handlers []Handler) (http.Handler, error) {
recordMap := make(map[string][]Record)
for _, h := range handlers {
recordMap[h.Method] = append(recordMap[h.Method], NewRecord(h.Path, h.Func))
}
mux := newServeMux()
for m, records := range recordMap {
router := New()
if err := router.Build(records); err != nil {
return nil, err
}
mux.routers[m] = router
}
return mux, nil
}
// Handler represents a handler of HTTP request.
type Handler struct {
// Method is an HTTP method.
Method string
// Path is a routing path for handler.
Path string
// Func is a function of handler of HTTP request.
Func HandlerFunc
}
// The HandlerFunc type is aliased to type of handler function.
type HandlerFunc func(w http.ResponseWriter, r *http.Request, params Params)
type serveMux struct {
routers map[string]*Router
}
func newServeMux() *serveMux {
return &serveMux{
routers: make(map[string]*Router),
}
}
// ServeHTTP implements http.Handler interface.
func (mux *serveMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler, params := mux.handler(r.Method, r.URL.Path)
handler(w, r, params)
}
func (mux *serveMux) handler(method, path string) (HandlerFunc, []Param) {
if router, found := mux.routers[method]; found {
if handler, params, found := router.Lookup(path); found {
return handler.(HandlerFunc), params
}
}
return NotFound, nil
}
// NotFound replies to the request with an HTTP 404 not found error.
// NotFound is called when unknown HTTP method or a handler not found.
// If you want to use the your own NotFound handler, please overwrite this variable.
var NotFound = func(w http.ResponseWriter, r *http.Request, _ Params) {
http.NotFound(w, r)
}

View File

@@ -0,0 +1,106 @@
package denco_test
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-openapi/runtime/middleware/denco"
)
func testHandlerFunc(w http.ResponseWriter, r *http.Request, params denco.Params) {
fmt.Fprintf(w, "method: %s, path: %s, params: %v", r.Method, r.URL.Path, params)
}
func TestMux(t *testing.T) {
mux := denco.NewMux()
handler, err := mux.Build([]denco.Handler{
mux.GET("/", testHandlerFunc),
mux.GET("/user/:name", testHandlerFunc),
mux.POST("/user/:name", testHandlerFunc),
mux.HEAD("/user/:name", testHandlerFunc),
mux.PUT("/user/:name", testHandlerFunc),
mux.Handler("GET", "/user/handler", testHandlerFunc),
mux.Handler("POST", "/user/handler", testHandlerFunc),
{"PUT", "/user/inference", testHandlerFunc},
})
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
for _, v := range []struct {
status int
method, path, expected string
}{
{200, "GET", "/", "method: GET, path: /, params: []"},
{200, "GET", "/user/alice", "method: GET, path: /user/alice, params: [{name alice}]"},
{200, "POST", "/user/bob", "method: POST, path: /user/bob, params: [{name bob}]"},
{200, "HEAD", "/user/alice", ""},
{200, "PUT", "/user/bob", "method: PUT, path: /user/bob, params: [{name bob}]"},
{404, "POST", "/", "404 page not found\n"},
{404, "GET", "/unknown", "404 page not found\n"},
{404, "POST", "/user/alice/1", "404 page not found\n"},
{200, "GET", "/user/handler", "method: GET, path: /user/handler, params: []"},
{200, "POST", "/user/handler", "method: POST, path: /user/handler, params: []"},
{200, "PUT", "/user/inference", "method: PUT, path: /user/inference, params: []"},
} {
req, err := http.NewRequest(v.method, server.URL+v.path, nil)
if err != nil {
t.Error(err)
continue
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
continue
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Error(err)
continue
}
actual := string(body)
expected := v.expected
if res.StatusCode != v.status || actual != expected {
t.Errorf(`%s "%s" => %#v %#v, want %#v %#v`, v.method, v.path, res.StatusCode, actual, v.status, expected)
}
}
}
func TestNotFound(t *testing.T) {
mux := denco.NewMux()
handler, err := mux.Build([]denco.Handler{})
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
origNotFound := denco.NotFound
defer func() {
denco.NotFound = origNotFound
}()
denco.NotFound = func(w http.ResponseWriter, r *http.Request, params denco.Params) {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, "method: %s, path: %s, params: %v", r.Method, r.URL.Path, params)
}
res, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
actual := string(body)
expected := "method: GET, path: /, params: []"
if res.StatusCode != http.StatusServiceUnavailable || actual != expected {
t.Errorf(`GET "/" => %#v %#v, want %#v %#v`, res.StatusCode, actual, http.StatusServiceUnavailable, expected)
}
}

View File

@@ -0,0 +1,12 @@
package denco
// NextSeparator returns an index of next separator in path.
func NextSeparator(path string, start int) int {
for start < len(path) {
if c := path[start]; c == '/' || c == TerminationCharacter {
break
}
start++
}
return start
}

View File

@@ -0,0 +1,31 @@
package denco_test
import (
"reflect"
"testing"
"github.com/go-openapi/runtime/middleware/denco"
)
func TestNextSeparator(t *testing.T) {
for _, testcase := range []struct {
path string
start int
expected interface{}
}{
{"/path/to/route", 0, 0},
{"/path/to/route", 1, 5},
{"/path/to/route", 9, 14},
{"/path.html", 1, 10},
{"/foo/bar.html", 1, 4},
{"/foo/bar.html/baz.png", 5, 13},
{"/foo/bar.html/baz.png", 14, 21},
{"path#", 0, 4},
} {
actual := denco.NextSeparator(testcase.path, testcase.start)
expected := testcase.expected
if !reflect.DeepEqual(actual, expected) {
t.Errorf("path = %q, start = %v expect %v, but %v", testcase.path, testcase.start, expected, actual)
}
}
}

65
vendor/github.com/go-openapi/runtime/middleware/doc.go generated vendored Normal file
View File

@@ -0,0 +1,65 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*Package middleware provides the library with helper functions for serving swagger APIs.
Pseudo middleware handler
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/gorilla/context"
)
func newCompleteMiddleware(ctx *Context) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer context.Clear(r)
// use context to lookup routes
if matched, ok := ctx.RouteInfo(r); ok {
if len(matched.Authenticators) > 0 {
if _, err := ctx.Authorize(r, matched); err != nil {
ctx.Respond(rw, r, matched.Produces, matched, err)
return
}
}
bound, validation := ctx.BindAndValidate(r, matched)
if validation != nil {
ctx.Respond(rw, r, matched.Produces, matched, validation)
return
}
result, err := matched.Handler.Handle(bound)
if err != nil {
ctx.Respond(rw, r, matched.Produces, matched, err)
return
}
ctx.Respond(rw, r, matched.Produces, matched, result)
return
}
// Not found, check if it exists in the other methods first
if others := ctx.AllowedMethods(r); len(others) > 0 {
ctx.Respond(rw, r, ctx.spec.RequiredProduces(), nil, errors.MethodNotAllowed(r.Method, others))
return
}
ctx.Respond(rw, r, ctx.spec.RequiredProduces(), nil, errors.NotFound("path %s was not found", r.URL.Path))
})
}
*/
package middleware

View File

@@ -0,0 +1,299 @@
// Copyright 2013 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
// this file was taken from the github.com/golang/gddo repository
// Package header provides functions for parsing HTTP headers.
package header
import (
"net/http"
"strings"
"time"
)
// Octet types from RFC 2616.
var octetTypes [256]octetType
type octetType byte
const (
isToken octetType = 1 << iota
isSpace
)
func init() {
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpace
}
if isChar && !isCtl && !isSeparator {
t |= isToken
}
octetTypes[c] = t
}
}
// Copy returns a shallow copy of the header.
func Copy(header http.Header) http.Header {
h := make(http.Header)
for k, vs := range header {
h[k] = vs
}
return h
}
var timeLayouts = []string{"Mon, 02 Jan 2006 15:04:05 GMT", time.RFC850, time.ANSIC}
// ParseTime parses the header as time. The zero value is returned if the
// header is not present or there is an error parsing the
// header.
func ParseTime(header http.Header, key string) time.Time {
if s := header.Get(key); s != "" {
for _, layout := range timeLayouts {
if t, err := time.Parse(layout, s); err == nil {
return t.UTC()
}
}
}
return time.Time{}
}
// ParseList parses a comma separated list of values. Commas are ignored in
// quoted strings. Quoted values are not unescaped or unquoted. Whitespace is
// trimmed.
func ParseList(header http.Header, key string) []string {
var result []string
for _, s := range header[http.CanonicalHeaderKey(key)] {
begin := 0
end := 0
escape := false
quote := false
for i := 0; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
end = i + 1
case quote:
switch b {
case '\\':
escape = true
case '"':
quote = false
}
end = i + 1
case b == '"':
quote = true
end = i + 1
case octetTypes[b]&isSpace != 0:
if begin == end {
begin = i + 1
end = begin
}
case b == ',':
if begin < end {
result = append(result, s[begin:end])
}
begin = i + 1
end = begin
default:
end = i + 1
}
}
if begin < end {
result = append(result, s[begin:end])
}
}
return result
}
// ParseValueAndParams parses a comma separated list of values with optional
// semicolon separated name-value pairs. Content-Type and Content-Disposition
// headers are in this format.
func ParseValueAndParams(header http.Header, key string) (value string, params map[string]string) {
params = make(map[string]string)
s := header.Get(key)
value, s = expectTokenSlash(s)
if value == "" {
return
}
value = strings.ToLower(value)
s = skipSpace(s)
for strings.HasPrefix(s, ";") {
var pkey string
pkey, s = expectToken(skipSpace(s[1:]))
if pkey == "" {
return
}
if !strings.HasPrefix(s, "=") {
return
}
var pvalue string
pvalue, s = expectTokenOrQuoted(s[1:])
if pvalue == "" {
return
}
pkey = strings.ToLower(pkey)
params[pkey] = pvalue
s = skipSpace(s)
}
return
}
type AcceptSpec struct {
Value string
Q float64
}
// ParseAccept parses Accept* headers.
func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {
loop:
for _, s := range header[key] {
for {
var spec AcceptSpec
spec.Value, s = expectTokenSlash(s)
if spec.Value == "" {
continue loop
}
spec.Q = 1.0
s = skipSpace(s)
if strings.HasPrefix(s, ";") {
s = skipSpace(s[1:])
if !strings.HasPrefix(s, "q=") {
continue loop
}
spec.Q, s = expectQuality(s[2:])
if spec.Q < 0.0 {
continue loop
}
}
specs = append(specs, spec)
s = skipSpace(s)
if !strings.HasPrefix(s, ",") {
continue loop
}
s = skipSpace(s[1:])
}
}
return
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpace == 0 {
break
}
}
return s[i:]
}
func expectToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isToken == 0 {
break
}
}
return s[:i], s[i:]
}
func expectTokenSlash(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
b := s[i]
if (octetTypes[b]&isToken == 0) && b != '/' {
break
}
}
return s[:i], s[i:]
}
func expectQuality(s string) (q float64, rest string) {
switch {
case len(s) == 0:
return -1, ""
case s[0] == '0':
q = 0
case s[0] == '1':
q = 1
default:
return -1, ""
}
s = s[1:]
if !strings.HasPrefix(s, ".") {
return q, s
}
s = s[1:]
i := 0
n := 0
d := 1
for ; i < len(s); i++ {
b := s[i]
if b < '0' || b > '9' {
break
}
n = n*10 + int(b) - '0'
d *= 10
}
return q + float64(n)/float64(d), s[i:]
}
func expectTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return expectToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j += 1
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j += 1
}
}
return "", ""
}
}
return "", ""
}

View File

@@ -0,0 +1,82 @@
// Copyright 2013 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
// this file was taken from the github.com/golang/gddo repository
package middleware
import (
"net/http"
"strings"
"github.com/go-openapi/runtime/middleware/header"
)
// NegotiateContentEncoding returns the best offered content encoding for the
// request's Accept-Encoding header. If two offers match with equal weight and
// then the offer earlier in the list is preferred. If no offers are
// acceptable, then "" is returned.
func NegotiateContentEncoding(r *http.Request, offers []string) string {
bestOffer := "identity"
bestQ := -1.0
specs := header.ParseAccept(r.Header, "Accept-Encoding")
for _, offer := range offers {
for _, spec := range specs {
if spec.Q > bestQ &&
(spec.Value == "*" || spec.Value == offer) {
bestQ = spec.Q
bestOffer = offer
}
}
}
if bestQ == 0 {
bestOffer = ""
}
return bestOffer
}
// NegotiateContentType returns the best offered content type for the request's
// Accept header. If two offers match with equal weight, then the more specific
// offer is preferred. For example, text/* trumps */*. If two offers match
// with equal weight and specificity, then the offer earlier in the list is
// preferred. If no offers match, then defaultOffer is returned.
func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string {
bestOffer := defaultOffer
bestQ := -1.0
bestWild := 3
specs := header.ParseAccept(r.Header, "Accept")
for _, offer := range offers {
for _, spec := range specs {
switch {
case spec.Q == 0.0:
// ignore
case spec.Q < bestQ:
// better match found
case spec.Value == "*/*":
if spec.Q > bestQ || bestWild > 2 {
bestQ = spec.Q
bestWild = 2
bestOffer = offer
}
case strings.HasSuffix(spec.Value, "/*"):
if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
(spec.Q > bestQ || bestWild > 1) {
bestQ = spec.Q
bestWild = 1
bestOffer = offer
}
default:
if spec.Value == offer &&
(spec.Q > bestQ || bestWild > 0) {
bestQ = spec.Q
bestWild = 0
bestOffer = offer
}
}
}
}
return bestOffer
}

View File

@@ -0,0 +1,70 @@
// Copyright 2013 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
package middleware
import (
"net/http"
"testing"
)
var negotiateContentEncodingTests = []struct {
s string
offers []string
expect string
}{
{"", []string{"identity", "gzip"}, "identity"},
{"*;q=0", []string{"identity", "gzip"}, ""},
{"gzip", []string{"identity", "gzip"}, "gzip"},
}
func TestNegotiateContentEnoding(t *testing.T) {
for _, tt := range negotiateContentEncodingTests {
r := &http.Request{Header: http.Header{"Accept-Encoding": {tt.s}}}
actual := NegotiateContentEncoding(r, tt.offers)
if actual != tt.expect {
t.Errorf("NegotiateContentEncoding(%q, %#v)=%q, want %q", tt.s, tt.offers, actual, tt.expect)
}
}
}
var negotiateContentTypeTests = []struct {
s string
offers []string
defaultOffer string
expect string
}{
{"text/html, */*;q=0", []string{"x/y"}, "", ""},
{"text/html, */*", []string{"x/y"}, "", "x/y"},
{"text/html, image/png", []string{"text/html", "image/png"}, "", "text/html"},
{"text/html, image/png", []string{"image/png", "text/html"}, "", "image/png"},
{"text/html, image/png; q=0.5", []string{"image/png"}, "", "image/png"},
{"text/html, image/png; q=0.5", []string{"text/html"}, "", "text/html"},
{"text/html, image/png; q=0.5", []string{"foo/bar"}, "", ""},
{"text/html, image/png; q=0.5", []string{"image/png", "text/html"}, "", "text/html"},
{"text/html, image/png; q=0.5", []string{"text/html", "image/png"}, "", "text/html"},
{"text/html;q=0.5, image/png", []string{"image/png"}, "", "image/png"},
{"text/html;q=0.5, image/png", []string{"text/html"}, "", "text/html"},
{"text/html;q=0.5, image/png", []string{"image/png", "text/html"}, "", "image/png"},
{"text/html;q=0.5, image/png", []string{"text/html", "image/png"}, "", "image/png"},
{"image/png, image/*;q=0.5", []string{"image/jpg", "image/png"}, "", "image/png"},
{"image/png, image/*;q=0.5", []string{"image/jpg"}, "", "image/jpg"},
{"image/png, image/*;q=0.5", []string{"image/jpg", "image/gif"}, "", "image/jpg"},
{"image/png, image/*", []string{"image/jpg", "image/gif"}, "", "image/jpg"},
{"image/png, image/*", []string{"image/gif", "image/jpg"}, "", "image/gif"},
{"image/png, image/*", []string{"image/gif", "image/png"}, "", "image/png"},
{"image/png, image/*", []string{"image/png", "image/gif"}, "", "image/png"},
}
func TestNegotiateContentType(t *testing.T) {
for _, tt := range negotiateContentTypeTests {
r := &http.Request{Header: http.Header{"Accept": {tt.s}}}
actual := NegotiateContentType(r, tt.offers, tt.defaultOffer)
if actual != tt.expect {
t.Errorf("NegotiateContentType(%q, %#v, %q)=%q, want %q", tt.s, tt.offers, tt.defaultOffer, actual, tt.expect)
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"github.com/go-openapi/runtime"
)
type errorResp struct {
code int
response interface{}
headers http.Header
}
func (e *errorResp) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
for k, v := range e.headers {
for _, val := range v {
rw.Header().Add(k, val)
}
}
if e.code > 0 {
rw.WriteHeader(e.code)
} else {
rw.WriteHeader(http.StatusInternalServerError)
}
if err := producer.Produce(rw, e.response); err != nil {
panic(err)
}
}
// NotImplemented the error response when the response is not implemented
func NotImplemented(message string) Responder {
return &errorResp{http.StatusNotImplemented, message, make(http.Header)}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import "net/http"
// NewOperationExecutor creates a context aware middleware that handles the operations after routing
func NewOperationExecutor(ctx *Context) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// use context to lookup routes
route, _ := ctx.RouteInfo(r)
route.Handler.ServeHTTP(rw, r)
})
}

View File

@@ -0,0 +1,64 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/stretchr/testify/assert"
)
func TestOperationExecutor(t *testing.T) {
spec, api := petstore.NewAPI(t)
api.RegisterOperation("get", "/pets", runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) {
return []interface{}{
map[string]interface{}{"id": 1, "name": "a dog"},
}, nil
}))
context := NewContext(spec, api, nil)
context.router = DefaultRouter(spec, context.api)
mw := NewOperationExecutor(context)
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/api/pets", nil)
request.Header.Add("Accept", "application/json")
request.SetBasicAuth("admin", "admin")
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, `[{"id":1,"name":"a dog"}]`+"\n", recorder.Body.String())
spec, api = petstore.NewAPI(t)
api.RegisterOperation("get", "/pets", runtime.OperationHandlerFunc(func(params interface{}) (interface{}, error) {
return nil, errors.New(422, "expected")
}))
context = NewContext(spec, api, nil)
context.router = DefaultRouter(spec, context.api)
mw = NewOperationExecutor(context)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/api/pets", nil)
request.Header.Add("Accept", "application/json")
request.SetBasicAuth("admin", "admin")
mw.ServeHTTP(recorder, request)
assert.Equal(t, 422, recorder.Code)
assert.Equal(t, `{"code":422,"message":"expected"}`, recorder.Body.String())
}

View File

@@ -0,0 +1,480 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"encoding"
"encoding/base64"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
const defaultMaxMemory = 32 << 20
var textUnmarshalType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
func newUntypedParamBinder(param spec.Parameter, spec *spec.Swagger, formats strfmt.Registry) *untypedParamBinder {
binder := new(untypedParamBinder)
binder.Name = param.Name
binder.parameter = &param
binder.formats = formats
if param.In != "body" {
binder.validator = validate.NewParamValidator(&param, formats)
} else {
binder.validator = validate.NewSchemaValidator(param.Schema, spec, param.Name, formats)
}
return binder
}
type untypedParamBinder struct {
parameter *spec.Parameter
formats strfmt.Registry
Name string
validator validate.EntityValidator
}
func (p *untypedParamBinder) Type() reflect.Type {
return p.typeForSchema(p.parameter.Type, p.parameter.Format, p.parameter.Items)
}
func (p *untypedParamBinder) typeForSchema(tpe, format string, items *spec.Items) reflect.Type {
switch tpe {
case "boolean":
return reflect.TypeOf(true)
case "string":
if tt, ok := p.formats.GetType(format); ok {
return tt
}
return reflect.TypeOf("")
case "integer":
switch format {
case "int8":
return reflect.TypeOf(int8(0))
case "int16":
return reflect.TypeOf(int16(0))
case "int32":
return reflect.TypeOf(int32(0))
case "int64":
return reflect.TypeOf(int64(0))
default:
return reflect.TypeOf(int64(0))
}
case "number":
switch format {
case "float":
return reflect.TypeOf(float32(0))
case "double":
return reflect.TypeOf(float64(0))
}
case "array":
if items == nil {
return nil
}
itemsType := p.typeForSchema(items.Type, items.Format, items.Items)
if itemsType == nil {
return nil
}
return reflect.MakeSlice(reflect.SliceOf(itemsType), 0, 0).Type()
case "file":
return reflect.TypeOf(&runtime.File{}).Elem()
case "object":
return reflect.TypeOf(map[string]interface{}{})
}
return nil
}
func (p *untypedParamBinder) allowsMulti() bool {
return p.parameter.In == "query" || p.parameter.In == "formData"
}
func (p *untypedParamBinder) readValue(values runtime.Gettable, target reflect.Value) ([]string, bool, bool, error) {
name, in, cf, tpe := p.parameter.Name, p.parameter.In, p.parameter.CollectionFormat, p.parameter.Type
if tpe == "array" {
if cf == "multi" {
if !p.allowsMulti() {
return nil, false, false, errors.InvalidCollectionFormat(name, in, cf)
}
vv, hasKey, _ := values.GetOK(name)
return vv, false, hasKey, nil
}
v, hk, hv := values.GetOK(name)
if !hv {
return nil, false, hk, nil
}
d, c, e := p.readFormattedSliceFieldValue(v[len(v)-1], target)
return d, c, hk, e
}
vv, hk, _ := values.GetOK(name)
return vv, false, hk, nil
}
func (p *untypedParamBinder) Bind(request *http.Request, routeParams RouteParams, consumer runtime.Consumer, target reflect.Value) error {
// fmt.Println("binding", p.name, "as", p.Type())
switch p.parameter.In {
case "query":
data, custom, hasKey, err := p.readValue(runtime.Values(request.URL.Query()), target)
if err != nil {
return err
}
if custom {
return nil
}
return p.bindValue(data, hasKey, target)
case "header":
data, custom, hasKey, err := p.readValue(runtime.Values(request.Header), target)
if err != nil {
return err
}
if custom {
return nil
}
return p.bindValue(data, hasKey, target)
case "path":
data, custom, hasKey, err := p.readValue(routeParams, target)
if err != nil {
return err
}
if custom {
return nil
}
return p.bindValue(data, hasKey, target)
case "formData":
var err error
var mt string
mt, _, e := runtime.ContentType(request.Header)
if e != nil {
// because of the interface conversion go thinks the error is not nil
// so we first check for nil and then set the err var if it's not nil
err = e
}
if err != nil {
return errors.InvalidContentType("", []string{"multipart/form-data", "application/x-www-form-urlencoded"})
}
if mt != "multipart/form-data" && mt != "application/x-www-form-urlencoded" {
return errors.InvalidContentType(mt, []string{"multipart/form-data", "application/x-www-form-urlencoded"})
}
if mt == "multipart/form-data" {
if err := request.ParseMultipartForm(defaultMaxMemory); err != nil {
return errors.NewParseError(p.Name, p.parameter.In, "", err)
}
}
if err := request.ParseForm(); err != nil {
return errors.NewParseError(p.Name, p.parameter.In, "", err)
}
if p.parameter.Type == "file" {
file, header, err := request.FormFile(p.parameter.Name)
if err != nil {
return errors.NewParseError(p.Name, p.parameter.In, "", err)
}
target.Set(reflect.ValueOf(runtime.File{Data: file, Header: header}))
return nil
}
if request.MultipartForm != nil {
data, custom, hasKey, err := p.readValue(runtime.Values(request.MultipartForm.Value), target)
if err != nil {
return err
}
if custom {
return nil
}
return p.bindValue(data, hasKey, target)
}
data, custom, hasKey, err := p.readValue(runtime.Values(request.PostForm), target)
if err != nil {
return err
}
if custom {
return nil
}
return p.bindValue(data, hasKey, target)
case "body":
newValue := reflect.New(target.Type())
if !runtime.HasBody(request) {
if p.parameter.Default != nil {
target.Set(reflect.ValueOf(p.parameter.Default))
}
return nil
}
if err := consumer.Consume(request.Body, newValue.Interface()); err != nil {
if err == io.EOF && p.parameter.Default != nil {
target.Set(reflect.ValueOf(p.parameter.Default))
return nil
}
tpe := p.parameter.Type
if p.parameter.Format != "" {
tpe = p.parameter.Format
}
return errors.InvalidType(p.Name, p.parameter.In, tpe, nil)
}
target.Set(reflect.Indirect(newValue))
return nil
default:
return errors.New(500, fmt.Sprintf("invalid parameter location %q", p.parameter.In))
}
}
func (p *untypedParamBinder) bindValue(data []string, hasKey bool, target reflect.Value) error {
if p.parameter.Type == "array" {
return p.setSliceFieldValue(target, p.parameter.Default, data, hasKey)
}
var d string
if len(data) > 0 {
d = data[len(data)-1]
}
return p.setFieldValue(target, p.parameter.Default, d, hasKey)
}
func (p *untypedParamBinder) setFieldValue(target reflect.Value, defaultValue interface{}, data string, hasKey bool) error {
tpe := p.parameter.Type
if p.parameter.Format != "" {
tpe = p.parameter.Format
}
if (!hasKey || (!p.parameter.AllowEmptyValue && data == "")) && p.parameter.Required && p.parameter.Default == nil {
return errors.Required(p.Name, p.parameter.In)
}
ok, err := p.tryUnmarshaler(target, defaultValue, data)
if err != nil {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if ok {
return nil
}
defVal := reflect.Zero(target.Type())
if defaultValue != nil {
defVal = reflect.ValueOf(defaultValue)
}
if tpe == "byte" {
if data == "" {
if target.CanSet() {
target.SetBytes(defVal.Bytes())
}
return nil
}
b, err := base64.StdEncoding.DecodeString(data)
if err != nil {
b, err = base64.URLEncoding.DecodeString(data)
if err != nil {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
}
if target.CanSet() {
target.SetBytes(b)
}
return nil
}
switch target.Kind() {
case reflect.Bool:
if data == "" {
if target.CanSet() {
target.SetBool(defVal.Bool())
}
return nil
}
b, err := swag.ConvertBool(data)
if err != nil {
return err
}
if target.CanSet() {
target.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if data == "" {
if target.CanSet() {
rd := defVal.Convert(reflect.TypeOf(int64(0)))
target.SetInt(rd.Int())
}
return nil
}
i, err := strconv.ParseInt(data, 10, 64)
if err != nil {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if target.OverflowInt(i) {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if target.CanSet() {
target.SetInt(i)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if data == "" {
if target.CanSet() {
rd := defVal.Convert(reflect.TypeOf(uint64(0)))
target.SetUint(rd.Uint())
}
return nil
}
u, err := strconv.ParseUint(data, 10, 64)
if err != nil {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if target.OverflowUint(u) {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if target.CanSet() {
target.SetUint(u)
}
case reflect.Float32, reflect.Float64:
if data == "" {
if target.CanSet() {
rd := defVal.Convert(reflect.TypeOf(float64(0)))
target.SetFloat(rd.Float())
}
return nil
}
f, err := strconv.ParseFloat(data, 64)
if err != nil {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if target.OverflowFloat(f) {
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
if target.CanSet() {
target.SetFloat(f)
}
case reflect.String:
value := data
if value == "" {
value = defVal.String()
}
// validate string
if target.CanSet() {
target.SetString(value)
}
case reflect.Ptr:
if data == "" && defVal.Kind() == reflect.Ptr {
if target.CanSet() {
target.Set(defVal)
}
return nil
}
newVal := reflect.New(target.Type().Elem())
if err := p.setFieldValue(reflect.Indirect(newVal), defVal, data, hasKey); err != nil {
return err
}
if target.CanSet() {
target.Set(newVal)
}
default:
return errors.InvalidType(p.Name, p.parameter.In, tpe, data)
}
return nil
}
func (p *untypedParamBinder) tryUnmarshaler(target reflect.Value, defaultValue interface{}, data string) (bool, error) {
if !target.CanSet() {
return false, nil
}
// When a type implements encoding.TextUnmarshaler we'll use that instead of reflecting some more
if reflect.PtrTo(target.Type()).Implements(textUnmarshalType) {
if defaultValue != nil && len(data) == 0 {
target.Set(reflect.ValueOf(defaultValue))
return true, nil
}
value := reflect.New(target.Type())
if err := value.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(data)); err != nil {
return true, err
}
target.Set(reflect.Indirect(value))
return true, nil
}
return false, nil
}
func (p *untypedParamBinder) readFormattedSliceFieldValue(data string, target reflect.Value) ([]string, bool, error) {
ok, err := p.tryUnmarshaler(target, p.parameter.Default, data)
if err != nil {
return nil, true, err
}
if ok {
return nil, true, nil
}
return swag.SplitByFormat(data, p.parameter.CollectionFormat), false, nil
}
func (p *untypedParamBinder) setSliceFieldValue(target reflect.Value, defaultValue interface{}, data []string, hasKey bool) error {
sz := len(data)
if (!hasKey || (!p.parameter.AllowEmptyValue && (sz == 0 || (sz == 1 && data[0] == "")))) && p.parameter.Required && defaultValue == nil {
return errors.Required(p.Name, p.parameter.In)
}
defVal := reflect.Zero(target.Type())
if defaultValue != nil {
defVal = reflect.ValueOf(defaultValue)
}
if !target.CanSet() {
return nil
}
if sz == 0 {
target.Set(defVal)
return nil
}
value := reflect.MakeSlice(reflect.SliceOf(target.Type().Elem()), sz, sz)
for i := 0; i < sz; i++ {
if err := p.setFieldValue(value.Index(i), nil, data[i], hasKey); err != nil {
return err
}
}
target.Set(value)
return nil
}

View File

@@ -0,0 +1,340 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"math"
"net/url"
"reflect"
"strconv"
"testing"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/stretchr/testify/assert"
)
type email struct {
Address string
}
type paramFactory func(string) *spec.Parameter
var paramFactories = []paramFactory{
spec.QueryParam,
spec.HeaderParam,
spec.PathParam,
spec.FormDataParam,
}
func np(param *spec.Parameter) *untypedParamBinder {
return newUntypedParamBinder(*param, new(spec.Swagger), strfmt.Default)
}
var stringItems = new(spec.Items)
func init() {
stringItems.Type = "string"
}
func testCollectionFormat(t *testing.T, param *spec.Parameter, valid bool) {
binder := &untypedParamBinder{
parameter: param,
}
_, _, _, err := binder.readValue(runtime.Values(nil), reflect.ValueOf(nil))
if valid {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, errors.InvalidCollectionFormat(param.Name, param.In, param.CollectionFormat), err)
}
}
func requiredError(param *spec.Parameter) *errors.Validation {
return errors.Required(param.Name, param.In)
}
func validateRequiredTest(t *testing.T, param *spec.Parameter, value reflect.Value) {
binder := np(param)
err := binder.bindValue([]string{}, true, value)
assert.Error(t, err)
assert.NotNil(t, param)
assert.EqualError(t, requiredError(param), err.Error())
err = binder.bindValue([]string{""}, true, value)
if assert.Error(t, err) {
assert.EqualError(t, requiredError(param), err.Error())
}
// should be impossible data, but let's go with it
err = binder.bindValue([]string{"a"}, false, value)
assert.Error(t, err)
assert.EqualError(t, requiredError(param), err.Error())
err = binder.bindValue([]string{""}, false, value)
assert.Error(t, err)
assert.EqualError(t, requiredError(param), err.Error())
}
func validateRequiredAllowEmptyTest(t *testing.T, param *spec.Parameter, value reflect.Value) {
param.AllowEmptyValue = true
binder := np(param)
err := binder.bindValue([]string{}, true, value)
assert.NoError(t, err)
if assert.NotNil(t, param) {
err = binder.bindValue([]string{""}, true, value)
assert.NoError(t, err)
err = binder.bindValue([]string{"1"}, false, value)
assert.Error(t, err)
assert.EqualError(t, requiredError(param), err.Error())
err = binder.bindValue([]string{""}, false, value)
assert.Error(t, err)
assert.EqualError(t, requiredError(param), err.Error())
}
}
func TestRequiredValidation(t *testing.T) {
strParam := spec.QueryParam("name").Typed("string", "").AsRequired()
validateRequiredTest(t, strParam, reflect.ValueOf(""))
validateRequiredAllowEmptyTest(t, strParam, reflect.ValueOf(""))
intParam := spec.QueryParam("id").Typed("integer", "int32").AsRequired()
validateRequiredTest(t, intParam, reflect.ValueOf(int32(0)))
validateRequiredAllowEmptyTest(t, intParam, reflect.ValueOf(int32(0)))
longParam := spec.QueryParam("id").Typed("integer", "int64").AsRequired()
validateRequiredTest(t, longParam, reflect.ValueOf(int64(0)))
validateRequiredAllowEmptyTest(t, longParam, reflect.ValueOf(int64(0)))
floatParam := spec.QueryParam("score").Typed("number", "float").AsRequired()
validateRequiredTest(t, floatParam, reflect.ValueOf(float32(0)))
validateRequiredAllowEmptyTest(t, floatParam, reflect.ValueOf(float32(0)))
doubleParam := spec.QueryParam("score").Typed("number", "double").AsRequired()
validateRequiredTest(t, doubleParam, reflect.ValueOf(float64(0)))
validateRequiredAllowEmptyTest(t, doubleParam, reflect.ValueOf(float64(0)))
dateTimeParam := spec.QueryParam("registered").Typed("string", "date-time").AsRequired()
validateRequiredTest(t, dateTimeParam, reflect.ValueOf(strfmt.DateTime{}))
// validateRequiredAllowEmptyTest(t, dateTimeParam, reflect.ValueOf(strfmt.DateTime{}))
dateParam := spec.QueryParam("registered").Typed("string", "date").AsRequired()
validateRequiredTest(t, dateParam, reflect.ValueOf(strfmt.Date{}))
// validateRequiredAllowEmptyTest(t, dateParam, reflect.ValueOf(strfmt.DateTime{}))
sliceParam := spec.QueryParam("tags").CollectionOf(stringItems, "").AsRequired()
validateRequiredTest(t, sliceParam, reflect.MakeSlice(reflect.TypeOf([]string{}), 0, 0))
validateRequiredAllowEmptyTest(t, sliceParam, reflect.MakeSlice(reflect.TypeOf([]string{}), 0, 0))
}
func TestInvalidCollectionFormat(t *testing.T) {
validCf1 := spec.QueryParam("validFmt").CollectionOf(stringItems, "multi")
validCf2 := spec.FormDataParam("validFmt2").CollectionOf(stringItems, "multi")
invalidCf1 := spec.HeaderParam("invalidHdr").CollectionOf(stringItems, "multi")
invalidCf2 := spec.PathParam("invalidPath").CollectionOf(stringItems, "multi")
testCollectionFormat(t, validCf1, true)
testCollectionFormat(t, validCf2, true)
testCollectionFormat(t, invalidCf1, false)
testCollectionFormat(t, invalidCf2, false)
}
func invalidTypeError(param *spec.Parameter, data interface{}) *errors.Validation {
tpe := param.Type
if param.Format != "" {
tpe = param.Format
}
return errors.InvalidType(param.Name, param.In, tpe, data)
}
func TestTypeValidation(t *testing.T) {
for _, newParam := range paramFactories {
intParam := newParam("badInt").Typed("integer", "int32")
value := reflect.ValueOf(int32(0))
binder := np(intParam)
err := binder.bindValue([]string{"yada"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(intParam, "yada"), err)
// fails for overflow
val := int64(math.MaxInt32)
str := strconv.FormatInt(val, 10) + "0"
v := int32(0)
value = reflect.ValueOf(&v).Elem()
binder = np(intParam)
err = binder.bindValue([]string{str}, true, value)
assert.Error(t, err)
assert.Equal(t, invalidTypeError(intParam, str), err)
longParam := newParam("badLong").Typed("integer", "int64")
value = reflect.ValueOf(int64(0))
binder = np(longParam)
err = binder.bindValue([]string{"yada"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(longParam, "yada"), err)
// fails for overflow
str2 := strconv.FormatInt(math.MaxInt64, 10) + "0"
v2 := int64(0)
vv2 := reflect.ValueOf(&v2).Elem()
binder = np(longParam)
err = binder.bindValue([]string{str2}, true, vv2)
assert.Error(t, err)
assert.Equal(t, invalidTypeError(longParam, str2), err)
floatParam := newParam("badFloat").Typed("number", "float")
value = reflect.ValueOf(float64(0))
binder = np(floatParam)
err = binder.bindValue([]string{"yada"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(floatParam, "yada"), err)
// fails for overflow
str3 := strconv.FormatFloat(math.MaxFloat64, 'f', 5, 64)
v3 := reflect.TypeOf(float32(0))
value = reflect.New(v3).Elem()
binder = np(floatParam)
err = binder.bindValue([]string{str3}, true, value)
assert.Error(t, err)
assert.Equal(t, invalidTypeError(floatParam, str3), err)
doubleParam := newParam("badDouble").Typed("number", "double")
value = reflect.ValueOf(float64(0))
binder = np(doubleParam)
err = binder.bindValue([]string{"yada"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(doubleParam, "yada"), err)
// fails for overflow
str4 := "9" + strconv.FormatFloat(math.MaxFloat64, 'f', 5, 64)
v4 := reflect.TypeOf(float64(0))
value = reflect.New(v4).Elem()
binder = np(doubleParam)
err = binder.bindValue([]string{str4}, true, value)
assert.Error(t, err)
assert.Equal(t, invalidTypeError(doubleParam, str4), err)
dateParam := newParam("badDate").Typed("string", "date")
value = reflect.ValueOf(strfmt.Date{})
binder = np(dateParam)
err = binder.bindValue([]string{"yada"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(dateParam, "yada"), err)
dateTimeParam := newParam("badDateTime").Typed("string", "date-time")
value = reflect.ValueOf(strfmt.DateTime{})
binder = np(dateTimeParam)
err = binder.bindValue([]string{"yada"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(dateTimeParam, "yada"), err)
byteParam := newParam("badByte").Typed("string", "byte")
values := url.Values(map[string][]string{})
values.Add("badByte", "yaüda")
v5 := []byte{}
value = reflect.ValueOf(&v5).Elem()
binder = np(byteParam)
err = binder.bindValue([]string{"yaüda"}, true, value)
// fails for invalid string
assert.Error(t, err)
assert.Equal(t, invalidTypeError(byteParam, "yaüda"), err)
}
}
func TestTypeDetectionInvalidItems(t *testing.T) {
withoutItems := spec.QueryParam("without").CollectionOf(nil, "")
binder := &untypedParamBinder{
Name: "without",
parameter: withoutItems,
}
assert.Nil(t, binder.Type())
items := new(spec.Items)
items.Type = "array"
withInvalidItems := spec.QueryParam("invalidItems").CollectionOf(items, "")
binder = &untypedParamBinder{
Name: "invalidItems",
parameter: withInvalidItems,
}
assert.Nil(t, binder.Type())
noType := spec.QueryParam("invalidType")
noType.Type = "invalid"
binder = &untypedParamBinder{
Name: "invalidType",
parameter: noType,
}
assert.Nil(t, binder.Type())
}
// type emailStrFmt struct {
// name string
// tpe reflect.Type
// validator FormatValidator
// }
//
// func (e *emailStrFmt) Name() string {
// return e.name
// }
//
// func (e *emailStrFmt) Type() reflect.Type {
// return e.tpe
// }
//
// func (e *emailStrFmt) Matches(str string) bool {
// return e.validator(str)
// }
//
// func TestTypeDetectionValid(t *testing.T) {
// // emlFmt := &emailStrFmt{
// // name: "email",
// // tpe: reflect.TypeOf(email{}),
// // }
// // formats := []StringFormat{emlFmt}
//
// expected := map[string]reflect.Type{
// "name": reflect.TypeOf(""),
// "id": reflect.TypeOf(int64(0)),
// "age": reflect.TypeOf(int32(0)),
// "score": reflect.TypeOf(float32(0)),
// "factor": reflect.TypeOf(float64(0)),
// "friend": reflect.TypeOf(map[string]interface{}{}),
// "X-Request-Id": reflect.TypeOf(int64(0)),
// "tags": reflect.TypeOf([]string{}),
// "confirmed": reflect.TypeOf(true),
// "planned": reflect.TypeOf(swagger.Date{}),
// "delivered": reflect.TypeOf(swagger.DateTime{}),
// "email": reflect.TypeOf(email{}),
// "picture": reflect.TypeOf([]byte{}),
// "file": reflect.TypeOf(&swagger.File{}).Elem(),
// }
//
// params := parametersForAllTypes("")
// emailParam := spec.QueryParam("email").Typed("string", "email")
// params["email"] = *emailParam
//
// fileParam := spec.FileParam("file")
// params["file"] = *fileParam
//
// for _, v := range params {
// binder := &paramBinder{
// formats: formats,
// name: v.Name,
// parameter: &v,
// }
// assert.Equal(t, expected[v.Name], binder.Type(), "name: %s", v.Name)
// }
// }

View File

@@ -0,0 +1,101 @@
package middleware
import (
"bytes"
"fmt"
"html/template"
"net/http"
"path"
)
// RedocOpts configures the Redoc middlewares
type RedocOpts struct {
// BasePath for the UI path, defaults to: /
BasePath string
// Path combines with BasePath for the full UI path, defaults to: docs
Path string
// SpecURL the url to find the spec for
SpecURL string
// RedocURL for the js that generates the redoc site, defaults to: https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js
RedocURL string
// Title for the documentation site, default to: API documentation
Title string
}
// EnsureDefaults in case some options are missing
func (r *RedocOpts) EnsureDefaults() {
if r.BasePath == "" {
r.BasePath = "/"
}
if r.Path == "" {
r.Path = "docs"
}
if r.SpecURL == "" {
r.SpecURL = "/swagger.json"
}
if r.RedocURL == "" {
r.RedocURL = redocLatest
}
if r.Title == "" {
r.Title = "API documentation"
}
}
// Redoc creates a middleware to serve a documentation site for a swagger spec.
// This allows for altering the spec before starting the http listener.
//
func Redoc(opts RedocOpts, next http.Handler) http.Handler {
opts.EnsureDefaults()
pth := path.Join(opts.BasePath, opts.Path)
tmpl := template.Must(template.New("redoc").Parse(redocTemplate))
buf := bytes.NewBuffer(nil)
tmpl.Execute(buf, opts)
b := buf.Bytes()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == pth {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusNotFound)
rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
return
}
next.ServeHTTP(rw, r)
})
}
const (
redocLatest = "https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"
redocTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{ .Title }}</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='{{ .SpecURL }}'></redoc>
<script src="{{ .RedocURL }}"> </script>
</body>
</html>
`
)

View File

@@ -0,0 +1,22 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRedocMiddleware(t *testing.T) {
redoc := Redoc(RedocOpts{}, nil)
req, _ := http.NewRequest("GET", "/docs", nil)
recorder := httptest.NewRecorder()
redoc.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "text/html; charset=utf-8", recorder.Header().Get("Content-Type"))
assert.Contains(t, recorder.Body.String(), "<title>API documentation</title>")
assert.Contains(t, recorder.Body.String(), "<redoc spec-url='/swagger.json'></redoc>")
assert.Contains(t, recorder.Body.String(), redocLatest)
}

View File

@@ -0,0 +1,104 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"reflect"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
)
// RequestBinder binds and validates the data from a http request
type untypedRequestBinder struct {
Spec *spec.Swagger
Parameters map[string]spec.Parameter
Formats strfmt.Registry
paramBinders map[string]*untypedParamBinder
}
// NewRequestBinder creates a new binder for reading a request.
func newUntypedRequestBinder(parameters map[string]spec.Parameter, spec *spec.Swagger, formats strfmt.Registry) *untypedRequestBinder {
binders := make(map[string]*untypedParamBinder)
for fieldName, param := range parameters {
binders[fieldName] = newUntypedParamBinder(param, spec, formats)
}
return &untypedRequestBinder{
Parameters: parameters,
paramBinders: binders,
Spec: spec,
Formats: formats,
}
}
// Bind perform the databinding and validation
func (o *untypedRequestBinder) Bind(request *http.Request, routeParams RouteParams, consumer runtime.Consumer, data interface{}) error {
val := reflect.Indirect(reflect.ValueOf(data))
isMap := val.Kind() == reflect.Map
var result []error
debugLog("binding %d parameters for %s %s", len(o.Parameters), request.Method, request.URL.EscapedPath())
for fieldName, param := range o.Parameters {
binder := o.paramBinders[fieldName]
debugLog("binding paramter %s for %s %s", fieldName, request.Method, request.URL.EscapedPath())
var target reflect.Value
if !isMap {
binder.Name = fieldName
target = val.FieldByName(fieldName)
}
if isMap {
tpe := binder.Type()
if tpe == nil {
if param.Schema.Type.Contains("array") {
tpe = reflect.TypeOf([]interface{}{})
} else {
tpe = reflect.TypeOf(map[string]interface{}{})
}
}
target = reflect.Indirect(reflect.New(tpe))
}
if !target.IsValid() {
result = append(result, errors.New(500, "parameter name %q is an unknown field", binder.Name))
continue
}
if err := binder.Bind(request, routeParams, consumer, target); err != nil {
result = append(result, err)
continue
}
if binder.validator != nil {
rr := binder.validator.Validate(target.Interface())
if rr != nil && rr.HasErrors() {
result = append(result, rr.AsError())
}
}
if isMap {
val.SetMapIndex(reflect.ValueOf(param.Name), target)
}
}
if len(result) > 0 {
return errors.CompositeValidationError(result...)
}
return nil
}

View File

@@ -0,0 +1,481 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"bytes"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/go-openapi/runtime"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/stretchr/testify/assert"
)
type stubConsumer struct {
}
func (s *stubConsumer) Consume(_ io.Reader, _ interface{}) error {
return nil
}
type friend struct {
Name string `json:"name"`
Age int `json:"age"`
}
type jsonRequestParams struct {
ID int64 // path
Name string // query
Friend friend // body
RequestID int64 // header
Tags []string // csv
}
type jsonRequestPtr struct {
ID int64 // path
Name string // query
RequestID int64 // header
Tags []string // csv
Friend *friend
}
type jsonRequestSlice struct {
ID int64 // path
Name string // query
RequestID int64 // header
Tags []string // csv
Friend []friend
}
type jsonRequestAllTypes struct {
Confirmed bool
Planned strfmt.Date
Delivered strfmt.DateTime
Age int32
ID int64
Score float32
Factor float64
Friend friend
Name string
Tags []string
Picture []byte
RequestID int64
}
func parametersForAllTypes(fmt string) map[string]spec.Parameter {
if fmt == "" {
fmt = "csv"
}
nameParam := spec.QueryParam("name").Typed("string", "")
idParam := spec.PathParam("id").Typed("integer", "int64")
ageParam := spec.QueryParam("age").Typed("integer", "int32")
scoreParam := spec.QueryParam("score").Typed("number", "float")
factorParam := spec.QueryParam("factor").Typed("number", "double")
friendSchema := new(spec.Schema).Typed("object", "")
friendParam := spec.BodyParam("friend", friendSchema)
requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64")
requestIDParam.Extensions = spec.Extensions(map[string]interface{}{})
requestIDParam.Extensions.Add("go-name", "RequestID")
items := new(spec.Items)
items.Type = "string"
tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt)
confirmedParam := spec.QueryParam("confirmed").Typed("boolean", "")
plannedParam := spec.QueryParam("planned").Typed("string", "date")
deliveredParam := spec.QueryParam("delivered").Typed("string", "date-time")
pictureParam := spec.QueryParam("picture").Typed("string", "byte") // base64 encoded during transport
return map[string]spec.Parameter{
"ID": *idParam,
"Name": *nameParam,
"RequestID": *requestIDParam,
"Friend": *friendParam,
"Tags": *tagsParam,
"Age": *ageParam,
"Score": *scoreParam,
"Factor": *factorParam,
"Confirmed": *confirmedParam,
"Planned": *plannedParam,
"Delivered": *deliveredParam,
"Picture": *pictureParam,
}
}
func parametersForJSONRequestParams(fmt string) map[string]spec.Parameter {
if fmt == "" {
fmt = "csv"
}
nameParam := spec.QueryParam("name").Typed("string", "")
idParam := spec.PathParam("id").Typed("integer", "int64")
friendSchema := new(spec.Schema).Typed("object", "")
friendParam := spec.BodyParam("friend", friendSchema)
requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64")
requestIDParam.Extensions = spec.Extensions(map[string]interface{}{})
requestIDParam.Extensions.Add("go-name", "RequestID")
items := new(spec.Items)
items.Type = "string"
tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt)
return map[string]spec.Parameter{
"ID": *idParam,
"Name": *nameParam,
"RequestID": *requestIDParam,
"Friend": *friendParam,
"Tags": *tagsParam,
}
}
func parametersForJSONRequestSliceParams(fmt string) map[string]spec.Parameter {
if fmt == "" {
fmt = "csv"
}
nameParam := spec.QueryParam("name").Typed("string", "")
idParam := spec.PathParam("id").Typed("integer", "int64")
friendSchema := new(spec.Schema).Typed("object", "")
friendParam := spec.BodyParam("friend", spec.ArrayProperty(friendSchema))
requestIDParam := spec.HeaderParam("X-Request-Id").Typed("integer", "int64")
requestIDParam.Extensions = spec.Extensions(map[string]interface{}{})
requestIDParam.Extensions.Add("go-name", "RequestID")
items := new(spec.Items)
items.Type = "string"
tagsParam := spec.QueryParam("tags").CollectionOf(items, fmt)
return map[string]spec.Parameter{
"ID": *idParam,
"Name": *nameParam,
"RequestID": *requestIDParam,
"Friend": *friendParam,
"Tags": *tagsParam,
}
}
func TestRequestBindingDefaultValue(t *testing.T) {
confirmed := true
name := "thomas"
friend := map[string]interface{}{"name": "toby", "age": float64(32)}
id, age, score, factor := int64(7575), int32(348), float32(5.309), float64(37.403)
requestID := 19394858
tags := []string{"one", "two", "three"}
dt1 := time.Date(2014, 8, 9, 0, 0, 0, 0, time.UTC)
planned := strfmt.Date(dt1)
dt2 := time.Date(2014, 10, 12, 8, 5, 5, 0, time.UTC)
delivered := strfmt.DateTime(dt2)
uri, _ := url.Parse("http://localhost:8002/hello")
defaults := map[string]interface{}{
"id": id,
"age": age,
"score": score,
"factor": factor,
"name": name,
"friend": friend,
"X-Request-Id": requestID,
"tags": tags,
"confirmed": confirmed,
"planned": planned,
"delivered": delivered,
"picture": []byte("hello"),
}
op2 := parametersForAllTypes("")
op3 := make(map[string]spec.Parameter)
for k, p := range op2 {
p.Default = defaults[p.Name]
op3[k] = p
}
req, _ := http.NewRequest("POST", uri.String(), bytes.NewBuffer(nil))
req.Header.Set("Content-Type", "application/json")
binder := newUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default)
data := make(map[string]interface{})
err := binder.Bind(req, RouteParams(nil), runtime.JSONConsumer(), &data)
assert.NoError(t, err)
assert.Equal(t, defaults["id"], data["id"])
assert.Equal(t, name, data["name"])
assert.Equal(t, friend, data["friend"])
assert.EqualValues(t, requestID, data["X-Request-Id"])
assert.Equal(t, tags, data["tags"])
assert.Equal(t, planned, data["planned"])
assert.Equal(t, delivered, data["delivered"])
assert.Equal(t, confirmed, data["confirmed"])
assert.Equal(t, age, data["age"])
assert.Equal(t, factor, data["factor"])
assert.Equal(t, score, data["score"])
assert.Equal(t, "hello", string(data["picture"].(strfmt.Base64)))
}
func TestRequestBindingForInvalid(t *testing.T) {
invalidParam := spec.QueryParam("some")
op1 := map[string]spec.Parameter{"Some": *invalidParam}
binder := newUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default)
req, _ := http.NewRequest("GET", "http://localhost:8002/hello?name=the-name", nil)
err := binder.Bind(req, nil, new(stubConsumer), new(jsonRequestParams))
assert.Error(t, err)
op2 := parametersForJSONRequestParams("")
binder = newUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default)
req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{"name":"toby","age":32}`)))
req.Header.Set("Content-Type", "application(")
data := jsonRequestParams{}
err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
assert.Error(t, err)
req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{]`)))
req.Header.Set("Content-Type", "application/json")
data = jsonRequestParams{}
err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
assert.Error(t, err)
invalidMultiParam := spec.HeaderParam("tags").CollectionOf(new(spec.Items), "multi")
op3 := map[string]spec.Parameter{"Tags": *invalidMultiParam}
binder = newUntypedRequestBinder(op3, new(spec.Swagger), strfmt.Default)
req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{}`)))
req.Header.Set("Content-Type", "application/json")
data = jsonRequestParams{}
err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
assert.Error(t, err)
invalidMultiParam = spec.PathParam("").CollectionOf(new(spec.Items), "multi")
op4 := map[string]spec.Parameter{"Tags": *invalidMultiParam}
binder = newUntypedRequestBinder(op4, new(spec.Swagger), strfmt.Default)
req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{}`)))
req.Header.Set("Content-Type", "application/json")
data = jsonRequestParams{}
err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
assert.Error(t, err)
invalidInParam := spec.HeaderParam("tags").Typed("string", "")
invalidInParam.In = "invalid"
op5 := map[string]spec.Parameter{"Tags": *invalidInParam}
binder = newUntypedRequestBinder(op5, new(spec.Swagger), strfmt.Default)
req, _ = http.NewRequest("POST", "http://localhost:8002/hello/1?name=the-name", bytes.NewBuffer([]byte(`{}`)))
req.Header.Set("Content-Type", "application/json")
data = jsonRequestParams{}
err = binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
assert.Error(t, err)
}
func TestRequestBindingForValid(t *testing.T) {
for _, fmt := range []string{"csv", "pipes", "tsv", "ssv", "multi"} {
op1 := parametersForJSONRequestParams(fmt)
binder := newUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default)
lval := []string{"one", "two", "three"}
queryString := ""
switch fmt {
case "multi":
queryString = strings.Join(lval, "&tags=")
case "ssv":
queryString = strings.Join(lval, " ")
case "pipes":
queryString = strings.Join(lval, "|")
case "tsv":
queryString = strings.Join(lval, "\t")
default:
queryString = strings.Join(lval, ",")
}
urlStr := "http://localhost:8002/hello/1?name=the-name&tags=" + queryString
req, _ := http.NewRequest("POST", urlStr, bytes.NewBuffer([]byte(`{"name":"toby","age":32}`)))
req.Header.Set("Content-Type", "application/json;charset=utf-8")
req.Header.Set("X-Request-Id", "1325959595")
data := jsonRequestParams{}
err := binder.Bind(req, RouteParams([]RouteParam{{"id", "1"}}), runtime.JSONConsumer(), &data)
expected := jsonRequestParams{
ID: 1,
Name: "the-name",
Friend: friend{"toby", 32},
RequestID: 1325959595,
Tags: []string{"one", "two", "three"},
}
assert.NoError(t, err)
assert.Equal(t, expected, data)
}
op1 := parametersForJSONRequestParams("")
binder := newUntypedRequestBinder(op1, new(spec.Swagger), strfmt.Default)
urlStr := "http://localhost:8002/hello/1?name=the-name&tags=one,two,three"
req, _ := http.NewRequest("POST", urlStr, bytes.NewBuffer([]byte(`{"name":"toby","age":32}`)))
req.Header.Set("Content-Type", "application/json;charset=utf-8")
req.Header.Set("X-Request-Id", "1325959595")
data2 := jsonRequestPtr{}
err := binder.Bind(req, []RouteParam{{"id", "1"}}, runtime.JSONConsumer(), &data2)
expected2 := jsonRequestPtr{
Friend: &friend{"toby", 32},
Tags: []string{"one", "two", "three"},
}
assert.NoError(t, err)
if data2.Friend == nil {
t.Fatal("friend is nil")
}
assert.Equal(t, *expected2.Friend, *data2.Friend)
assert.Equal(t, expected2.Tags, data2.Tags)
req, _ = http.NewRequest("POST", urlStr, bytes.NewBuffer([]byte(`[{"name":"toby","age":32}]`)))
req.Header.Set("Content-Type", "application/json;charset=utf-8")
req.Header.Set("X-Request-Id", "1325959595")
op2 := parametersForJSONRequestSliceParams("")
binder = newUntypedRequestBinder(op2, new(spec.Swagger), strfmt.Default)
data3 := jsonRequestSlice{}
err = binder.Bind(req, []RouteParam{{"id", "1"}}, runtime.JSONConsumer(), &data3)
expected3 := jsonRequestSlice{
Friend: []friend{{"toby", 32}},
Tags: []string{"one", "two", "three"},
}
assert.NoError(t, err)
assert.Equal(t, expected3.Friend, data3.Friend)
assert.Equal(t, expected3.Tags, data3.Tags)
}
type formRequest struct {
Name string
Age int
}
func parametersForFormUpload() map[string]spec.Parameter {
nameParam := spec.FormDataParam("name").Typed("string", "")
ageParam := spec.FormDataParam("age").Typed("integer", "int32")
return map[string]spec.Parameter{"Name": *nameParam, "Age": *ageParam}
}
func TestFormUpload(t *testing.T) {
params := parametersForFormUpload()
binder := newUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default)
urlStr := "http://localhost:8002/hello"
req, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(`name=the-name&age=32`))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
data := formRequest{}
res := binder.Bind(req, nil, runtime.JSONConsumer(), &data)
assert.NoError(t, res)
assert.Equal(t, "the-name", data.Name)
assert.Equal(t, 32, data.Age)
req, _ = http.NewRequest("POST", urlStr, bytes.NewBufferString(`name=%3&age=32`))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
data = formRequest{}
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
}
type fileRequest struct {
Name string // body
File runtime.File // upload
}
func paramsForFileUpload() *untypedRequestBinder {
nameParam := spec.FormDataParam("name").Typed("string", "")
fileParam := spec.FileParam("file")
params := map[string]spec.Parameter{"Name": *nameParam, "File": *fileParam}
return newUntypedRequestBinder(params, new(spec.Swagger), strfmt.Default)
}
func TestBindingFileUpload(t *testing.T) {
binder := paramsForFileUpload()
body := bytes.NewBuffer(nil)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "plain-jane.txt")
assert.NoError(t, err)
part.Write([]byte("the file contents"))
writer.WriteField("name", "the-name")
assert.NoError(t, writer.Close())
urlStr := "http://localhost:8002/hello"
req, _ := http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
data := fileRequest{}
assert.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
assert.Equal(t, "the-name", data.Name)
assert.NotNil(t, data.File)
assert.NotNil(t, data.File.Header)
assert.Equal(t, "plain-jane.txt", data.File.Header.Filename)
bb, err := ioutil.ReadAll(data.File.Data)
assert.NoError(t, err)
assert.Equal(t, []byte("the file contents"), bb)
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", "application/json")
data = fileRequest{}
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", "application(")
data = fileRequest{}
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
body = bytes.NewBuffer(nil)
writer = multipart.NewWriter(body)
part, err = writer.CreateFormFile("bad-name", "plain-jane.txt")
assert.NoError(t, err)
part.Write([]byte("the file contents"))
writer.WriteField("name", "the-name")
assert.NoError(t, writer.Close())
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
data = fileRequest{}
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.MultipartReader()
data = fileRequest{}
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
}

View File

@@ -0,0 +1,38 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRouteParams(t *testing.T) {
coll1 := RouteParams([]RouteParam{
{"blah", "foo"},
{"abc", "bar"},
{"ccc", "efg"},
})
v := coll1.Get("blah")
assert.Equal(t, v, "foo")
v2 := coll1.Get("abc")
assert.Equal(t, v2, "bar")
v3 := coll1.Get("ccc")
assert.Equal(t, v3, "efg")
v4 := coll1.Get("ydkdk")
assert.Empty(t, v4)
}

View File

@@ -0,0 +1,270 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"net/url"
fpath "path"
"regexp"
"strings"
"github.com/go-openapi/analysis"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware/denco"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/gorilla/context"
)
// RouteParam is a object to capture route params in a framework agnostic way.
// implementations of the muxer should use these route params to communicate with the
// swagger framework
type RouteParam struct {
Name string
Value string
}
// RouteParams the collection of route params
type RouteParams []RouteParam
// Get gets the value for the route param for the specified key
func (r RouteParams) Get(name string) string {
vv, _, _ := r.GetOK(name)
if len(vv) > 0 {
return vv[len(vv)-1]
}
return ""
}
// GetOK gets the value but also returns booleans to indicate if a key or value
// is present. This aids in validation and satisfies an interface in use there
//
// The returned values are: data, has key, has value
func (r RouteParams) GetOK(name string) ([]string, bool, bool) {
for _, p := range r {
if p.Name == name {
return []string{p.Value}, true, p.Value != ""
}
}
return nil, false, false
}
// NewRouter creates a new context aware router middleware
func NewRouter(ctx *Context, next http.Handler) http.Handler {
if ctx.router == nil {
ctx.router = DefaultRouter(ctx.spec, ctx.api)
}
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer context.Clear(r)
if _, ok := ctx.RouteInfo(r); ok {
next.ServeHTTP(rw, r)
return
}
// Not found, check if it exists in the other methods first
if others := ctx.AllowedMethods(r); len(others) > 0 {
ctx.Respond(rw, r, ctx.analyzer.RequiredProduces(), nil, errors.MethodNotAllowed(r.Method, others))
return
}
ctx.Respond(rw, r, ctx.analyzer.RequiredProduces(), nil, errors.NotFound("path %s was not found", r.URL.EscapedPath()))
})
}
// RoutableAPI represents an interface for things that can serve
// as a provider of implementations for the swagger router
type RoutableAPI interface {
HandlerFor(string, string) (http.Handler, bool)
ServeErrorFor(string) func(http.ResponseWriter, *http.Request, error)
ConsumersFor([]string) map[string]runtime.Consumer
ProducersFor([]string) map[string]runtime.Producer
AuthenticatorsFor(map[string]spec.SecurityScheme) map[string]runtime.Authenticator
Formats() strfmt.Registry
DefaultProduces() string
DefaultConsumes() string
}
// Router represents a swagger aware router
type Router interface {
Lookup(method, path string) (*MatchedRoute, bool)
OtherMethods(method, path string) []string
}
type defaultRouteBuilder struct {
spec *loads.Document
analyzer *analysis.Spec
api RoutableAPI
records map[string][]denco.Record
}
type defaultRouter struct {
spec *loads.Document
api RoutableAPI
routers map[string]*denco.Router
}
func newDefaultRouteBuilder(spec *loads.Document, api RoutableAPI) *defaultRouteBuilder {
return &defaultRouteBuilder{
spec: spec,
analyzer: analysis.New(spec.Spec()),
api: api,
records: make(map[string][]denco.Record),
}
}
// DefaultRouter creates a default implemenation of the router
func DefaultRouter(spec *loads.Document, api RoutableAPI) Router {
builder := newDefaultRouteBuilder(spec, api)
if spec != nil {
for method, paths := range builder.analyzer.Operations() {
for path, operation := range paths {
fp := fpath.Join(spec.BasePath(), path)
debugLog("adding route %s %s %q", method, fp, operation.ID)
builder.AddRoute(method, fp, operation)
}
}
}
return builder.Build()
}
type routeEntry struct {
PathPattern string
BasePath string
Operation *spec.Operation
Consumes []string
Consumers map[string]runtime.Consumer
Produces []string
Producers map[string]runtime.Producer
Parameters map[string]spec.Parameter
Handler http.Handler
Formats strfmt.Registry
Binder *untypedRequestBinder
Authenticators map[string]runtime.Authenticator
Scopes map[string][]string
}
// MatchedRoute represents the route that was matched in this request
type MatchedRoute struct {
routeEntry
Params RouteParams
Consumer runtime.Consumer
Producer runtime.Producer
}
func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) {
mth := strings.ToUpper(method)
debugLog("looking up route for %s %s", method, path)
if Debug {
if len(d.routers) == 0 {
debugLog("there are no known routers")
}
for meth := range d.routers {
debugLog("got a router for %s", meth)
}
}
if router, ok := d.routers[mth]; ok {
if m, rp, ok := router.Lookup(fpath.Clean(path)); ok && m != nil {
if entry, ok := m.(*routeEntry); ok {
debugLog("found a route for %s %s with %d parameters", method, path, len(entry.Parameters))
var params RouteParams
for _, p := range rp {
v, err := url.QueryUnescape(p.Value)
if err != nil {
debugLog("failed to escape %q: %v", p.Value, err)
v = p.Value
}
params = append(params, RouteParam{Name: p.Name, Value: v})
}
return &MatchedRoute{routeEntry: *entry, Params: params}, true
}
} else {
debugLog("couldn't find a route by path for %s %s", method, path)
}
} else {
debugLog("couldn't find a route by method for %s %s", method, path)
}
return nil, false
}
func (d *defaultRouter) OtherMethods(method, path string) []string {
mn := strings.ToUpper(method)
var methods []string
for k, v := range d.routers {
if k != mn {
if _, _, ok := v.Lookup(fpath.Clean(path)); ok {
methods = append(methods, k)
continue
}
}
}
return methods
}
var pathConverter = regexp.MustCompile(`{(.+?)}`)
func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Operation) {
mn := strings.ToUpper(method)
bp := fpath.Clean(d.spec.BasePath())
if len(bp) > 0 && bp[len(bp)-1] == '/' {
bp = bp[:len(bp)-1]
}
if handler, ok := d.api.HandlerFor(method, strings.TrimPrefix(path, bp)); ok {
consumes := d.analyzer.ConsumesFor(operation)
produces := d.analyzer.ProducesFor(operation)
parameters := d.analyzer.ParamsFor(method, strings.TrimPrefix(path, bp))
definitions := d.analyzer.SecurityDefinitionsFor(operation)
requirements := d.analyzer.SecurityRequirementsFor(operation)
scopes := make(map[string][]string, len(requirements))
for _, v := range requirements {
scopes[v.Name] = v.Scopes
}
record := denco.NewRecord(pathConverter.ReplaceAllString(path, ":$1"), &routeEntry{
BasePath: bp,
PathPattern: path,
Operation: operation,
Handler: handler,
Consumes: consumes,
Produces: produces,
Consumers: d.api.ConsumersFor(consumes),
Producers: d.api.ProducersFor(produces),
Parameters: parameters,
Formats: d.api.Formats(),
Binder: newUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats()),
Authenticators: d.api.AuthenticatorsFor(definitions),
Scopes: scopes,
})
d.records[mn] = append(d.records[mn], record)
}
}
func (d *defaultRouteBuilder) Build() *defaultRouter {
routers := make(map[string]*denco.Router)
for method, records := range d.records {
router := denco.New()
router.Build(records)
routers[method] = router
}
return &defaultRouter{
spec: d.spec,
routers: routers,
}
}

View File

@@ -0,0 +1,227 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"net/http/httptest"
"sort"
"strings"
"testing"
"github.com/go-openapi/analysis"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/go-openapi/runtime/middleware/untyped"
"github.com/stretchr/testify/assert"
)
func terminator(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
}
func TestRouterMiddleware(t *testing.T) {
spec, api := petstore.NewAPI(t)
context := NewContext(spec, api, nil)
mw := NewRouter(context, http.HandlerFunc(terminator))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/api/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("DELETE", "/api/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusMethodNotAllowed, recorder.Code)
methods := strings.Split(recorder.Header().Get("Allow"), ",")
sort.Sort(sort.StringSlice(methods))
assert.Equal(t, "GET,POST", strings.Join(methods, ","))
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/nopets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotFound, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotFound, recorder.Code)
spec, api = petstore.NewRootAPI(t)
context = NewContext(spec, api, nil)
mw = NewRouter(context, http.HandlerFunc(terminator))
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("DELETE", "/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusMethodNotAllowed, recorder.Code)
methods = strings.Split(recorder.Header().Get("Allow"), ",")
sort.Sort(sort.StringSlice(methods))
assert.Equal(t, "GET,POST", strings.Join(methods, ","))
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/nopets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotFound, recorder.Code)
}
func TestRouterBuilder(t *testing.T) {
spec, api := petstore.NewAPI(t)
analyzed := analysis.New(spec.Spec())
assert.Len(t, analyzed.RequiredConsumes(), 3)
assert.Len(t, analyzed.RequiredProduces(), 5)
assert.Len(t, analyzed.OperationIDs(), 4)
// context := NewContext(spec, api)
builder := petAPIRouterBuilder(spec, api, analyzed)
getRecords := builder.records["GET"]
postRecords := builder.records["POST"]
deleteRecords := builder.records["DELETE"]
assert.Len(t, getRecords, 2)
assert.Len(t, postRecords, 1)
assert.Len(t, deleteRecords, 1)
assert.Empty(t, builder.records["PATCH"])
assert.Empty(t, builder.records["OPTIONS"])
assert.Empty(t, builder.records["HEAD"])
assert.Empty(t, builder.records["PUT"])
rec := postRecords[0]
assert.Equal(t, rec.Key, "/pets")
val := rec.Value.(*routeEntry)
assert.Len(t, val.Consumers, 1)
assert.Len(t, val.Producers, 1)
assert.Len(t, val.Consumes, 1)
assert.Len(t, val.Produces, 1)
assert.Len(t, val.Parameters, 1)
recG := getRecords[0]
assert.Equal(t, recG.Key, "/pets")
valG := recG.Value.(*routeEntry)
assert.Len(t, valG.Consumers, 2)
assert.Len(t, valG.Producers, 4)
assert.Len(t, valG.Consumes, 2)
assert.Len(t, valG.Produces, 4)
assert.Len(t, valG.Parameters, 2)
}
func TestRouterCanonicalBasePath(t *testing.T) {
spec, api := petstore.NewAPI(t)
spec.Spec().BasePath = "/api///"
context := NewContext(spec, api, nil)
mw := NewRouter(context, http.HandlerFunc(terminator))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/api/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
}
func TestRouter_EscapedPath(t *testing.T) {
spec, api := petstore.NewAPI(t)
spec.Spec().BasePath = "/api/"
context := NewContext(spec, api, nil)
mw := NewRouter(context, http.HandlerFunc(terminator))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/api/pets/123", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/api/pets/abc%2Fdef", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
ri, _ := context.RouteInfo(request)
if assert.NotNil(t, ri) {
if assert.NotNil(t, ri.Params) {
assert.Equal(t, "abc/def", ri.Params.Get("id"))
}
}
}
func TestRouterStruct(t *testing.T) {
spec, api := petstore.NewAPI(t)
router := DefaultRouter(spec, newRoutableUntypedAPI(spec, api, new(Context)))
methods := router.OtherMethods("post", "/api/pets/{id}")
assert.Len(t, methods, 2)
entry, ok := router.Lookup("delete", "/api/pets/{id}")
assert.True(t, ok)
assert.NotNil(t, entry)
assert.Len(t, entry.Params, 1)
assert.Equal(t, "id", entry.Params[0].Name)
_, ok = router.Lookup("delete", "/pets")
assert.False(t, ok)
_, ok = router.Lookup("post", "/no-pets")
assert.False(t, ok)
}
func petAPIRouterBuilder(spec *loads.Document, api *untyped.API, analyzed *analysis.Spec) *defaultRouteBuilder {
builder := newDefaultRouteBuilder(spec, newRoutableUntypedAPI(spec, api, new(Context)))
builder.AddRoute("GET", "/pets", analyzed.AllPaths()["/pets"].Get)
builder.AddRoute("POST", "/pets", analyzed.AllPaths()["/pets"].Post)
builder.AddRoute("DELETE", "/pets/{id}", analyzed.AllPaths()["/pets/{id}"].Delete)
builder.AddRoute("GET", "/pets/{id}", analyzed.AllPaths()["/pets/{id}"].Get)
return builder
}
func TestPathConverter(t *testing.T) {
cases := []struct {
swagger string
denco string
}{
{"/", "/"},
{"/something", "/something"},
{"/{id}", "/:id"},
{"/{id}/something/{anotherId}", "/:id/something/:anotherId"},
{"/{petid}", "/:petid"},
{"/{pet_id}", "/:pet_id"},
{"/{petId}", "/:petId"},
{"/{pet-id}", "/:pet-id"},
}
for _, tc := range cases {
actual := pathConverter.ReplaceAllString(tc.swagger, ":$1")
assert.Equal(t, tc.denco, actual, "expected swagger path %s to match %s but got %s", tc.swagger, tc.denco, actual)
}
}

View File

@@ -0,0 +1,34 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import "net/http"
func newSecureAPI(ctx *Context, next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
route, _ := ctx.RouteInfo(r)
if route != nil && len(route.Authenticators) == 0 {
next.ServeHTTP(rw, r)
return
}
if _, err := ctx.Authorize(r, route); err != nil {
ctx.Respond(rw, r, route.Produces, route, err)
return
}
next.ServeHTTP(rw, r)
})
}

View File

@@ -0,0 +1,58 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/stretchr/testify/assert"
)
func TestSecurityMiddleware(t *testing.T) {
spec, api := petstore.NewAPI(t)
context := NewContext(spec, api, nil)
context.router = DefaultRouter(spec, context.api)
mw := newSecureAPI(context, http.HandlerFunc(terminator))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/api/pets", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 401, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/api/pets", nil)
request.SetBasicAuth("admin", "wrong")
mw.ServeHTTP(recorder, request)
assert.Equal(t, 401, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/api/pets", nil)
request.SetBasicAuth("admin", "admin")
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "//apipets/1", nil)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
}

View File

@@ -0,0 +1,47 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"path"
)
// Spec creates a middleware to serve a swagger spec.
// This allows for altering the spec before starting the http listener.
// This can be useful if you want to serve the swagger spec from another path than /swagger.json
//
func Spec(basePath string, b []byte, next http.Handler) http.Handler {
if basePath == "" {
basePath = "/"
}
pth := path.Join(basePath, "swagger.json")
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == pth {
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
rw.Write(b)
return
}
if next == nil {
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusNotFound)
return
}
next.ServeHTTP(rw, r)
})
}

View File

@@ -0,0 +1,56 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/stretchr/testify/assert"
)
func TestServeSpecMiddleware(t *testing.T) {
spec, api := petstore.NewAPI(t)
ctx := NewContext(spec, api, nil)
handler := Spec("", ctx.spec.Raw(), nil)
// serves spec
request, _ := http.NewRequest("GET", "/swagger.json", nil)
request.Header.Add(runtime.HeaderContentType, runtime.JSONMime)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
// returns 404 when no next handler
request, _ = http.NewRequest("GET", "/api/pets", nil)
request.Header.Add(runtime.HeaderContentType, runtime.JSONMime)
recorder = httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
assert.Equal(t, 404, recorder.Code)
// forwards to next handler for other url
handler = Spec("", ctx.spec.Raw(), http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
request, _ = http.NewRequest("GET", "/api/pets", nil)
request.Header.Add(runtime.HeaderContentType, runtime.JSONMime)
recorder = httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)
}

View File

@@ -0,0 +1,301 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"errors"
"reflect"
"strings"
"testing"
"time"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/stretchr/testify/assert"
)
var evaluatesAsTrue = []string{"true", "1", "yes", "ok", "y", "on", "selected", "checked", "t", "enabled"}
type unmarshallerSlice []string
func (u *unmarshallerSlice) UnmarshalText(data []byte) error {
if len(data) == 0 {
return errors.New("an error")
}
*u = strings.Split(string(data), ",")
return nil
}
type SomeOperationParams struct {
Name string
ID int64
Confirmed bool
Age int
Visits int32
Count int16
Seq int8
UID uint64
UAge uint
UVisits uint32
UCount uint16
USeq uint8
Score float32
Rate float64
Timestamp strfmt.DateTime
Birthdate strfmt.Date
LastFailure *strfmt.DateTime
Unsupported struct{}
Tags []string
Prefs []int32
Categories unmarshallerSlice
}
func FloatParamTest(t *testing.T, fName, pName, format string, val reflect.Value, defVal, expectedDef interface{}, actual func() interface{}) {
fld := val.FieldByName(pName)
binder := &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("number", "double").WithDefault(defVal),
Name: pName,
}
err := binder.setFieldValue(fld, defVal, "5", true)
assert.NoError(t, err)
assert.EqualValues(t, 5, actual())
err = binder.setFieldValue(fld, defVal, "", true)
assert.NoError(t, err)
assert.EqualValues(t, expectedDef, actual())
err = binder.setFieldValue(fld, defVal, "yada", true)
assert.Error(t, err)
}
func IntParamTest(t *testing.T, pName string, val reflect.Value, defVal, expectedDef interface{}, actual func() interface{}) {
fld := val.FieldByName(pName)
binder := &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("integer", "int64").WithDefault(defVal),
Name: pName,
}
err := binder.setFieldValue(fld, defVal, "5", true)
assert.NoError(t, err)
assert.EqualValues(t, 5, actual())
err = binder.setFieldValue(fld, defVal, "", true)
assert.NoError(t, err)
assert.EqualValues(t, expectedDef, actual())
err = binder.setFieldValue(fld, defVal, "yada", true)
assert.Error(t, err)
}
func TestParamBinding(t *testing.T) {
actual := new(SomeOperationParams)
val := reflect.ValueOf(actual).Elem()
pName := "Name"
fld := val.FieldByName(pName)
binder := &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("string", "").WithDefault("some-name"),
Name: pName,
}
err := binder.setFieldValue(fld, "some-name", "the name value", true)
assert.NoError(t, err)
assert.Equal(t, "the name value", actual.Name)
err = binder.setFieldValue(fld, "some-name", "", true)
assert.NoError(t, err)
assert.Equal(t, "some-name", actual.Name)
IntParamTest(t, "ID", val, 1, 1, func() interface{} { return actual.ID })
IntParamTest(t, "ID", val, nil, 0, func() interface{} { return actual.ID })
IntParamTest(t, "Age", val, 1, 1, func() interface{} { return actual.Age })
IntParamTest(t, "Age", val, nil, 0, func() interface{} { return actual.Age })
IntParamTest(t, "Visits", val, 1, 1, func() interface{} { return actual.Visits })
IntParamTest(t, "Visits", val, nil, 0, func() interface{} { return actual.Visits })
IntParamTest(t, "Count", val, 1, 1, func() interface{} { return actual.Count })
IntParamTest(t, "Count", val, nil, 0, func() interface{} { return actual.Count })
IntParamTest(t, "Seq", val, 1, 1, func() interface{} { return actual.Seq })
IntParamTest(t, "Seq", val, nil, 0, func() interface{} { return actual.Seq })
IntParamTest(t, "UID", val, uint64(1), 1, func() interface{} { return actual.UID })
IntParamTest(t, "UID", val, uint64(0), 0, func() interface{} { return actual.UID })
IntParamTest(t, "UAge", val, uint(1), 1, func() interface{} { return actual.UAge })
IntParamTest(t, "UAge", val, nil, 0, func() interface{} { return actual.UAge })
IntParamTest(t, "UVisits", val, uint32(1), 1, func() interface{} { return actual.UVisits })
IntParamTest(t, "UVisits", val, nil, 0, func() interface{} { return actual.UVisits })
IntParamTest(t, "UCount", val, uint16(1), 1, func() interface{} { return actual.UCount })
IntParamTest(t, "UCount", val, nil, 0, func() interface{} { return actual.UCount })
IntParamTest(t, "USeq", val, uint8(1), 1, func() interface{} { return actual.USeq })
IntParamTest(t, "USeq", val, nil, 0, func() interface{} { return actual.USeq })
FloatParamTest(t, "score", "Score", "float", val, 1.0, 1, func() interface{} { return actual.Score })
FloatParamTest(t, "score", "Score", "float", val, nil, 0, func() interface{} { return actual.Score })
FloatParamTest(t, "rate", "Rate", "double", val, 1.0, 1, func() interface{} { return actual.Rate })
FloatParamTest(t, "rate", "Rate", "double", val, nil, 0, func() interface{} { return actual.Rate })
pName = "Confirmed"
confirmedField := val.FieldByName(pName)
binder = &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("boolean", "").WithDefault(true),
Name: pName,
}
for _, tv := range evaluatesAsTrue {
err = binder.setFieldValue(confirmedField, true, tv, true)
assert.NoError(t, err)
assert.True(t, actual.Confirmed)
}
err = binder.setFieldValue(confirmedField, true, "", true)
assert.NoError(t, err)
assert.True(t, actual.Confirmed)
err = binder.setFieldValue(confirmedField, true, "0", true)
assert.NoError(t, err)
assert.False(t, actual.Confirmed)
pName = "Timestamp"
timeField := val.FieldByName(pName)
dt := strfmt.DateTime(time.Date(2014, 3, 19, 2, 9, 0, 0, time.UTC))
binder = &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("string", "date-time").WithDefault(dt),
Name: pName,
}
exp := strfmt.DateTime(time.Date(2014, 5, 14, 2, 9, 0, 0, time.UTC))
err = binder.setFieldValue(timeField, dt, exp.String(), true)
assert.NoError(t, err)
assert.Equal(t, exp, actual.Timestamp)
err = binder.setFieldValue(timeField, dt, "", true)
assert.NoError(t, err)
assert.Equal(t, dt, actual.Timestamp)
err = binder.setFieldValue(timeField, dt, "yada", true)
assert.Error(t, err)
ddt := strfmt.Date(time.Date(2014, 3, 19, 0, 0, 0, 0, time.UTC))
pName = "Birthdate"
dateField := val.FieldByName(pName)
binder = &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("string", "date").WithDefault(ddt),
Name: pName,
}
expd := strfmt.Date(time.Date(2014, 5, 14, 0, 0, 0, 0, time.UTC))
err = binder.setFieldValue(dateField, ddt, expd.String(), true)
assert.NoError(t, err)
assert.Equal(t, expd, actual.Birthdate)
err = binder.setFieldValue(dateField, ddt, "", true)
assert.NoError(t, err)
assert.Equal(t, ddt, actual.Birthdate)
err = binder.setFieldValue(dateField, ddt, "yada", true)
assert.Error(t, err)
dt = strfmt.DateTime(time.Date(2014, 3, 19, 2, 9, 0, 0, time.UTC))
fdt := &dt
pName = "LastFailure"
ftimeField := val.FieldByName(pName)
binder = &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("string", "date").WithDefault(fdt),
Name: pName,
}
exp = strfmt.DateTime(time.Date(2014, 5, 14, 2, 9, 0, 0, time.UTC))
fexp := &exp
err = binder.setFieldValue(ftimeField, fdt, fexp.String(), true)
assert.NoError(t, err)
assert.Equal(t, fexp, actual.LastFailure)
err = binder.setFieldValue(ftimeField, fdt, "", true)
assert.NoError(t, err)
assert.Equal(t, fdt, actual.LastFailure)
err = binder.setFieldValue(ftimeField, fdt, "", true)
assert.NoError(t, err)
assert.Equal(t, fdt, actual.LastFailure)
actual.LastFailure = nil
err = binder.setFieldValue(ftimeField, fdt, "yada", true)
assert.Error(t, err)
assert.Nil(t, actual.LastFailure)
pName = "Unsupported"
unsupportedField := val.FieldByName(pName)
binder = &untypedParamBinder{
parameter: spec.QueryParam(pName).Typed("string", ""),
Name: pName,
}
err = binder.setFieldValue(unsupportedField, nil, "", true)
assert.Error(t, err)
}
func TestSliceConversion(t *testing.T) {
actual := new(SomeOperationParams)
val := reflect.ValueOf(actual).Elem()
// prefsField := val.FieldByName("Prefs")
// cData := "yada,2,3"
// _, _, err := readFormattedSliceFieldValue("Prefs", prefsField, cData, "csv", nil)
// assert.Error(t, err)
sliced := []string{"some", "string", "values"}
seps := map[string]string{"ssv": " ", "tsv": "\t", "pipes": "|", "csv": ",", "": ","}
tagsField := val.FieldByName("Tags")
for k, sep := range seps {
binder := &untypedParamBinder{
Name: "Tags",
parameter: spec.QueryParam("tags").CollectionOf(stringItems, k),
}
actual.Tags = nil
cData := strings.Join(sliced, sep)
tags, _, err := binder.readFormattedSliceFieldValue(cData, tagsField)
assert.NoError(t, err)
assert.Equal(t, sliced, tags)
cData = strings.Join(sliced, " "+sep+" ")
tags, _, err = binder.readFormattedSliceFieldValue(cData, tagsField)
assert.NoError(t, err)
assert.Equal(t, sliced, tags)
tags, _, err = binder.readFormattedSliceFieldValue("", tagsField)
assert.NoError(t, err)
assert.Empty(t, tags)
}
assert.Nil(t, swag.SplitByFormat("yada", "multi"))
assert.Nil(t, swag.SplitByFormat("", ""))
categoriesField := val.FieldByName("Categories")
binder := &untypedParamBinder{
Name: "Categories",
parameter: spec.QueryParam("categories").CollectionOf(stringItems, "csv"),
}
cData := strings.Join(sliced, ",")
categories, custom, err := binder.readFormattedSliceFieldValue(cData, categoriesField)
assert.NoError(t, err)
assert.EqualValues(t, sliced, actual.Categories)
assert.True(t, custom)
assert.Empty(t, categories)
categories, custom, err = binder.readFormattedSliceFieldValue("", categoriesField)
assert.Error(t, err)
assert.True(t, custom)
assert.Empty(t, categories)
}

View File

@@ -0,0 +1,275 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package untyped
import (
"fmt"
"net/http"
"sort"
"strings"
"github.com/go-openapi/analysis"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
)
// NewAPI creates the default untyped API
func NewAPI(spec *loads.Document) *API {
var an *analysis.Spec
if spec != nil && spec.Spec() != nil {
an = analysis.New(spec.Spec())
}
api := &API{
spec: spec,
analyzer: an,
consumers: make(map[string]runtime.Consumer, 10),
producers: make(map[string]runtime.Producer, 10),
authenticators: make(map[string]runtime.Authenticator),
operations: make(map[string]map[string]runtime.OperationHandler),
ServeError: errors.ServeError,
Models: make(map[string]func() interface{}),
formats: strfmt.NewFormats(),
}
return api.WithJSONDefaults()
}
// API represents an untyped mux for a swagger spec
type API struct {
spec *loads.Document
analyzer *analysis.Spec
DefaultProduces string
DefaultConsumes string
consumers map[string]runtime.Consumer
producers map[string]runtime.Producer
authenticators map[string]runtime.Authenticator
operations map[string]map[string]runtime.OperationHandler
ServeError func(http.ResponseWriter, *http.Request, error)
Models map[string]func() interface{}
formats strfmt.Registry
}
// WithJSONDefaults loads the json defaults for this api
func (d *API) WithJSONDefaults() *API {
d.DefaultConsumes = runtime.JSONMime
d.DefaultProduces = runtime.JSONMime
d.consumers[runtime.JSONMime] = runtime.JSONConsumer()
d.producers[runtime.JSONMime] = runtime.JSONProducer()
return d
}
// WithoutJSONDefaults clears the json defaults for this api
func (d *API) WithoutJSONDefaults() *API {
d.DefaultConsumes = ""
d.DefaultProduces = ""
delete(d.consumers, runtime.JSONMime)
delete(d.producers, runtime.JSONMime)
return d
}
// Formats returns the registered string formats
func (d *API) Formats() strfmt.Registry {
if d.formats == nil {
d.formats = strfmt.NewFormats()
}
return d.formats
}
// RegisterFormat registers a custom format validator
func (d *API) RegisterFormat(name string, format strfmt.Format, validator strfmt.Validator) {
if d.formats == nil {
d.formats = strfmt.NewFormats()
}
d.formats.Add(name, format, validator)
}
// RegisterAuth registers an auth handler in this api
func (d *API) RegisterAuth(scheme string, handler runtime.Authenticator) {
if d.authenticators == nil {
d.authenticators = make(map[string]runtime.Authenticator)
}
d.authenticators[scheme] = handler
}
// RegisterConsumer registers a consumer for a media type.
func (d *API) RegisterConsumer(mediaType string, handler runtime.Consumer) {
if d.consumers == nil {
d.consumers = make(map[string]runtime.Consumer, 10)
}
d.consumers[strings.ToLower(mediaType)] = handler
}
// RegisterProducer registers a producer for a media type
func (d *API) RegisterProducer(mediaType string, handler runtime.Producer) {
if d.producers == nil {
d.producers = make(map[string]runtime.Producer, 10)
}
d.producers[strings.ToLower(mediaType)] = handler
}
// RegisterOperation registers an operation handler for an operation name
func (d *API) RegisterOperation(method, path string, handler runtime.OperationHandler) {
if d.operations == nil {
d.operations = make(map[string]map[string]runtime.OperationHandler, 30)
}
um := strings.ToUpper(method)
if b, ok := d.operations[um]; !ok || b == nil {
d.operations[um] = make(map[string]runtime.OperationHandler)
}
d.operations[um][path] = handler
}
// OperationHandlerFor returns the operation handler for the specified id if it can be found
func (d *API) OperationHandlerFor(method, path string) (runtime.OperationHandler, bool) {
if d.operations == nil {
return nil, false
}
if pi, ok := d.operations[strings.ToUpper(method)]; ok {
h, ok := pi[path]
return h, ok
}
return nil, false
}
// ConsumersFor gets the consumers for the specified media types
func (d *API) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer {
result := make(map[string]runtime.Consumer)
for _, mt := range mediaTypes {
if consumer, ok := d.consumers[mt]; ok {
result[mt] = consumer
}
}
return result
}
// ProducersFor gets the producers for the specified media types
func (d *API) ProducersFor(mediaTypes []string) map[string]runtime.Producer {
result := make(map[string]runtime.Producer)
for _, mt := range mediaTypes {
if producer, ok := d.producers[mt]; ok {
result[mt] = producer
}
}
return result
}
// AuthenticatorsFor gets the authenticators for the specified security schemes
func (d *API) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator {
result := make(map[string]runtime.Authenticator)
for k := range schemes {
if a, ok := d.authenticators[k]; ok {
result[k] = a
}
}
return result
}
// Validate validates this API for any missing items
func (d *API) Validate() error {
return d.validate()
}
// validateWith validates the registrations in this API against the provided spec analyzer
func (d *API) validate() error {
var consumes []string
for k := range d.consumers {
consumes = append(consumes, k)
}
var produces []string
for k := range d.producers {
produces = append(produces, k)
}
var authenticators []string
for k := range d.authenticators {
authenticators = append(authenticators, k)
}
var operations []string
for m, v := range d.operations {
for p := range v {
operations = append(operations, fmt.Sprintf("%s %s", strings.ToUpper(m), p))
}
}
var definedAuths []string
for k := range d.spec.Spec().SecurityDefinitions {
definedAuths = append(definedAuths, k)
}
if err := d.verify("consumes", consumes, d.analyzer.RequiredConsumes()); err != nil {
return err
}
if err := d.verify("produces", produces, d.analyzer.RequiredProduces()); err != nil {
return err
}
if err := d.verify("operation", operations, d.analyzer.OperationMethodPaths()); err != nil {
return err
}
requiredAuths := d.analyzer.RequiredSecuritySchemes()
if err := d.verify("auth scheme", authenticators, requiredAuths); err != nil {
return err
}
if err := d.verify("security definitions", definedAuths, requiredAuths); err != nil {
return err
}
return nil
}
func (d *API) verify(name string, registrations []string, expectations []string) error {
sort.Sort(sort.StringSlice(registrations))
sort.Sort(sort.StringSlice(expectations))
expected := map[string]struct{}{}
seen := map[string]struct{}{}
for _, v := range expectations {
expected[v] = struct{}{}
}
var unspecified []string
for _, v := range registrations {
seen[v] = struct{}{}
if _, ok := expected[v]; !ok {
unspecified = append(unspecified, v)
}
}
for k := range seen {
delete(expected, k)
}
var unregistered []string
for k := range expected {
unregistered = append(unregistered, k)
}
sort.Sort(sort.StringSlice(unspecified))
sort.Sort(sort.StringSlice(unregistered))
if len(unregistered) > 0 || len(unspecified) > 0 {
return &errors.APIVerificationFailed{
Section: name,
MissingSpecification: unspecified,
MissingRegistration: unregistered,
}
}
return nil
}

View File

@@ -0,0 +1,276 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package untyped
import (
"io"
"sort"
"testing"
"github.com/go-openapi/analysis"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
swaggerspec "github.com/go-openapi/spec"
"github.com/stretchr/testify/assert"
)
func stubAutenticator() runtime.Authenticator {
return runtime.AuthenticatorFunc(func(_ interface{}) (bool, interface{}, error) { return false, nil, nil })
}
type stubConsumer struct {
}
func (s *stubConsumer) Consume(_ io.Reader, _ interface{}) error {
return nil
}
type stubProducer struct {
}
func (s *stubProducer) Produce(_ io.Writer, _ interface{}) error {
return nil
}
type stubOperationHandler struct {
}
func (s *stubOperationHandler) ParameterModel() interface{} {
return nil
}
func (s *stubOperationHandler) Handle(params interface{}) (interface{}, error) {
return nil, nil
}
func TestUntypedAPIRegistrations(t *testing.T) {
api := NewAPI(new(loads.Document)).WithJSONDefaults()
api.RegisterConsumer("application/yada", new(stubConsumer))
api.RegisterProducer("application/yada-2", new(stubProducer))
api.RegisterOperation("get", "/{someId}", new(stubOperationHandler))
api.RegisterAuth("basic", stubAutenticator())
assert.NotEmpty(t, api.authenticators)
_, ok := api.authenticators["basic"]
assert.True(t, ok)
_, ok = api.consumers["application/yada"]
assert.True(t, ok)
_, ok = api.producers["application/yada-2"]
assert.True(t, ok)
_, ok = api.consumers["application/json"]
assert.True(t, ok)
_, ok = api.producers["application/json"]
assert.True(t, ok)
_, ok = api.operations["GET"]["/{someId}"]
assert.True(t, ok)
h, ok := api.OperationHandlerFor("get", "/{someId}")
assert.True(t, ok)
assert.NotNil(t, h)
_, ok = api.OperationHandlerFor("doesntExist", "/{someId}")
assert.False(t, ok)
}
func TestUntypedAppValidation(t *testing.T) {
invalidSpecStr := `{
"consumes": ["application/json"],
"produces": ["application/json"],
"security": [
{"apiKey":[]}
],
"parameters": {
"format": {
"in": "query",
"name": "format",
"type": "string"
}
},
"paths": {
"/": {
"parameters": [
{
"name": "limit",
"type": "integer",
"format": "int32",
"x-go-name": "Limit"
}
],
"get": {
"consumes": ["application/x-yaml"],
"produces": ["application/x-yaml"],
"security": [
{"basic":[]}
],
"parameters": [
{
"name": "skip",
"type": "integer",
"format": "int32"
}
]
}
}
}
}`
specStr := `{
"consumes": ["application/json"],
"produces": ["application/json"],
"security": [
{"apiKey":[]}
],
"securityDefinitions": {
"basic": { "type": "basic" },
"apiKey": { "type": "apiKey", "in":"header", "name":"X-API-KEY" }
},
"parameters": {
"format": {
"in": "query",
"name": "format",
"type": "string"
}
},
"paths": {
"/": {
"parameters": [
{
"name": "limit",
"type": "integer",
"format": "int32",
"x-go-name": "Limit"
}
],
"get": {
"consumes": ["application/x-yaml"],
"produces": ["application/x-yaml"],
"security": [
{"basic":[]}
],
"parameters": [
{
"name": "skip",
"type": "integer",
"format": "int32"
}
]
}
}
}
}`
validSpec, err := loads.Analyzed([]byte(specStr), "")
assert.NoError(t, err)
assert.NotNil(t, validSpec)
spec, err := loads.Analyzed([]byte(invalidSpecStr), "")
assert.NoError(t, err)
assert.NotNil(t, spec)
analyzed := analysis.New(spec.Spec())
analyzedValid := analysis.New(validSpec.Spec())
cons := analyzed.ConsumesFor(analyzed.AllPaths()["/"].Get)
assert.Len(t, cons, 1)
prods := analyzed.RequiredProduces()
assert.Len(t, prods, 2)
api1 := NewAPI(spec)
err = api1.Validate()
assert.Error(t, err)
assert.Equal(t, "missing [application/x-yaml] consumes registrations", err.Error())
api1.RegisterConsumer("application/x-yaml", new(stubConsumer))
err = api1.validate()
assert.Error(t, err)
assert.Equal(t, "missing [application/x-yaml] produces registrations", err.Error())
api1.RegisterProducer("application/x-yaml", new(stubProducer))
err = api1.validate()
assert.Error(t, err)
assert.Equal(t, "missing [GET /] operation registrations", err.Error())
api1.RegisterOperation("get", "/", new(stubOperationHandler))
err = api1.validate()
assert.Error(t, err)
assert.Equal(t, "missing [apiKey, basic] auth scheme registrations", err.Error())
api1.RegisterAuth("basic", stubAutenticator())
api1.RegisterAuth("apiKey", stubAutenticator())
err = api1.validate()
assert.Error(t, err)
assert.Equal(t, "missing [apiKey, basic] security definitions registrations", err.Error())
api3 := NewAPI(validSpec)
api3.RegisterConsumer("application/x-yaml", new(stubConsumer))
api3.RegisterProducer("application/x-yaml", new(stubProducer))
api3.RegisterOperation("get", "/", new(stubOperationHandler))
api3.RegisterAuth("basic", stubAutenticator())
api3.RegisterAuth("apiKey", stubAutenticator())
err = api3.validate()
assert.NoError(t, err)
api3.RegisterConsumer("application/something", new(stubConsumer))
err = api3.validate()
assert.Error(t, err)
assert.Equal(t, "missing from spec file [application/something] consumes", err.Error())
api2 := NewAPI(spec)
api2.RegisterConsumer("application/something", new(stubConsumer))
err = api2.validate()
assert.Error(t, err)
assert.Equal(t, "missing [application/x-yaml] consumes registrations\nmissing from spec file [application/something] consumes", err.Error())
api2.RegisterConsumer("application/x-yaml", new(stubConsumer))
delete(api2.consumers, "application/something")
api2.RegisterProducer("application/something", new(stubProducer))
err = api2.validate()
assert.Error(t, err)
assert.Equal(t, "missing [application/x-yaml] produces registrations\nmissing from spec file [application/something] produces", err.Error())
delete(api2.producers, "application/something")
api2.RegisterProducer("application/x-yaml", new(stubProducer))
expected := []string{"application/x-yaml"}
sort.Sort(sort.StringSlice(expected))
consumes := analyzed.ConsumesFor(analyzed.AllPaths()["/"].Get)
sort.Sort(sort.StringSlice(consumes))
assert.Equal(t, expected, consumes)
consumers := api1.ConsumersFor(consumes)
assert.Len(t, consumers, 1)
produces := analyzed.ProducesFor(analyzed.AllPaths()["/"].Get)
sort.Sort(sort.StringSlice(produces))
assert.Equal(t, expected, produces)
producers := api1.ProducersFor(produces)
assert.Len(t, producers, 1)
definitions := analyzedValid.SecurityDefinitionsFor(analyzedValid.AllPaths()["/"].Get)
expectedSchemes := map[string]swaggerspec.SecurityScheme{"basic": *swaggerspec.BasicAuth()}
assert.Equal(t, expectedSchemes, definitions)
authenticators := api3.AuthenticatorsFor(definitions)
assert.Len(t, authenticators, 1)
opHandler := runtime.OperationHandlerFunc(func(data interface{}) (interface{}, error) {
return data, nil
})
d, err := opHandler.Handle(1)
assert.NoError(t, err)
assert.Equal(t, 1, d)
authenticator := runtime.AuthenticatorFunc(func(params interface{}) (bool, interface{}, error) {
if str, ok := params.(string); ok {
return ok, str, nil
}
return true, nil, errors.Unauthenticated("authenticator")
})
ok, p, err := authenticator.Authenticate("hello")
assert.True(t, ok)
assert.NoError(t, err)
assert.Equal(t, "hello", p)
}

View File

@@ -0,0 +1,164 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/stretchr/testify/assert"
)
func TestUntypedFormPost(t *testing.T) {
params := parametersForFormUpload()
binder := newUntypedRequestBinder(params, nil, strfmt.Default)
urlStr := "http://localhost:8002/hello"
req, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(`name=the-name&age=32`))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
data := make(map[string]interface{})
assert.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
assert.Equal(t, "the-name", data["name"])
assert.EqualValues(t, 32, data["age"])
req, _ = http.NewRequest("POST", urlStr, bytes.NewBufferString(`name=%3&age=32`))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
data = make(map[string]interface{})
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
}
func TestUntypedFileUpload(t *testing.T) {
binder := paramsForFileUpload()
body := bytes.NewBuffer(nil)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "plain-jane.txt")
assert.NoError(t, err)
part.Write([]byte("the file contents"))
writer.WriteField("name", "the-name")
assert.NoError(t, writer.Close())
urlStr := "http://localhost:8002/hello"
req, _ := http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
data := make(map[string]interface{})
assert.NoError(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
assert.Equal(t, "the-name", data["name"])
assert.NotNil(t, data["file"])
assert.IsType(t, runtime.File{}, data["file"])
file := data["file"].(runtime.File)
assert.NotNil(t, file.Header)
assert.Equal(t, "plain-jane.txt", file.Header.Filename)
bb, err := ioutil.ReadAll(file.Data)
assert.NoError(t, err)
assert.Equal(t, []byte("the file contents"), bb)
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", "application/json")
data = make(map[string]interface{})
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", "application(")
data = make(map[string]interface{})
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
body = bytes.NewBuffer(nil)
writer = multipart.NewWriter(body)
part, err = writer.CreateFormFile("bad-name", "plain-jane.txt")
assert.NoError(t, err)
part.Write([]byte("the file contents"))
writer.WriteField("name", "the-name")
assert.NoError(t, writer.Close())
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
data = make(map[string]interface{})
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
req, _ = http.NewRequest("POST", urlStr, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.MultipartReader()
data = make(map[string]interface{})
assert.Error(t, binder.Bind(req, nil, runtime.JSONConsumer(), &data))
}
func TestUntypedBindingTypesForValid(t *testing.T) {
op2 := parametersForAllTypes("")
binder := newUntypedRequestBinder(op2, nil, strfmt.Default)
confirmed := true
name := "thomas"
friend := map[string]interface{}{"name": "toby", "age": json.Number("32")}
id, age, score, factor := int64(7575), int32(348), float32(5.309), float64(37.403)
requestID := 19394858
tags := []string{"one", "two", "three"}
dt1 := time.Date(2014, 8, 9, 0, 0, 0, 0, time.UTC)
planned := strfmt.Date(dt1)
dt2 := time.Date(2014, 10, 12, 8, 5, 5, 0, time.UTC)
delivered := strfmt.DateTime(dt2)
picture := base64.URLEncoding.EncodeToString([]byte("hello"))
uri, _ := url.Parse("http://localhost:8002/hello/7575")
qs := uri.Query()
qs.Add("name", name)
qs.Add("confirmed", "true")
qs.Add("age", "348")
qs.Add("score", "5.309")
qs.Add("factor", "37.403")
qs.Add("tags", strings.Join(tags, ","))
qs.Add("planned", planned.String())
qs.Add("delivered", delivered.String())
qs.Add("picture", picture)
req, _ := http.NewRequest("POST", uri.String()+"?"+qs.Encode(), bytes.NewBuffer([]byte(`{"name":"toby","age":32}`)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-Id", "19394858")
data := make(map[string]interface{})
err := binder.Bind(req, RouteParams([]RouteParam{{"id", "7575"}}), runtime.JSONConsumer(), &data)
assert.NoError(t, err)
assert.Equal(t, id, data["id"])
assert.Equal(t, name, data["name"])
assert.Equal(t, friend, data["friend"])
assert.EqualValues(t, requestID, data["X-Request-Id"])
assert.Equal(t, tags, data["tags"])
assert.Equal(t, planned, data["planned"])
assert.Equal(t, delivered, data["delivered"])
assert.Equal(t, confirmed, data["confirmed"])
assert.Equal(t, age, data["age"])
assert.Equal(t, factor, data["factor"])
assert.Equal(t, score, data["score"])
pb, _ := base64.URLEncoding.DecodeString(picture)
assert.EqualValues(t, pb, data["picture"].(strfmt.Base64))
}

View File

@@ -0,0 +1,141 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"mime"
"net/http"
"strings"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag"
)
// NewValidation starts a new validation middleware
func newValidation(ctx *Context, next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
matched, _ := ctx.RouteInfo(r)
if matched == nil {
ctx.NotFound(rw, r)
return
}
_, result := ctx.BindAndValidate(r, matched)
if result != nil {
ctx.Respond(rw, r, matched.Produces, matched, result)
return
}
debugLog("no result for %s %s", r.Method, r.URL.EscapedPath())
next.ServeHTTP(rw, r)
})
}
type validation struct {
context *Context
result []error
request *http.Request
route *MatchedRoute
bound map[string]interface{}
}
type untypedBinder map[string]interface{}
func (ub untypedBinder) BindRequest(r *http.Request, route *MatchedRoute, consumer runtime.Consumer) error {
if err := route.Binder.Bind(r, route.Params, consumer, ub); err != nil {
return err
}
return nil
}
// ContentType validates the content type of a request
func validateContentType(allowed []string, actual string) error {
debugLog("validating content type for %q against [%s]", actual, strings.Join(allowed, ", "))
if len(allowed) == 0 {
return nil
}
mt, _, err := mime.ParseMediaType(actual)
if err != nil {
return errors.InvalidContentType(actual, allowed)
}
if swag.ContainsStringsCI(allowed, mt) {
return nil
}
return errors.InvalidContentType(actual, allowed)
}
func validateRequest(ctx *Context, request *http.Request, route *MatchedRoute) *validation {
debugLog("validating request %s %s", request.Method, request.URL.EscapedPath())
validate := &validation{
context: ctx,
request: request,
route: route,
bound: make(map[string]interface{}),
}
validate.contentType()
if len(validate.result) == 0 {
validate.responseFormat()
}
if len(validate.result) == 0 {
validate.parameters()
}
return validate
}
func (v *validation) parameters() {
debugLog("validating request parameters for %s %s", v.request.Method, v.request.URL.EscapedPath())
if result := v.route.Binder.Bind(v.request, v.route.Params, v.route.Consumer, v.bound); result != nil {
if result.Error() == "validation failure list" {
for _, e := range result.(*errors.Validation).Value.([]interface{}) {
v.result = append(v.result, e.(error))
}
return
}
v.result = append(v.result, result)
}
}
func (v *validation) contentType() {
if len(v.result) == 0 && runtime.HasBody(v.request) {
debugLog("validating body content type for %s %s", v.request.Method, v.request.URL.EscapedPath())
ct, _, err := v.context.ContentType(v.request)
if err != nil {
v.result = append(v.result, err)
}
if len(v.result) == 0 {
if err := validateContentType(v.route.Consumes, ct); err != nil {
v.result = append(v.result, err)
}
}
if ct != "" && v.route.Consumer == nil {
cons, ok := v.route.Consumers[ct]
if !ok {
v.result = append(v.result, errors.New(500, "no consumer registered for %s", ct))
} else {
v.route.Consumer = cons
}
}
}
}
func (v *validation) responseFormat() {
if str := v.context.ResponseFormat(v.request, v.route.Produces); str == "" && runtime.HasBody(v.request) {
v.result = append(v.result, errors.InvalidResponseFormat(v.request.Header.Get(runtime.HeaderAccept), v.route.Produces))
}
}

View File

@@ -0,0 +1,130 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package middleware
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/internal/testing/petstore"
"github.com/stretchr/testify/assert"
)
func TestContentTypeValidation(t *testing.T) {
spec, api := petstore.NewAPI(t)
context := NewContext(spec, api, nil)
context.router = DefaultRouter(spec, context.api)
mw := newValidation(context, http.HandlerFunc(terminator))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/api/pets", nil)
request.Header.Add("Accept", "*/*")
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusOK, recorder.Code)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "/api/pets", nil)
request.Header.Add("content-type", "application(")
request.ContentLength = 1
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "/api/pets", nil)
request.Header.Add("Accept", "application/json")
request.Header.Add("content-type", "text/html")
request.ContentLength = 1
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusUnsupportedMediaType, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "/api/pets", nil)
request.Header.Add("Accept", "application/json")
request.Header.Add("content-type", "text/html")
request.TransferEncoding = []string{"chunked"}
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusUnsupportedMediaType, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "/api/pets", nil)
request.Header.Add("Accept", "application/json")
request.Header.Add("content-type", "text/html")
mw.ServeHTTP(recorder, request)
assert.Equal(t, 422, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))
}
func TestResponseFormatValidation(t *testing.T) {
spec, api := petstore.NewAPI(t)
context := NewContext(spec, api, nil)
context.router = DefaultRouter(spec, context.api)
mw := newValidation(context, http.HandlerFunc(terminator))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/api/pets", bytes.NewBuffer([]byte(`name: Dog`)))
request.Header.Set(runtime.HeaderContentType, "application/x-yaml")
request.Header.Set(runtime.HeaderAccept, "application/x-yaml")
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code, recorder.Body.String())
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "/api/pets", bytes.NewBuffer([]byte(`name: Dog`)))
request.Header.Set(runtime.HeaderContentType, "application/x-yaml")
request.Header.Set(runtime.HeaderAccept, "application/sml")
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotAcceptable, recorder.Code)
}
func TestValidateContentType(t *testing.T) {
data := []struct {
hdr string
allowed []string
err *errors.Validation
}{
{"application/json", []string{"application/json"}, nil},
{"application/json", []string{"application/x-yaml", "text/html"}, errors.InvalidContentType("application/json", []string{"application/x-yaml", "text/html"})},
{"text/html; charset=utf-8", []string{"text/html"}, nil},
{"text/html;charset=utf-8", []string{"text/html"}, nil},
{"", []string{"application/json"}, errors.InvalidContentType("", []string{"application/json"})},
{"text/html; charset=utf-8", []string{"application/json"}, errors.InvalidContentType("text/html; charset=utf-8", []string{"application/json"})},
{"application(", []string{"application/json"}, errors.InvalidContentType("application(", []string{"application/json"})},
{"application/json;char*", []string{"application/json"}, errors.InvalidContentType("application/json;char*", []string{"application/json"})},
}
for _, v := range data {
err := validateContentType(v.allowed, v.hdr)
if v.err == nil {
assert.NoError(t, err, "input: %q", v.hdr)
} else {
assert.Error(t, err, "input: %q", v.hdr)
assert.IsType(t, &errors.Validation{}, err, "input: %q", v.hdr)
assert.Equal(t, v.err.Error(), err.Error(), "input: %q", v.hdr)
assert.EqualValues(t, http.StatusUnsupportedMediaType, err.(*errors.Validation).Code())
}
}
}