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

Query, Header Default Value Tag Feature #2699

Draft
wants to merge 30 commits into
base: v2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1054478
add: QueryParser default value tag feature and struct cache for speed
muratmirgun Nov 1, 2023
8181949
Merge branch 'gofiber:master' into master
muratmirgun Nov 1, 2023
80b3a67
:memo: docs: add QueryParser defualt value docs
muratmirgun Nov 1, 2023
0afc148
:adhesive_bandage: lint: reflect type handle exhaustive disable
muratmirgun Nov 1, 2023
016c5a6
:pencil2: typo: empty line and wrong test run comment fix
muratmirgun Nov 1, 2023
4d557ee
move: query default parser main functions to utils
muratmirgun Nov 3, 2023
06a8cf8
:test_tube: test: write basic tests for util/default
muratmirgun Nov 3, 2023
257b399
:memo: docs: add DefaultValueParser config field description
muratmirgun Nov 3, 2023
ca1943e
:hammer: refactor: slice default parser optimization
muratmirgun Nov 3, 2023
b85aecc
:rocket: feat: add pointer slice support for parser and test case update
muratmirgun Nov 6, 2023
ba8eb0d
:test_tube: update header parser test files
muratmirgun Nov 7, 2023
8394b30
:memo: docs: add header parser usage with default values
muratmirgun Nov 7, 2023
662f9d6
lint: delete redundant conversion
muratmirgun Nov 9, 2023
00e4761
test: fix int assign issue on default slice parser
muratmirgun Nov 9, 2023
253b4c9
feat: add default value parser to cookie parser with tests
muratmirgun Nov 28, 2023
8b99f13
fix: codeql security fix
muratmirgun Nov 28, 2023
a869138
security: int value conversion check change for security
muratmirgun Nov 28, 2023
ad89ced
add mutex
efectn Dec 23, 2023
413f42a
fix linter
efectn Dec 23, 2023
26dd708
fix tests
efectn Dec 23, 2023
daaed0d
fix sec
efectn Dec 23, 2023
14f8a80
Merge remote-tracking branch 'upstream/master'
ReneWerner87 Jan 4, 2024
721c284
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 4, 2024
ce98c49
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 4, 2024
a163888
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 4, 2024
96c39d4
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 4, 2024
d1e3487
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 4, 2024
a091b23
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 4, 2024
fc9ed03
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 5, 2024
0102600
Query, Header Default Value Tag Feature #2699
ReneWerner87 Jan 5, 2024
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
6 changes: 6 additions & 0 deletions app.go
Expand Up @@ -144,6 +144,12 @@ type Config struct {
// Default: false
CaseSensitive bool `json:"case_sensitive"`

// When set to true, this will add a default value tag support to all parsers.
// This will allow you to set default values for query, path, and body parameters.
//
// Default: false
DefaultValueParser bool `json:"default_value_parser"`

// When set to true, this relinquishes the 0-allocation promise in certain
// cases in order to access the handler values (e.g. request bodies) in an
// immutable fashion so that these values are available even if you return
Expand Down
51 changes: 32 additions & 19 deletions ctx.go
Expand Up @@ -388,11 +388,9 @@ func (c *Ctx) BodyParser(out interface{}) error {
}

// Parse body accordingly
if strings.HasSuffix(ctype, "json") {
return c.app.config.JSONDecoder(c.Body(), out)
}
if strings.HasPrefix(ctype, MIMEApplicationForm) {
data := make(map[string][]string)
switch {
case strings.HasPrefix(ctype, MIMEApplicationForm):
formBody := make(map[string][]string)
var err error

c.fasthttp.PostArgs().VisitAll(func(key, val []byte) {
Expand All @@ -410,28 +408,37 @@ func (c *Ctx) BodyParser(out interface{}) error {
if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, bodyTag) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
formBody[k] = append(formBody[k], values[i])
}
} else {
data[k] = append(data[k], v)
formBody[k] = append(formBody[k], v)
}
})

return c.parseToStruct(bodyTag, out, data)
}
if strings.HasPrefix(ctype, MIMEMultipartForm) {
data, err := c.fasthttp.MultipartForm()
return c.parseToStruct(bodyTag, out, formBody)
case strings.HasPrefix(ctype, MIMEMultipartForm):
multipartBody, err := c.fasthttp.MultipartForm()
if err != nil {
return err
}
return c.parseToStruct(bodyTag, out, data.Value)
}
if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) {
if err := xml.Unmarshal(c.Body(), out); err != nil {
return fmt.Errorf("failed to unmarshal: %w", err)
return c.parseToStruct(bodyTag, out, multipartBody.Value)
// part for custom parser -> not the schema parser
default:
if c.app.config.DefaultValueParser {
utils.SetDefaultValues(out)
}

switch {
case strings.HasPrefix(ctype, MIMETextXML), strings.HasPrefix(ctype, MIMEApplicationXML):
if err := xml.Unmarshal(c.Body(), out); err != nil {
return fmt.Errorf("failed to unmarshal: %w", err)
}
return nil
case strings.HasSuffix(ctype, "json"):
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
return c.app.config.JSONDecoder(c.Body(), out)
}
return nil
}

// No suitable content type found
return ErrUnprocessableEntity
}
Expand Down Expand Up @@ -1086,7 +1093,10 @@ func (c *Ctx) AllParams() map[string]string {
func (c *Ctx) ParamsParser(out interface{}) error {
params := make(map[string][]string, len(c.route.Params))
for _, param := range c.route.Params {
params[param] = append(params[param], c.Params(param))
value := c.Params(param)
if value != "" {
params[param] = append(params[param], value)
}
}
return c.parseToStruct(paramsTag, out, params)
}
Expand Down Expand Up @@ -1342,7 +1352,10 @@ func (c *Ctx) ReqHeaderParser(out interface{}) error {
return c.parseToStruct(reqHeaderTag, out, data)
}

func (*Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]string) error {
func (c *Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]string) error {
if c.app.config.DefaultValueParser {
utils.SetDefaultValues(out)
}
// Get decoder from pool
schemaDecoder, ok := decoderPoolMap[aliasTag].Get().(*schema.Decoder)
if !ok {
Expand Down
214 changes: 210 additions & 4 deletions ctx_test.go
Expand Up @@ -36,6 +36,10 @@ import (
"github.com/valyala/fasthttp"
)

const (
cookieName = "Joseph"
)

// go test -run Test_Ctx_Accepts
func Test_Ctx_Accepts(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -987,6 +991,181 @@ func Test_Ctx_CookieParser(t *testing.T) {
utils.AssertEqual(t, 0, len(empty.Courses))
}

func Test_Ctx_ParserWithDefaultValues(t *testing.T) {
t.Parallel()
// setup
app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true})

type TestStruct struct {
Name string
Class int
NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" reqHeader:"name2" default:"doe"`
ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" reqHeader:"class2" default:"10"`
}

withValues := func(t *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) {
t.Helper()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
testStruct := new(TestStruct)

utils.AssertEqual(t, nil, actionFn(c, testStruct))
utils.AssertEqual(t, "foo", testStruct.Name)
utils.AssertEqual(t, 111, testStruct.Class)
utils.AssertEqual(t, "bar", testStruct.NameWithDefault)
utils.AssertEqual(t, 222, testStruct.ClassWithDefault)
}
withoutValues := func(t *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) {
t.Helper()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
testStruct := new(TestStruct)

utils.AssertEqual(t, nil, actionFn(c, testStruct))
utils.AssertEqual(t, "", testStruct.Name)
utils.AssertEqual(t, 0, testStruct.Class)
utils.AssertEqual(t, "doe", testStruct.NameWithDefault)
utils.AssertEqual(t, 10, testStruct.ClassWithDefault)
}

t.Run("BodyParser:xml", func(t *testing.T) {
t.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.SetContentType(MIMEApplicationXML)
c.Request().SetBody([]byte(`<TestStruct><Name>foo</Name><Class>111</Class><Name2>bar</Name2><Class2>222</Class2></TestStruct>`))
return c.BodyParser(testStruct)
})
})
t.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.SetContentType(MIMEApplicationXML)
c.Request().SetBody([]byte(`<TestStruct></TestStruct>`))
return c.BodyParser(testStruct)
})
})
})
t.Run("BodyParser:form", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.SetContentType(MIMEApplicationForm)
c.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222`))
return c.BodyParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.SetContentType(MIMEApplicationForm)
c.Request().SetBody([]byte(``))

return c.BodyParser(testStruct)
})
})
})
t.Run("BodyParser:json", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.SetContentType(MIMEApplicationJSON)
c.Request().SetBody([]byte(`{"name":"foo","class":111,"name2":"bar","class2":222}`))
return c.BodyParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.SetContentType(MIMEApplicationJSON)
c.Request().SetBody([]byte(`{}`))

return c.BodyParser(testStruct)
})
})
})
t.Run("BodyParser:multiform", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"class\"\r\n\r\n111\r\n--b\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"class2\"\r\n\r\n222\r\n--b--")
c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`)
c.Request().Header.SetContentLength(len(body))
return c.BodyParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
body := []byte("--b\n\n--b--")
c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`)
c.Request().Header.SetContentLength(len(body))

return c.BodyParser(testStruct)
})
})
})
t.Run("CookieParser", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.Set("Cookie", "name=foo;name2=bar;class=111;class2=222")
return c.CookieParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
return c.CookieParser(testStruct)
})
})
})
t.Run("QueryParser", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().URI().SetQueryString("name=foo&name2=bar&class=111&class2=222")
return c.QueryParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
return c.QueryParser(testStruct)
})
})
})
t.Run("ParamsParser", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.route = &Route{Params: []string{"name", "name2", "class", "class2"}}
c.values = [30]string{"foo", "bar", "111", "222"}
return c.ParamsParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
// no params declared in route
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.route = &Route{Params: []string{}}
c.values = [30]string{}
return c.ParamsParser(testStruct)
})
// no values for route
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.route = &Route{Params: []string{"name", "name2", "class", "class2"}}
c.values = [30]string{"", "", "", ""}
return c.ParamsParser(testStruct)
})
})
})
t.Run("ReqHeaderParser", func(tt *testing.T) {
tt.Run("withValues", func(t *testing.T) {
withValues(t, func(c *Ctx, testStruct *TestStruct) error {
c.Request().Header.Add("name", "foo")
c.Request().Header.Add("name2", "bar")
c.Request().Header.Add("class", "111")
c.Request().Header.Add("class2", "222")
return c.ReqHeaderParser(testStruct)
})
})
tt.Run("withoutValues", func(t *testing.T) {
withoutValues(t, func(c *Ctx, testStruct *TestStruct) error {
return c.ReqHeaderParser(testStruct)
})
})
})
}

// go test -run Test_Ctx_CookieParserUsingTag -v
func Test_Ctx_CookieParserUsingTag(t *testing.T) {
t.Parallel()
Expand All @@ -1002,8 +1181,8 @@ func Test_Ctx_CookieParserUsingTag(t *testing.T) {
Grades []uint8 `cookie:"score"`
}
cookie1 := new(Cook)
cookie1.Name = "Joseph"
utils.AssertEqual(t, "Joseph", cookie1.Name)
cookie1.Name = cookieName
utils.AssertEqual(t, cookieName, cookie1.Name)

c.Request().Header.Set("Cookie", "id=1")
c.Request().Header.Set("Cookie", "name=Joey")
Expand Down Expand Up @@ -1049,7 +1228,7 @@ func Test_Ctx_CookieParser_Schema(t *testing.T) {
Result result `cookie:"result"`
}
res := &resStruct{
Name: "Joseph",
Name: cookieName,
Age: 10,
Result: result{
Maths: 10,
Expand Down Expand Up @@ -1083,7 +1262,7 @@ func Benchmark_Ctx_CookieParser(b *testing.B) {
Grades []uint8 `cookie:"score"`
}
cookie1 := new(Cook)
cookie1.Name = "Joseph"
cookie1.Name = cookieName

c.Request().Header.Set("Cookie", "id=1")
c.Request().Header.Set("Cookie", "name=Joey")
Expand All @@ -1100,6 +1279,33 @@ func Benchmark_Ctx_CookieParser(b *testing.B) {
utils.AssertEqual(b, nil, err)
}

func Benchmark_Ctx_BodyParserJsonWithDefaultValues(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()

app := New(Config{DefaultValueParser: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type TestStruct struct {
Name string
NameWithDefault string `json:"name2" default:"doe"`
NameWithDefaultNoValue string `json:"name3" default:"doe"`
}
c.Request().Header.SetContentType(MIMEApplicationJSON)
c.Request().SetBody([]byte(`{"name":"foo","name2":"bar"}`))

var err error
testStruct := new(TestStruct)
// Run the function b.N times
for i := 0; i < b.N; i++ {
err = c.BodyParser(testStruct)
}
utils.AssertEqual(b, nil, err)
utils.AssertEqual(b, "foo", testStruct.Name)
utils.AssertEqual(b, "bar", testStruct.NameWithDefault)
utils.AssertEqual(b, "doe", testStruct.NameWithDefaultNoValue)
}

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