Explorar el Código

move router to httpx

kevin hace 4 años
padre
commit
b73684d9a6

+ 0 - 869
rest/httpx/requests_test.go

@@ -1,25 +1,15 @@
 package httpx
 
 import (
-	"bytes"
-	"fmt"
-	"io"
 	"net/http"
 	"net/http/httptest"
 	"strconv"
 	"strings"
 	"testing"
 
-	"zero/rest/internal/router"
-
 	"github.com/stretchr/testify/assert"
 )
 
-const (
-	applicationJsonWithUtf8 = "application/json; charset=utf-8"
-	contentLength           = "Content-Length"
-)
-
 func TestParseForm(t *testing.T) {
 	var v struct {
 		Name    string  `form:"name"`
@@ -119,865 +109,6 @@ func TestParseRequired(t *testing.T) {
 	assert.NotNil(t, err)
 }
 
-func TestParseSlice(t *testing.T) {
-	body := `names=%5B%22first%22%2C%22second%22%5D`
-	reader := strings.NewReader(body)
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader)
-	assert.Nil(t, err)
-	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
-	rt := router.NewPatRouter()
-	err = rt.Handle(http.MethodPost, "/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		v := struct {
-			Names []string `form:"names"`
-		}{}
-
-		err = Parse(r, &v)
-		assert.Nil(t, err)
-		assert.Equal(t, 2, len(v.Names))
-		assert.Equal(t, "first", v.Names[0])
-		assert.Equal(t, "second", v.Names[1])
-	}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	rt.ServeHTTP(rr, r)
-}
-
-func TestParseJsonPost(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(
-		w http.ResponseWriter, r *http.Request) {
-		v := struct {
-			Name     string `path:"name"`
-			Year     int    `path:"year"`
-			Nickname string `form:"nickname"`
-			Zipcode  int64  `form:"zipcode"`
-			Location string `json:"location"`
-			Time     int64  `json:"time"`
-		}{}
-
-		err = Parse(r, &v)
-		assert.Nil(t, err)
-		_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d:%s:%d", v.Name, v.Year,
-			v.Nickname, v.Zipcode, v.Location, v.Time))
-		assert.Nil(t, err)
-	}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017:whatever:200000:shanghai:20170912", rr.Body.String())
-}
-
-func TestParseJsonPostWithIntSlice(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
-		bytes.NewBufferString(`{"ages": [1, 2], "years": [3, 4]}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(
-		w http.ResponseWriter, r *http.Request) {
-		v := struct {
-			Name  string  `path:"name"`
-			Year  int     `path:"year"`
-			Ages  []int   `json:"ages"`
-			Years []int64 `json:"years"`
-		}{}
-
-		err = Parse(r, &v)
-		assert.Nil(t, err)
-		assert.ElementsMatch(t, []int{1, 2}, v.Ages)
-		assert.ElementsMatch(t, []int64{3, 4}, v.Years)
-	}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseJsonPostError(t *testing.T) {
-	payload := `[{"abcd": "cdef"}]`
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(payload))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseJsonPostInvalidRequest(t *testing.T) {
-	payload := `{"ages": ["cdef"]}`
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/",
-		bytes.NewBufferString(payload))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Ages []int `json:"ages"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseJsonPostRequired(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
-		bytes.NewBufferString(`{"location": "shanghai"`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParsePath(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name string `path:"name"`
-				Year int    `path:"year"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s in %d", v.Name, v.Year))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin in 2017", rr.Body.String())
-}
-
-func TestParsePathRequired(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name string `path:"name"`
-				Year int    `path:"year"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseQuery(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "whatever:200000", rr.Body.String())
-}
-
-func TestParseQueryRequired(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		v := struct {
-			Nickname string `form:"nickname"`
-			Zipcode  int64  `form:"zipcode"`
-		}{}
-
-		err = Parse(r, &v)
-		assert.NotNil(t, err)
-	}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseOptional(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode,optional"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "whatever:0", rr.Body.String())
-}
-
-func TestParseNestedInRequestEmpty(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017", bytes.NewBufferString("{}"))
-	assert.Nil(t, err)
-
-	type (
-		Request struct {
-			Name string `path:"name"`
-			Year int    `path:"year"`
-		}
-
-		Audio struct {
-			Volume int `json:"volume"`
-		}
-
-		WrappedRequest struct {
-			Request
-			Audio Audio `json:"audio,optional"`
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017", rr.Body.String())
-}
-
-func TestParsePtrInRequest(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
-		bytes.NewBufferString(`{"audio": {"volume": 100}}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	type (
-		Request struct {
-			Name string `path:"name"`
-			Year int    `path:"year"`
-		}
-
-		Audio struct {
-			Volume int `json:"volume"`
-		}
-
-		WrappedRequest struct {
-			Request
-			Audio *Audio `json:"audio,optional"`
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%d", v.Name, v.Year, v.Audio.Volume))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017:100", rr.Body.String())
-}
-
-func TestParsePtrInRequestEmpty(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin", bytes.NewBufferString("{}"))
-	assert.Nil(t, err)
-
-	type (
-		Audio struct {
-			Volume int `json:"volume"`
-		}
-
-		WrappedRequest struct {
-			Audio *Audio `json:"audio,optional"`
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/kevin", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseQueryOptional(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode,optional"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "whatever:0", rr.Body.String())
-}
-
-func TestParse(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d", v.Name, v.Year, v.Nickname, v.Zipcode))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017:whatever:200000", rr.Body.String())
-}
-
-func TestParseWrappedRequest(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
-	assert.Nil(t, err)
-
-	type (
-		Request struct {
-			Name string `path:"name"`
-			Year int    `path:"year"`
-		}
-
-		WrappedRequest struct {
-			Request
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017", rr.Body.String())
-}
-
-func TestParseWrappedGetRequestWithJsonHeader(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, applicationJsonWithUtf8)
-
-	type (
-		Request struct {
-			Name string `path:"name"`
-			Year int    `path:"year"`
-		}
-
-		WrappedRequest struct {
-			Request
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017", rr.Body.String())
-}
-
-func TestParseWrappedHeadRequestWithJsonHeader(t *testing.T) {
-	r, err := http.NewRequest(http.MethodHead, "http://hello.com/kevin/2017", nil)
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, applicationJsonWithUtf8)
-
-	type (
-		Request struct {
-			Name string `path:"name"`
-			Year int    `path:"year"`
-		}
-
-		WrappedRequest struct {
-			Request
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodHead, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017", rr.Body.String())
-}
-
-func TestParseWrappedRequestPtr(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
-	assert.Nil(t, err)
-
-	type (
-		Request struct {
-			Name string `path:"name"`
-			Year int    `path:"year"`
-		}
-
-		WrappedRequest struct {
-			*Request
-		}
-	)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			var v WrappedRequest
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017", rr.Body.String())
-}
-
-func TestParseWithAll(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		v := struct {
-			Name     string `path:"name"`
-			Year     int    `path:"year"`
-			Nickname string `form:"nickname"`
-			Zipcode  int64  `form:"zipcode"`
-			Location string `json:"location"`
-			Time     int64  `json:"time"`
-		}{}
-
-		err = Parse(r, &v)
-		assert.Nil(t, err)
-		_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d:%s:%d", v.Name, v.Year,
-			v.Nickname, v.Zipcode, v.Location, v.Time))
-		assert.Nil(t, err)
-	}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017:whatever:200000:shanghai:20170912", rr.Body.String())
-}
-
-func TestParseWithAllUtf8(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, applicationJsonWithUtf8)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.Nil(t, err)
-			_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d:%s:%d", v.Name, v.Year,
-				v.Nickname, v.Zipcode, v.Location, v.Time))
-			assert.Nil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-
-	assert.Equal(t, "kevin:2017:whatever:200000:shanghai:20170912", rr.Body.String())
-}
-
-func TestParseWithMissingForm(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-			assert.Equal(t, "field zipcode is not set", err.Error())
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseWithMissingAllForms(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseWithMissingJson(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"location": "shanghai"}`))
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotEqual(t, io.EOF, err)
-			assert.NotNil(t, Parse(r, &v))
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseWithMissingAllJsons(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotEqual(t, io.EOF, err)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseWithMissingPath(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-			assert.Equal(t, "field name is not set", err.Error())
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseWithMissingAllPaths(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
-	assert.Nil(t, err)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseGetWithContentLengthHeader(t *testing.T) {
-	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, ApplicationJson)
-	r.Header.Set(contentLength, "1024")
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Location string `json:"location"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseJsonPostWithTypeMismatch(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
-		bytes.NewBufferString(`{"time": "20170912"}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, applicationJsonWithUtf8)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name     string `path:"name"`
-				Year     int    `path:"year"`
-				Nickname string `form:"nickname"`
-				Zipcode  int64  `form:"zipcode"`
-				Time     int64  `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
-func TestParseJsonPostWithInt2String(t *testing.T) {
-	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
-		bytes.NewBufferString(`{"time": 20170912}`))
-	assert.Nil(t, err)
-	r.Header.Set(ContentType, applicationJsonWithUtf8)
-
-	router := router.NewPatRouter()
-	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			v := struct {
-				Name string `path:"name"`
-				Year int    `path:"year"`
-				Time string `json:"time"`
-			}{}
-
-			err = Parse(r, &v)
-			assert.NotNil(t, err)
-		}))
-	assert.Nil(t, err)
-
-	rr := httptest.NewRecorder()
-	router.ServeHTTP(rr, r)
-}
-
 func BenchmarkParseRaw(b *testing.B) {
 	r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", nil)
 	if err != nil {

+ 9 - 0
rest/httpx/router.go

@@ -0,0 +1,9 @@
+package httpx
+
+import "net/http"
+
+type Router interface {
+	http.Handler
+	Handle(method string, path string, handler http.Handler) error
+	SetNotFoundHandler(handler http.Handler)
+}

+ 8 - 1
rest/internal/router/patrouter.go

@@ -1,11 +1,13 @@
 package router
 
 import (
+	"errors"
 	"net/http"
 	"path"
 	"strings"
 
 	"zero/core/search"
+	"zero/rest/httpx"
 	"zero/rest/internal/context"
 )
 
@@ -14,12 +16,17 @@ const (
 	allowMethodSeparator = ", "
 )
 
+var (
+	ErrInvalidMethod = errors.New("not a valid http method")
+	ErrInvalidPath   = errors.New("path must begin with '/'")
+)
+
 type PatRouter struct {
 	trees    map[string]*search.Tree
 	notFound http.Handler
 }
 
-func NewPatRouter() Router {
+func NewPatRouter() httpx.Router {
 	return &PatRouter{
 		trees: make(map[string]*search.Tree),
 	}

+ 871 - 1
rest/internal/router/patrouter_test.go

@@ -1,12 +1,23 @@
 package router
 
 import (
+	"bytes"
+	"fmt"
+	"io"
 	"net/http"
+	"net/http/httptest"
+	"strings"
 	"testing"
 
+	"zero/rest/httpx"
+	"zero/rest/internal/context"
+
 	"github.com/stretchr/testify/assert"
+)
 
-	"zero/rest/internal/context"
+const (
+	applicationJsonWithUtf8 = "application/json; charset=utf-8"
+	contentLength           = "Content-Length"
 )
 
 type mockedResponseWriter struct {
@@ -108,6 +119,865 @@ func TestPatRouter(t *testing.T) {
 	}
 }
 
+func TestParseSlice(t *testing.T) {
+	body := `names=%5B%22first%22%2C%22second%22%5D`
+	reader := strings.NewReader(body)
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader)
+	assert.Nil(t, err)
+	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	rt := NewPatRouter()
+	err = rt.Handle(http.MethodPost, "/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		v := struct {
+			Names []string `form:"names"`
+		}{}
+
+		err = httpx.Parse(r, &v)
+		assert.Nil(t, err)
+		assert.Equal(t, 2, len(v.Names))
+		assert.Equal(t, "first", v.Names[0])
+		assert.Equal(t, "second", v.Names[1])
+	}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	rt.ServeHTTP(rr, r)
+}
+
+func TestParseJsonPost(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(
+		w http.ResponseWriter, r *http.Request) {
+		v := struct {
+			Name     string `path:"name"`
+			Year     int    `path:"year"`
+			Nickname string `form:"nickname"`
+			Zipcode  int64  `form:"zipcode"`
+			Location string `json:"location"`
+			Time     int64  `json:"time"`
+		}{}
+
+		err = httpx.Parse(r, &v)
+		assert.Nil(t, err)
+		_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d:%s:%d", v.Name, v.Year,
+			v.Nickname, v.Zipcode, v.Location, v.Time))
+		assert.Nil(t, err)
+	}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017:whatever:200000:shanghai:20170912", rr.Body.String())
+}
+
+func TestParseJsonPostWithIntSlice(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
+		bytes.NewBufferString(`{"ages": [1, 2], "years": [3, 4]}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(
+		w http.ResponseWriter, r *http.Request) {
+		v := struct {
+			Name  string  `path:"name"`
+			Year  int     `path:"year"`
+			Ages  []int   `json:"ages"`
+			Years []int64 `json:"years"`
+		}{}
+
+		err = httpx.Parse(r, &v)
+		assert.Nil(t, err)
+		assert.ElementsMatch(t, []int{1, 2}, v.Ages)
+		assert.ElementsMatch(t, []int64{3, 4}, v.Years)
+	}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseJsonPostError(t *testing.T) {
+	payload := `[{"abcd": "cdef"}]`
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(payload))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseJsonPostInvalidRequest(t *testing.T) {
+	payload := `{"ages": ["cdef"]}`
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/",
+		bytes.NewBufferString(payload))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Ages []int `json:"ages"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseJsonPostRequired(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
+		bytes.NewBufferString(`{"location": "shanghai"`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParsePath(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name string `path:"name"`
+				Year int    `path:"year"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s in %d", v.Name, v.Year))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin in 2017", rr.Body.String())
+}
+
+func TestParsePathRequired(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name string `path:"name"`
+				Year int    `path:"year"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseQuery(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "whatever:200000", rr.Body.String())
+}
+
+func TestParseQueryRequired(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		v := struct {
+			Nickname string `form:"nickname"`
+			Zipcode  int64  `form:"zipcode"`
+		}{}
+
+		err = httpx.Parse(r, &v)
+		assert.NotNil(t, err)
+	}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseOptional(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode,optional"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "whatever:0", rr.Body.String())
+}
+
+func TestParseNestedInRequestEmpty(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017", bytes.NewBufferString("{}"))
+	assert.Nil(t, err)
+
+	type (
+		Request struct {
+			Name string `path:"name"`
+			Year int    `path:"year"`
+		}
+
+		Audio struct {
+			Volume int `json:"volume"`
+		}
+
+		WrappedRequest struct {
+			Request
+			Audio Audio `json:"audio,optional"`
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017", rr.Body.String())
+}
+
+func TestParsePtrInRequest(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
+		bytes.NewBufferString(`{"audio": {"volume": 100}}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	type (
+		Request struct {
+			Name string `path:"name"`
+			Year int    `path:"year"`
+		}
+
+		Audio struct {
+			Volume int `json:"volume"`
+		}
+
+		WrappedRequest struct {
+			Request
+			Audio *Audio `json:"audio,optional"`
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%d", v.Name, v.Year, v.Audio.Volume))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017:100", rr.Body.String())
+}
+
+func TestParsePtrInRequestEmpty(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin", bytes.NewBufferString("{}"))
+	assert.Nil(t, err)
+
+	type (
+		Audio struct {
+			Volume int `json:"volume"`
+		}
+
+		WrappedRequest struct {
+			Audio *Audio `json:"audio,optional"`
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/kevin", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseQueryOptional(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode,optional"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "whatever:0", rr.Body.String())
+}
+
+func TestParse(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d", v.Name, v.Year, v.Nickname, v.Zipcode))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017:whatever:200000", rr.Body.String())
+}
+
+func TestParseWrappedRequest(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
+	assert.Nil(t, err)
+
+	type (
+		Request struct {
+			Name string `path:"name"`
+			Year int    `path:"year"`
+		}
+
+		WrappedRequest struct {
+			Request
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017", rr.Body.String())
+}
+
+func TestParseWrappedGetRequestWithJsonHeader(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, applicationJsonWithUtf8)
+
+	type (
+		Request struct {
+			Name string `path:"name"`
+			Year int    `path:"year"`
+		}
+
+		WrappedRequest struct {
+			Request
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017", rr.Body.String())
+}
+
+func TestParseWrappedHeadRequestWithJsonHeader(t *testing.T) {
+	r, err := http.NewRequest(http.MethodHead, "http://hello.com/kevin/2017", nil)
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, applicationJsonWithUtf8)
+
+	type (
+		Request struct {
+			Name string `path:"name"`
+			Year int    `path:"year"`
+		}
+
+		WrappedRequest struct {
+			Request
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodHead, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017", rr.Body.String())
+}
+
+func TestParseWrappedRequestPtr(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017", nil)
+	assert.Nil(t, err)
+
+	type (
+		Request struct {
+			Name string `path:"name"`
+			Year int    `path:"year"`
+		}
+
+		WrappedRequest struct {
+			*Request
+		}
+	)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var v WrappedRequest
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Name, v.Year))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017", rr.Body.String())
+}
+
+func TestParseWithAll(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		v := struct {
+			Name     string `path:"name"`
+			Year     int    `path:"year"`
+			Nickname string `form:"nickname"`
+			Zipcode  int64  `form:"zipcode"`
+			Location string `json:"location"`
+			Time     int64  `json:"time"`
+		}{}
+
+		err = httpx.Parse(r, &v)
+		assert.Nil(t, err)
+		_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d:%s:%d", v.Name, v.Year,
+			v.Nickname, v.Zipcode, v.Location, v.Time))
+		assert.Nil(t, err)
+	}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017:whatever:200000:shanghai:20170912", rr.Body.String())
+}
+
+func TestParseWithAllUtf8(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, applicationJsonWithUtf8)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.Nil(t, err)
+			_, err = io.WriteString(w, fmt.Sprintf("%s:%d:%s:%d:%s:%d", v.Name, v.Year,
+				v.Nickname, v.Zipcode, v.Location, v.Time))
+			assert.Nil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+
+	assert.Equal(t, "kevin:2017:whatever:200000:shanghai:20170912", rr.Body.String())
+}
+
+func TestParseWithMissingForm(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+			assert.Equal(t, "field zipcode is not set", err.Error())
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseWithMissingAllForms(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseWithMissingJson(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"location": "shanghai"}`))
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotEqual(t, io.EOF, err)
+			assert.NotNil(t, httpx.Parse(r, &v))
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseWithMissingAllJsons(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotEqual(t, io.EOF, err)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseWithMissingPath(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+			assert.Equal(t, "field name is not set", err.Error())
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseWithMissingAllPaths(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"location": "shanghai", "time": 20170912}`))
+	assert.Nil(t, err)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseGetWithContentLengthHeader(t *testing.T) {
+	r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000", nil)
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, httpx.ApplicationJson)
+	r.Header.Set(contentLength, "1024")
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Location string `json:"location"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseJsonPostWithTypeMismatch(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017?nickname=whatever&zipcode=200000",
+		bytes.NewBufferString(`{"time": "20170912"}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, applicationJsonWithUtf8)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name     string `path:"name"`
+				Year     int    `path:"year"`
+				Nickname string `form:"nickname"`
+				Zipcode  int64  `form:"zipcode"`
+				Time     int64  `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
+func TestParseJsonPostWithInt2String(t *testing.T) {
+	r, err := http.NewRequest(http.MethodPost, "http://hello.com/kevin/2017",
+		bytes.NewBufferString(`{"time": 20170912}`))
+	assert.Nil(t, err)
+	r.Header.Set(httpx.ContentType, applicationJsonWithUtf8)
+
+	router := NewPatRouter()
+	err = router.Handle(http.MethodPost, "/:name/:year", http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			v := struct {
+				Name string `path:"name"`
+				Year int    `path:"year"`
+				Time string `json:"time"`
+			}{}
+
+			err = httpx.Parse(r, &v)
+			assert.NotNil(t, err)
+		}))
+	assert.Nil(t, err)
+
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, r)
+}
+
 func BenchmarkPatRouter(b *testing.B) {
 	b.ReportAllocs()
 

+ 0 - 24
rest/internal/router/router.go

@@ -1,24 +0,0 @@
-package router
-
-import (
-	"errors"
-	"net/http"
-)
-
-var (
-	ErrInvalidMethod = errors.New("not a valid http method")
-	ErrInvalidPath   = errors.New("path must begin with '/'")
-)
-
-type (
-	Route struct {
-		Path    string
-		Handler http.HandlerFunc
-	}
-
-	Router interface {
-		http.Handler
-		Handle(method string, path string, handler http.Handler) error
-		SetNotFoundHandler(handler http.Handler)
-	}
-)

+ 2 - 2
rest/ngin.go

@@ -6,7 +6,7 @@ import (
 
 	"zero/core/logx"
 	"zero/rest/handler"
-	"zero/rest/internal/router"
+	"zero/rest/httpx"
 )
 
 type (
@@ -124,7 +124,7 @@ func WithPriority() RouteOption {
 	}
 }
 
-func WithRouter(router router.Router) RunOption {
+func WithRouter(router httpx.Router) RunOption {
 	return func(server *Server) {
 		server.opts.start = func(srv *engine) error {
 			return srv.StartWithRouter(router)

+ 5 - 4
rest/server.go

@@ -10,6 +10,7 @@ import (
 	"zero/core/load"
 	"zero/core/stat"
 	"zero/rest/handler"
+	"zero/rest/httpx"
 	"zero/rest/internal"
 	"zero/rest/internal/router"
 
@@ -60,7 +61,7 @@ func (s *engine) Start() error {
 	return s.StartWithRouter(router.NewPatRouter())
 }
 
-func (s *engine) StartWithRouter(router router.Router) error {
+func (s *engine) StartWithRouter(router httpx.Router) error {
 	if err := s.bindRoutes(router); err != nil {
 		return err
 	}
@@ -84,7 +85,7 @@ func (s *engine) appendAuthHandler(fr featuredRoutes, chain alice.Chain,
 	return verifier(chain)
 }
 
-func (s *engine) bindFeaturedRoutes(router router.Router, fr featuredRoutes, metrics *stat.Metrics) error {
+func (s *engine) bindFeaturedRoutes(router httpx.Router, fr featuredRoutes, metrics *stat.Metrics) error {
 	verifier, err := s.signatureVerifier(fr.signature)
 	if err != nil {
 		return err
@@ -99,7 +100,7 @@ func (s *engine) bindFeaturedRoutes(router router.Router, fr featuredRoutes, met
 	return nil
 }
 
-func (s *engine) bindRoute(fr featuredRoutes, router router.Router, metrics *stat.Metrics,
+func (s *engine) bindRoute(fr featuredRoutes, router httpx.Router, metrics *stat.Metrics,
 	route Route, verifier func(chain alice.Chain) alice.Chain) error {
 	chain := alice.New(
 		handler.TracingHandler,
@@ -124,7 +125,7 @@ func (s *engine) bindRoute(fr featuredRoutes, router router.Router, metrics *sta
 	return router.Handle(route.Method, route.Path, handle)
 }
 
-func (s *engine) bindRoutes(router router.Router) error {
+func (s *engine) bindRoutes(router httpx.Router) error {
 	metrics := s.createMetrics()
 
 	for _, fr := range s.routes {