Skip to content

Commit

Permalink
integrates validation into request binding
Browse files Browse the repository at this point in the history
refactors request binding to create all validations and binders
eagerly
  • Loading branch information
casualjim committed Jan 19, 2015
1 parent 812b132 commit f81ac89
Show file tree
Hide file tree
Showing 19 changed files with 203 additions and 156 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ Currently there is a spec validator tool:
- [ ] generate stub api based on swagger spec
- [ ] generate client from a swagger spec
- [ ] generate spec document based on the code
- [ ] generate "sensible" random data based on swagger spec
- [ ] generate tests based on swagger spec for server
- [ ] generate tests based on swagger spec for client
- [x] Middlewares:
- [x] routing
- [x] validation
- [ ] authorization
- [ ] swagger docs UI
- [ ] swagger editor UI
- [x] Typed JSON Schema implementation
- [x] extended string formats
- uuid, uuid3, uuid4, uuid5
Expand Down
3 changes: 2 additions & 1 deletion assets/assets.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package assets contains the embedded assets like the json schema json doc and the swagger 2.0 schema doc
//go:generate go-bindata -pkg=assets -prefix=../schemas -ignore=.*\.md ../schemas/...
package assets

//go:generate go-bindata -pkg=assets -prefix=../schemas -ignore=.*\.md ../schemas/...
8 changes: 1 addition & 7 deletions examples/2.0/petstore/server/api/petstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/casualjim/go-swagger/middleware"
"github.com/casualjim/go-swagger/spec"
"github.com/casualjim/go-swagger/testing"
"github.com/codegangsta/negroni"
)

// NewPetstore creates a new petstore api handler
Expand All @@ -25,12 +24,7 @@ func NewPetstore() (http.Handler, error) {
api.RegisterOperation("getPetById", getPetByID)

context := middleware.Serve(spec, api)
n := negroni.New()
// TODO: add security middleware
// n.Use(negroni.HandlerFunc(context.SecurityMiddleware()))
n.Use(negroni.HandlerFunc(context.RouterMiddleware()))
n.Use(negroni.HandlerFunc(context.ValidationMiddleware()))
return n, nil
return context.RouterMiddleware(context.ValidationMiddleware(nil)), nil
}

var getAllPets = swagger.OperationHandlerFunc(func(data interface{}) (interface{}, error) {
Expand Down
23 changes: 0 additions & 23 deletions fixtures/jsonschema_suite/singleTest.json

This file was deleted.

8 changes: 4 additions & 4 deletions middleware/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []st
}

// RouterMiddleware creates a new router middleware for this context
func (c *Context) RouterMiddleware() func(http.ResponseWriter, *http.Request, http.HandlerFunc) {
return newRouter(c)
func (c *Context) RouterMiddleware(handler http.Handler) http.Handler {
return newRouter(c, handler)
}

// ValidationMiddleware creates a new validation middleware for this context
func (c *Context) ValidationMiddleware() func(http.ResponseWriter, *http.Request, http.HandlerFunc) {
return newValidation(c)
func (c *Context) ValidationMiddleware(handler http.Handler) http.Handler {
return newValidation(c, handler)
}
8 changes: 4 additions & 4 deletions middleware/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import (
"github.com/gorilla/context"
)

func newRouter(ctx *Context) func(http.ResponseWriter, *http.Request, http.HandlerFunc) {
return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
func newRouter(ctx *Context, next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer context.Clear(r)
// use context to lookup routes
if _, ok := ctx.RouteInfo(r); ok {
next(rw, r)
next.ServeHTTP(rw, r)
return
}

Expand All @@ -22,5 +22,5 @@ func newRouter(ctx *Context) func(http.ResponseWriter, *http.Request, http.Handl
return
}
ctx.Respond(rw, r, ctx.spec.RequiredProduces(), errors.NotFound("path %s was not found", r.URL.Path))
}
})
}
8 changes: 4 additions & 4 deletions middleware/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ func terminator(rw http.ResponseWriter, r *http.Request) {

func TestRouterMiddleware(t *testing.T) {
context := Serve(petstore.NewAPI(t))
mw := newRouter(context)
mw := newRouter(context, http.HandlerFunc(terminator))

recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "http://localhost:8080/api/pets", nil)

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)

recorder = httptest.NewRecorder()
request, _ = http.NewRequest("DELETE", "http://localhost:8080/api/pets", nil)

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusMethodNotAllowed, recorder.Code)
methods := strings.Split(recorder.Header().Get("Allow"), ",")
sort.Sort(sort.StringSlice(methods))
Expand All @@ -37,7 +37,7 @@ func TestRouterMiddleware(t *testing.T) {
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "http://localhost:8080/pets", nil)

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotFound, recorder.Code)

}
18 changes: 6 additions & 12 deletions middleware/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
)

// NewValidation starts a new validation middleware
func newValidation(context *Context) func(http.ResponseWriter, *http.Request, http.HandlerFunc) {
func newValidation(context *Context, next http.Handler) http.Handler {

return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
matched, _ := context.RouteInfo(r)

result := validateRequest(context, r, matched)
Expand All @@ -22,8 +22,8 @@ func newValidation(context *Context) func(http.ResponseWriter, *http.Request, ht
return
}

next(rw, r)
}
next.ServeHTTP(rw, r)
})
}

type validation struct {
Expand All @@ -48,14 +48,8 @@ func validateRequest(context *Context, request *http.Request, route *router.Matc
}

func (v *validation) parameters() {
// TODO: use just one consumer here
binder := &validate.RequestBinder{
Parameters: v.route.Parameters,
Consumer: v.consumer,
}
if err := binder.Bind(v.request, v.route.Params, v.bound); err != nil {
v.result.AddErrors(err)
}
result := v.route.Binder.Bind(v.request, v.route.Params, v.consumer, v.bound)
v.result.AddErrors(result.Errors...)
}

func (v *validation) contentType() {
Expand Down
14 changes: 7 additions & 7 deletions middleware/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ func TestResult(t *testing.T) {

func TestContentTypeValidation(t *testing.T) {
context := Serve(petstore.NewAPI(t))
mw := newValidation(context)
mw := newValidation(context, http.HandlerFunc(terminator))

recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "http://localhost:8080/api/pets", nil)
request.Header.Add("Accept", "*/*")
mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)

recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "http://localhost:8080/api/pets", nil)
request.Header.Add("content-type", "application(")

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 400, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))

Expand All @@ -44,28 +44,28 @@ func TestContentTypeValidation(t *testing.T) {
request.Header.Add("Accept", "*/*")
request.Header.Add("content-type", "text/html")

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 415, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))
}

func TestResponseFormatValidation(t *testing.T) {
context := Serve(petstore.NewAPI(t))
mw := newValidation(context)
mw := newValidation(context, http.HandlerFunc(terminator))

recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "http://localhost:8080/api/pets", nil)
request.Header.Set(httputils.HeaderContentType, "application/json")
request.Header.Set(httputils.HeaderAccept, "application/json")

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, 200, recorder.Code)

recorder = httptest.NewRecorder()
request, _ = http.NewRequest("POST", "http://localhost:8080/api/pets", nil)
request.Header.Set(httputils.HeaderContentType, "application/json")
request.Header.Set(httputils.HeaderAccept, "application/sml")

mw(recorder, request, terminator)
mw.ServeHTTP(recorder, request)
assert.Equal(t, http.StatusNotAcceptable, recorder.Code)
}
6 changes: 5 additions & 1 deletion router/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/casualjim/go-swagger"
"github.com/casualjim/go-swagger/spec"
"github.com/casualjim/go-swagger/validate"
"github.com/naoina/denco"
)

Expand Down Expand Up @@ -58,6 +59,7 @@ type routeEntry struct {
Producers map[string]swagger.Producer
Parameters map[string]spec.Parameter
Handler swagger.OperationHandler
Binder *validate.RequestBinder
}

// MatchedRoute represents the route that was matched in this request
Expand Down Expand Up @@ -103,6 +105,7 @@ func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Oper
if handler, ok := d.api.OperationHandlerFor(operation.ID); ok {
consumes := d.spec.ConsumesFor(operation)
produces := d.spec.ProducesFor(operation)
parameters := d.spec.ParamsFor(method, path)

record := denco.NewRecord(pathConverter.ReplaceAllString(path, ":$1"), &routeEntry{
Operation: operation,
Expand All @@ -111,7 +114,8 @@ func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Oper
Produces: produces,
Consumers: d.api.ConsumersFor(consumes),
Producers: d.api.ProducersFor(produces),
Parameters: d.spec.ParamsFor(method, path),
Parameters: parameters,
Binder: validate.NewRequestBinder(parameters, d.spec.Spec()),
})
d.records[mn] = append(d.records[mn], record)
}
Expand Down
11 changes: 9 additions & 2 deletions spec/expander.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,15 @@ func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error
nrr, _ := NewRef(schema.ID)
var rrr *Ref
if nrr.GetURL() != nil {
rid, _ := NewRef(root.(*Schema).ID)
rrr, _ = rid.Inherits(nrr)
switch root.(type) {
case *Schema:
rid, _ := NewRef(root.(*Schema).ID)
rrr, _ = rid.Inherits(nrr)
case *Swagger:
rid, _ := NewRef(root.(*Swagger).ID)
rrr, _ = rid.Inherits(nrr)
}

}

resolver, err := defaultSchemaLoader(root, rrr, cache)
Expand Down
17 changes: 17 additions & 0 deletions validate/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,25 @@ import (
"github.com/casualjim/go-swagger/util"
)

func newParamBinder(param spec.Parameter, spec *spec.Swagger, formats formats) *paramBinder {
binder := new(paramBinder)
binder.name = param.Name
binder.parameter = &param
binder.formats = formats
if param.In != "body" {
binder.validator = newParamValidator(&param)
} else {
binder.validator = newSchemaValidator(param.Schema, spec, param.Name)
}

return binder
}

type paramBinder struct {
parameter *spec.Parameter
formats formats
name string
validator entityValidator
}

func (p *paramBinder) Type() reflect.Type {
Expand Down Expand Up @@ -148,6 +163,7 @@ func (p *paramBinder) readValue(values interface{}, target reflect.Value) ([]str
}

func (p *paramBinder) Bind(request *http.Request, routeParams swagger.RouteParams, consumer swagger.Consumer, target reflect.Value) error {
// fmt.Println("binding", p.name, "as", p.Type())
switch p.parameter.In {
case "query":
data, custom, err := p.readValue(request.URL.Query(), target)
Expand Down Expand Up @@ -316,6 +332,7 @@ func (p *paramBinder) setFieldValue(target reflect.Value, defaultValue interface
if target.OverflowInt(i) {
return errors.InvalidType(p.name, p.parameter.In, tpe, data)
}

target.SetInt(i)

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
Expand Down
2 changes: 1 addition & 1 deletion validate/parameter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var paramFactories = []paramFactory{
}

func np(param *spec.Parameter) *paramBinder {
return &paramBinder{name: param.Name, parameter: param}
return newParamBinder(*param, new(spec.Swagger), nil)
}

var stringItems = new(spec.Items)
Expand Down
Loading

0 comments on commit f81ac89

Please sign in to comment.