Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 Feature: Add ReqHeaderParser. #1721

Merged
merged 4 commits into from Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 57 additions & 21 deletions ctx.go
Expand Up @@ -32,7 +32,12 @@ import (
// maxParams defines the maximum number of parameters per route.
const maxParams = 30

const queryTag = "query"
// Some constants for BodyParser, QueryParser and ReqHeaderParser.
const (
queryTag = "query"
reqHeaderTag = "reqHeader"
bodyTag = "form"
)

// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx
const userContextKey = "__local_user_context__"
Expand Down Expand Up @@ -285,7 +290,7 @@ func (c *Ctx) Body() []byte {
return body
}

// decoderPool helps to improve BodyParser's and QueryParser's performance
// decoderPool helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance
var decoderPool = &sync.Pool{New: func() interface{} {
return decoderBuilder(ParserConfig{
IgnoreUnknownKeys: true,
Expand Down Expand Up @@ -318,38 +323,42 @@ func decoderBuilder(parserConfig ParserConfig) interface{} {
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
// If none of the content types above are matched, it will return a ErrUnprocessableEntity error
func (c *Ctx) BodyParser(out interface{}) error {
// Get decoder from pool
schemaDecoder := decoderPool.Get().(*schema.Decoder)
defer decoderPool.Put(schemaDecoder)

// Get content-type
ctype := utils.ToLower(utils.UnsafeString(c.fasthttp.Request.Header.ContentType()))

ctype = utils.ParseVendorSpecificContentType(ctype)

// Parse body accordingly
if strings.HasPrefix(ctype, MIMEApplicationJSON) {
schemaDecoder.SetAliasTag("json")
return c.app.config.JSONDecoder(c.Body(), out)
}
if strings.HasPrefix(ctype, MIMEApplicationForm) {
schemaDecoder.SetAliasTag("form")
data := make(map[string][]string)
c.fasthttp.PostArgs().VisitAll(func(key []byte, val []byte) {
data[utils.UnsafeString(key)] = append(data[utils.UnsafeString(key)], utils.UnsafeString(val))
c.fasthttp.PostArgs().VisitAll(func(key, val []byte) {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)

if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}

})
return schemaDecoder.Decode(out, data)

return c.parseToStruct(bodyTag, out, data)
}
if strings.HasPrefix(ctype, MIMEMultipartForm) {
schemaDecoder.SetAliasTag("form")
data, err := c.fasthttp.MultipartForm()
if err != nil {
return err
}
return schemaDecoder.Decode(out, data.Value)
return c.parseToStruct(bodyTag, out, data.Value)
}
if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) {
schemaDecoder.SetAliasTag("xml")
return xml.Unmarshal(c.Body(), out)
}
// No suitable content type found
Expand Down Expand Up @@ -877,17 +886,32 @@ func (c *Ctx) Query(key string, defaultValue ...string) string {

// QueryParser binds the query string to a struct.
func (c *Ctx) QueryParser(out interface{}) error {
// Get decoder from pool
decoder := decoderPool.Get().(*schema.Decoder)
defer decoderPool.Put(decoder)
data := make(map[string][]string)
c.fasthttp.QueryArgs().VisitAll(func(key, val []byte) {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)

if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}

// Set correct alias tag
decoder.SetAliasTag(queryTag)
})

return c.parseToStruct(queryTag, out, data)
}

// ReqHeaderParser binds the request header strings to a struct.
func (c *Ctx) ReqHeaderParser(out interface{}) error {
data := make(map[string][]string)
efectn marked this conversation as resolved.
Show resolved Hide resolved
c.fasthttp.QueryArgs().VisitAll(func(key []byte, val []byte) {
c.fasthttp.Request.Header.VisitAll(func(key, val []byte) {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)

if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
Expand All @@ -896,9 +920,21 @@ func (c *Ctx) QueryParser(out interface{}) error {
} else {
data[k] = append(data[k], v)
}

})

return decoder.Decode(out, data)
return c.parseToStruct(reqHeaderTag, out, data)
}

func (c *Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]string) error {
// Get decoder from pool
schemaDecoder := decoderPool.Get().(*schema.Decoder)
defer decoderPool.Put(schemaDecoder)

// Set alias tag
schemaDecoder.SetAliasTag(aliasTag)

return schemaDecoder.Decode(out, data)
}

func equalFieldType(out interface{}, kind reflect.Kind, key string) bool {
Expand Down
240 changes: 240 additions & 0 deletions ctx_test.go
Expand Up @@ -2579,6 +2579,220 @@ func Test_Ctx_QueryParser_Schema(t *testing.T) {
utils.AssertEqual(t, 0, n.Next.Value)
}

// go test -run Test_Ctx_ReqHeaderParser -v
func Test_Ctx_ReqHeaderParser(t *testing.T) {
t.Parallel()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Header struct {
ID int
Name string
Hobby []string
}
c.Request().SetBody([]byte(``))
c.Request().Header.SetContentType("")

c.Request().Header.Add("id", "1")
c.Request().Header.Add("Name", "John Doe")
c.Request().Header.Add("Hobby", "golang,fiber")
q := new(Header)
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))
utils.AssertEqual(t, 2, len(q.Hobby))

c.Request().Header.Del("hobby")
c.Request().Header.Add("Hobby", "golang,fiber,go")
q = new(Header)
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))
utils.AssertEqual(t, 3, len(q.Hobby))

empty := new(Header)
c.Request().Header.Del("hobby")
utils.AssertEqual(t, nil, c.QueryParser(empty))
utils.AssertEqual(t, 0, len(empty.Hobby))

type Header2 struct {
Bool bool
ID int
Name string
Hobby string
FavouriteDrinks []string
Empty []string
Alloc []string
No []int64
}

c.Request().Header.Add("id", "2")
c.Request().Header.Add("Name", "Jane Doe")
c.Request().Header.Del("hobby")
c.Request().Header.Add("Hobby", "go,fiber")
c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi")
c.Request().Header.Add("alloc", "")
c.Request().Header.Add("no", "1")

h2 := new(Header2)
h2.Bool = true
h2.Name = "hello world"
utils.AssertEqual(t, nil, c.ReqHeaderParser(h2))
utils.AssertEqual(t, "go,fiber", h2.Hobby)
utils.AssertEqual(t, true, h2.Bool)
utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten
utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks)
var nilSlice []string
utils.AssertEqual(t, nilSlice, h2.Empty)
utils.AssertEqual(t, []string{""}, h2.Alloc)
utils.AssertEqual(t, []int64{1}, h2.No)

type RequiredHeader struct {
Name string `reqHeader:"name,required"`
}
rh := new(RequiredHeader)
c.Request().Header.Del("name")
utils.AssertEqual(t, "name is empty", c.ReqHeaderParser(rh).Error())
}

// go test -run Test_Ctx_ReqHeaderParser_WithSetParserDecoder -v
func Test_Ctx_ReqHeaderParser_WithSetParserDecoder(t *testing.T) {
type NonRFCTime time.Time

NonRFCConverter := func(value string) reflect.Value {
if v, err := time.Parse("2006-01-02", value); err == nil {
return reflect.ValueOf(v)
}
return reflect.Value{}
}

nonRFCTime := ParserType{
Customtype: NonRFCTime{},
Converter: NonRFCConverter,
}

SetParserDecoder(ParserConfig{
IgnoreUnknownKeys: true,
ParserType: []ParserType{nonRFCTime},
ZeroEmpty: true,
SetAliasTag: "req",
})

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)

type NonRFCTimeInput struct {
Date NonRFCTime `req:"date"`
Title string `req:"title"`
Body string `req:"body"`
}

c.Request().SetBody([]byte(``))
c.Request().Header.SetContentType("")
r := new(NonRFCTimeInput)

c.Request().Header.Add("Date", "2021-04-10")
c.Request().Header.Add("Title", "CustomDateTest")
c.Request().Header.Add("Body", "October")

utils.AssertEqual(t, nil, c.ReqHeaderParser(r))
fmt.Println(r.Date, "q.Date")
utils.AssertEqual(t, "CustomDateTest", r.Title)
date := fmt.Sprintf("%v", r.Date)
utils.AssertEqual(t, "{0 63753609600 <nil>}", date)
utils.AssertEqual(t, "October", r.Body)

c.Request().Header.Add("Title", "")
r = &NonRFCTimeInput{
Title: "Existing title",
Body: "Existing Body",
}
utils.AssertEqual(t, nil, c.ReqHeaderParser(r))
utils.AssertEqual(t, "", r.Title)
}

// go test -run Test_Ctx_ReqHeaderParser_Schema -v
func Test_Ctx_ReqHeaderParser_Schema(t *testing.T) {
t.Parallel()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Header1 struct {
Name string `reqHeader:"Name,required"`
Nested struct {
Age int `reqHeader:"Age"`
} `reqHeader:"Nested,required"`
}
c.Request().SetBody([]byte(``))
c.Request().Header.SetContentType("")

c.Request().Header.Add("Name", "tom")
c.Request().Header.Add("Nested.Age", "10")
q := new(Header1)
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))

c.Request().Header.Del("Name")
q = new(Header1)
utils.AssertEqual(t, "Name is empty", c.ReqHeaderParser(q).Error())

c.Request().Header.Add("Name", "tom")
c.Request().Header.Del("Nested.Age")
c.Request().Header.Add("Nested.Agex", "10")
q = new(Header1)
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))

c.Request().Header.Del("Nested.Agex")
q = new(Header1)
utils.AssertEqual(t, "Nested is empty", c.ReqHeaderParser(q).Error())

c.Request().Header.Del("Nested.Agex")
c.Request().Header.Del("Name")

type Header2 struct {
Name string `reqHeader:"Name"`
Nested struct {
Age int `reqHeader:"age,required"`
} `reqHeader:"Nested"`
}

c.Request().Header.Add("Name", "tom")
c.Request().Header.Add("Nested.Age", "10")

h2 := new(Header2)
utils.AssertEqual(t, nil, c.ReqHeaderParser(h2))

c.Request().Header.Del("Name")
h2 = new(Header2)
utils.AssertEqual(t, nil, c.ReqHeaderParser(h2))

c.Request().Header.Del("Name")
c.Request().Header.Del("Nested.Age")
c.Request().Header.Add("Nested.Agex", "10")
h2 = new(Header2)
utils.AssertEqual(t, "Nested.age is empty", c.ReqHeaderParser(h2).Error())

type Node struct {
Value int `reqHeader:"Val,required"`
Next *Node `reqHeader:"Next,required"`
}
c.Request().Header.Add("Val", "1")
c.Request().Header.Add("Next.Val", "3")
n := new(Node)
utils.AssertEqual(t, nil, c.ReqHeaderParser(n))
utils.AssertEqual(t, 1, n.Value)
utils.AssertEqual(t, 3, n.Next.Value)

c.Request().Header.Del("Val")
n = new(Node)
utils.AssertEqual(t, "Val is empty", c.ReqHeaderParser(n).Error())

c.Request().Header.Add("Val", "3")
c.Request().Header.Del("Next.Val")
c.Request().Header.Add("Next.Value", "2")
n = new(Node)
n.Next = new(Node)
utils.AssertEqual(t, nil, c.ReqHeaderParser(n))
utils.AssertEqual(t, 3, n.Value)
utils.AssertEqual(t, 0, n.Next.Value)
}

func Test_Ctx_EqualFieldType(t *testing.T) {
var out int
utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key"))
Expand Down Expand Up @@ -2647,6 +2861,32 @@ func Benchmark_Ctx_QueryParser_Comma(b *testing.B) {
utils.AssertEqual(b, nil, c.QueryParser(q))
}

// go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParser -benchmem -count=4
func Benchmark_Ctx_ReqHeaderParser(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type ReqHeader struct {
ID int
Name string
Hobby []string
}
c.Request().SetBody([]byte(``))
c.Request().Header.SetContentType("")

c.Request().Header.Add("id", "1")
c.Request().Header.Add("Name", "John Doe")
c.Request().Header.Add("Hobby", "golang,fiber")

q := new(ReqHeader)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
c.ReqHeaderParser(q)
}
utils.AssertEqual(b, nil, c.ReqHeaderParser(q))
}

// go test -run Test_Ctx_BodyStreamWriter
func Test_Ctx_BodyStreamWriter(t *testing.T) {
t.Parallel()
Expand Down