Skip to content

Commit

Permalink
break swagger out, simplify API, writing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jakecoffman committed Feb 25, 2021
1 parent ce1660c commit cefbd42
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 211 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ _testmain.go
*.exe
*.test
*.prof

swagger.json
2 changes: 1 addition & 1 deletion example/widgets/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var Routes = []crud.Spec{{
Tags: tags,
Validate: crud.Validate{
Query: map[string]crud.Field{
"limit": crud.Number(),
"limit": crud.Number().Required().Min(0).Max(25).Description("Records to return"),
},
},
}, {
Expand Down
99 changes: 99 additions & 0 deletions prehandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package crud

import (
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"strconv"
)

// this is where the validation happens!
func preHandler(spec Spec) gin.HandlerFunc {
return func(c *gin.Context) {
val := spec.Validate
if val.Query != nil {
values := c.Request.URL.Query()
for field, schema := range val.Query {
// query values are always strings, so we must try to convert
queryValue := values.Get(field)

// don't try to convert if the field is empty
if queryValue == "" {
if schema.required != nil && *schema.required {
c.AbortWithStatusJSON(400, fmt.Sprintf("Query validation failed for field %v: %v", field, ErrRequired))
}
return
}
var convertedValue interface{}
switch schema.kind {
case KindBoolean:
if queryValue == "true" {
convertedValue = true
} else if queryValue == "false" {
convertedValue = false
} else {
c.AbortWithStatusJSON(400, fmt.Sprintf("Query validation failed for field %v: %v", field, ErrWrongType))
return
}
case KindString:
convertedValue = queryValue
case KindNumber:
var err error
convertedValue, err = strconv.ParseFloat(queryValue, 64)
if err != nil {
c.AbortWithStatusJSON(400, fmt.Sprintf("Query validation failed for field %v: %v", field, ErrWrongType))
return
}
case KindInteger:
var err error
convertedValue, err = strconv.Atoi(queryValue)
if err != nil {
c.AbortWithStatusJSON(400, fmt.Sprintf("Query validation failed for field %v: %v", field, ErrWrongType))
return
}
case KindArray:
// TODO I'm not sure how this works yet
c.AbortWithStatusJSON(http.StatusNotImplemented, "TODO")
return
default:
c.AbortWithStatusJSON(400, fmt.Sprintf("Validation not possible due to kind: %v", schema.kind))
}
if err := schema.Validate(convertedValue); err != nil {
c.AbortWithStatusJSON(400, fmt.Sprintf("Query validation failed for field %v: %v", field, err.Error()))
return
}
}
}

if val.Body != nil {
// TODO this could be an array, basic type, or "null"
var body map[string]interface{}
if err := c.BindJSON(&body); err != nil {
c.AbortWithStatusJSON(400, err.Error())
return
}
for field, schema := range val.Body {
if err := schema.Validate(body[field]); err != nil {
c.AbortWithStatusJSON(400, fmt.Sprintf("Body validation failed for field %v: %v", field, err.Error()))
return
}
}
// TODO perhaps the user passes a struct to bind to instead?
data, _ := json.Marshal(body)
c.Request.Body = ioutil.NopCloser(bytes.NewReader(data))
}

if val.Path != nil {
for field, schema := range val.Path {
path := c.Param(field)
if schema.required != nil && *schema.required && path == "" {
c.AbortWithStatusJSON(400, fmt.Sprintf("Missing path param"))
return
}
}
}
}
}
154 changes: 154 additions & 0 deletions prehandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package crud

import (
"github.com/gin-gonic/gin"
"net/http/httptest"
"testing"
)

func init() {
gin.SetMode(gin.ReleaseMode)
}

func query(query string) (*httptest.ResponseRecorder, *gin.Context) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "http://example.com"+query, nil)
return w, c
}

func TestQueryValidation(t *testing.T) {
tests := []struct {
Schema map[string]Field
Input string
Expected int
}{
{
Schema: map[string]Field{
"testquery": String(),
},
Input: "",
Expected: 200,
}, {
Schema: map[string]Field{
"testquery": String().Required(),
},
Input: "",
Expected: 400,
}, {
Schema: map[string]Field{
"testquery": String().Required(),
},
Input: "?testquery=",
Expected: 400,
}, {
Schema: map[string]Field{
"testquery": String().Required(),
},
Input: "?testquery=ok",
Expected: 200,
}, {
Schema: map[string]Field{
"testquery": Number(),
},
Input: "",
Expected: 200,
}, {
Schema: map[string]Field{
"testquery": Number().Required(),
},
Input: "",
Expected: 400,
},
{
Schema: map[string]Field{
"testquery": Number().Required(),
},
Input: "?testquery=1",
Expected: 200,
},
{
Schema: map[string]Field{
"testquery": Number().Required(),
},
Input: "?testquery=1.1",
Expected: 200,
},
{
Schema: map[string]Field{
"testquery": Number(),
},
Input: "?testquery=a",
Expected: 400,
},
{
Schema: map[string]Field{
"testquery": Boolean(),
},
Input: "?testquery=true",
Expected: 200,
},
{
Schema: map[string]Field{
"testquery": Boolean(),
},
Input: "?testquery=false",
Expected: 200,
},
{
Schema: map[string]Field{
"testquery": Boolean(),
},
Input: "?testquery=1",
Expected: 400,
},
{
Schema: map[string]Field{
"testquery": Integer(),
},
Input: "?testquery=1",
Expected: 200,
},
{
Schema: map[string]Field{
"testquery": Integer().Max(1),
},
Input: "?testquery=2",
Expected: 400,
},
{
Schema: map[string]Field{
"testquery": Integer().Min(5),
},
Input: "?testquery=4",
Expected: 400,
},
{
Schema: map[string]Field{
"testquery": Integer(),
},
Input: "?testquery=1.1",
Expected: 400,
},
{
Schema: map[string]Field{
"testquery": Integer(),
},
Input: "?testquery=a",
Expected: 400,
},
}

for _, test := range tests {
handler := preHandler(Spec{
Validate: Validate{Query: test.Schema},
})

w, c := query(test.Input)
handler(c)

if w.Result().StatusCode != test.Expected {
t.Errorf("expected '%v' got '%v'. input: '%v'. schema: '%v'", test.Expected, w.Code, test.Input, test.Schema)
}
}
}
Loading

0 comments on commit cefbd42

Please sign in to comment.