From 1054478e5bec58597123e28107f832ed26039327 Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Wed, 1 Nov 2023 22:38:38 +0300 Subject: [PATCH 01/28] add: QueryParser default value tag feature and struct cache for speed Signed-off-by: Murat Mirgun Ercan --- ctx.go | 75 +++++++++++++++++++++++++ ctx_test.go | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) diff --git a/ctx.go b/ctx.go index 34d49a9b118..953f25371bf 100644 --- a/ctx.go +++ b/ctx.go @@ -1278,6 +1278,8 @@ func (c *Ctx) QueryParser(out interface{}) error { return err } + c.setDefaultValues(out) + return c.parseToStruct(queryTag, out, data) } @@ -1344,6 +1346,79 @@ func (*Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]st return nil } +func tagHandlers(field reflect.Value, tagValue string) { + switch field.Kind() { + case reflect.String: + if field.String() == "" { + field.SetString(tagValue) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if field.Int() == 0 { + if i, err := strconv.ParseInt(tagValue, 10, 64); err == nil { + field.SetInt(i) + } + } + case reflect.Float32, reflect.Float64: + if field.Float() == 0.0 { + if f, err := strconv.ParseFloat(tagValue, 64); err == nil { + field.SetFloat(f) + } + } + case reflect.Bool: + if !field.Bool() { + if b, err := strconv.ParseBool(tagValue); err == nil { + field.SetBool(b) + } + } + case reflect.Slice: + setDefaultForSlice(field, tagValue, field.Type().Elem().Kind()) + } +} + +func setDefaultForSlice(field reflect.Value, tagValue string, kind reflect.Kind) { + items := strings.Split(tagValue, ",") + for _, item := range items { + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if i, err := strconv.ParseInt(item, 10, 64); err == nil { + field.Set(reflect.Append(field, reflect.ValueOf(int(i)))) + } + case reflect.String: + field.Set(reflect.Append(field, reflect.ValueOf(item))) + } + } +} + +var structCache = make(map[reflect.Type][]reflect.StructField) + +func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { + if fields, ok := structCache[t]; ok { + return fields + } + + var fields []reflect.StructField + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if _, ok := field.Tag.Lookup("default"); ok { + fields = append(fields, field) + } + } + structCache[t] = fields + return fields +} + +func (c *Ctx) setDefaultValues(out interface{}) { + val := reflect.ValueOf(out).Elem() + typ := val.Type() + + fields := getFieldsWithDefaultTag(typ) + for _, fieldInfo := range fields { + field := val.FieldByName(fieldInfo.Name) + tagValue := fieldInfo.Tag.Get("default") + tagHandlers(field, tagValue) + } +} + func equalFieldType(out interface{}, kind reflect.Kind, key, tag string) bool { // Get type of interface outTyp := reflect.TypeOf(out).Elem() diff --git a/ctx_test.go b/ctx_test.go index ef9007293f6..cd512e85893 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4441,6 +4441,104 @@ func Test_Ctx_QueryParserUsingTag(t *testing.T) { utils.AssertEqual(t, 2, len(aq.Data)) } +// go test -run Test_Ctx_QueryParserWithDefaultValues -v +func Test_Ctx_QueryParserWithDefaultValues(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + IntValue int `query:"intValue" default:"10"` + IntSlice []int `query:"intSlice"` + StringValue string `query:"stringValue" default:"defaultStr"` + StringSlice []string `query:"stringSlice"` + BoolValue bool `query:"boolValue" default:"true"` + BoolSlice []bool `query:"boolSlice"` + FloatValue float64 `query:"floatValue" default:"3.14"` + FloatSlice []float64 `query:"floatSlice"` + } + + // Test with multiple values + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("intValue=20&intSlice=1,2,3&stringValue=test&stringSlice=a,b,c&boolValue=false&boolSlice=true,false,true&floatValue=6.28&floatSlice=1.1,2.2,3.3") + cq := new(Query) + utils.AssertEqual(t, nil, c.QueryParser(cq)) + utils.AssertEqual(t, 20, cq.IntValue) + utils.AssertEqual(t, 3, len(cq.IntSlice)) + utils.AssertEqual(t, "test", cq.StringValue) + utils.AssertEqual(t, 3, len(cq.StringSlice)) + utils.AssertEqual(t, false, cq.BoolValue) + utils.AssertEqual(t, 3, len(cq.BoolSlice)) + utils.AssertEqual(t, 6.28, cq.FloatValue) + utils.AssertEqual(t, 3, len(cq.FloatSlice)) + + // Test with missing values (should use defaults) + c.Request().URI().SetQueryString("intSlice=4,5,6&stringSlice=d,e,f&boolSlice=false,true,false&floatSlice=4.4,5.5,6.6") + cq = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(cq)) + utils.AssertEqual(t, 10, cq.IntValue) // default value + utils.AssertEqual(t, 3, len(cq.IntSlice)) + utils.AssertEqual(t, "defaultStr", cq.StringValue) // default value + utils.AssertEqual(t, 3, len(cq.StringSlice)) + utils.AssertEqual(t, true, cq.BoolValue) // default value + utils.AssertEqual(t, 3, len(cq.BoolSlice)) + utils.AssertEqual(t, 3.14, cq.FloatValue) // default value + utils.AssertEqual(t, 3, len(cq.FloatSlice)) + + // Test with empty query string + empty := new(Query) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, nil, c.QueryParser(empty)) + utils.AssertEqual(t, 10, empty.IntValue) // default value + utils.AssertEqual(t, "defaultStr", empty.StringValue) // default value + utils.AssertEqual(t, true, empty.BoolValue) // default value + utils.AssertEqual(t, 3.14, empty.FloatValue) // default value + utils.AssertEqual(t, 0, len(empty.IntSlice)) + utils.AssertEqual(t, 0, len(empty.StringSlice)) + utils.AssertEqual(t, 0, len(empty.BoolSlice)) + utils.AssertEqual(t, 0, len(empty.FloatSlice)) + type Query2 struct { + Bool bool `query:"bool"` + ID int `query:"id"` + Name string `query:"name"` + Hobby string `query:"hobby"` + FavouriteDrinks []string `query:"favouriteDrinks"` + Empty []string `query:"empty"` + Alloc []string `query:"alloc"` + No []int64 `query:"no"` + } + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") + q2 := new(Query2) + q2.Bool = true + q2.Name = "hello world 2" + utils.AssertEqual(t, nil, c.QueryParser(q2)) + utils.AssertEqual(t, "basketball,football", q2.Hobby) + utils.AssertEqual(t, true, q2.Bool) + utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, q2.Empty) + utils.AssertEqual(t, []string{""}, q2.Alloc) + utils.AssertEqual(t, []int64{1}, q2.No) + + type RequiredQuery struct { + Name string `query:"name,required"` + } + rq := new(RequiredQuery) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, "failed to decode: name is empty", c.QueryParser(rq).Error()) + + type ArrayQuery struct { + Data []string + } + aq := new(ArrayQuery) + c.Request().URI().SetQueryString("data[]=john&data[]=doe") + utils.AssertEqual(t, nil, c.QueryParser(aq)) + utils.AssertEqual(t, 2, len(aq.Data)) +} + // go test -run Test_Ctx_QueryParser -v func Test_Ctx_QueryParser_WithoutSplitting(t *testing.T) { t.Parallel() @@ -5061,6 +5159,62 @@ func Benchmark_Ctx_QueryParser(b *testing.B) { utils.AssertEqual(b, nil, c.QueryParser(q)) } +// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser -benchmem -count=4 +func Benchmark_Ctx_QueryParserWithDefaultValues(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + // Query struct with 2 default values + type Query struct { + ID int `default:"4"` + Name string `default:"john"` + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("hobby=basketball&hobby=football") + q := new(Query) + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.QueryParser(q) + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, c.QueryParser(q)) + +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValuesForSlices -benchmem -count=4 +func Benchmark_Ctx_QueryParserWithDefaultValuesForSlices(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + // Query struct slices with default values + type Query2 struct { + ID int + Name string + Hobby []int `default:"1,2,3"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("hobby=1&hobby=2") + q2 := new(Query2) + b.ReportAllocs() + b.ResetTimer() + + var err2 error + for n := 0; n < b.N; n++ { + err2 = c.QueryParser(q2) + } + utils.AssertEqual(b, nil, err2) + utils.AssertEqual(b, nil, c.QueryParser(q2)) +} + // go test -v -run=^$ -bench=Benchmark_Ctx_parseQuery -benchmem -count=4 func Benchmark_Ctx_parseQuery(b *testing.B) { app := New() From 80b3a67d1a3c53aac6fee84e2e5f16aecc516172 Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Wed, 1 Nov 2023 22:50:41 +0300 Subject: [PATCH 02/28] :memo: docs: add QueryParser defualt value docs Signed-off-by: Murat Mirgun Ercan --- docs/api/ctx.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 1729438d7f3..82e89515912 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -1351,6 +1351,36 @@ app.Get("/", func(c *fiber.Ctx) error { // curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" ``` +### Default Values with QueryParser +You can also assign default values to struct fields if the query parameter is not provided in the request. To do this, use the default struct tag alongside the query tag. + +```go title="WithDefaultValues" +type PersonWithDefaults struct { + Name string `query:"name" default:"DefaultName"` + Pass string `query:"pass" default:"DefaultPass"` + Products []string `query:"products" default:"defaultProduct1,defaultProduct2"` +} + +app.Get("/defaults", func(c *fiber.Ctx) error { + p := new(PersonWithDefaults) + + if err := c.QueryParser(p); err != nil { + return err + } + + log.Println(p.Name) // Will print "DefaultName" if name is not provided in the query + log.Println(p.Pass) // Will print "DefaultPass" if pass is not provided in the query + log.Println(p.Products) // Will print [defaultProduct1, defaultProduct2] if products is not provided in the query + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/defaults" +// This will use the default values since no query parameters are provided + +``` + ## Range A struct containing the type and a slice of ranges will be returned. From 0afc148143d257b4b9aaeb780acc82a78025d0f2 Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Wed, 1 Nov 2023 22:59:00 +0300 Subject: [PATCH 03/28] :adhesive_bandage: lint: reflect type handle exhaustive disable Signed-off-by: Murat Mirgun Ercan On branch master --- ctx.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ctx.go b/ctx.go index 953f25371bf..2be304d2214 100644 --- a/ctx.go +++ b/ctx.go @@ -1347,6 +1347,7 @@ func (*Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]st } func tagHandlers(field reflect.Value, tagValue string) { + //nolint:exhaustive // We don't need to handle all types switch field.Kind() { case reflect.String: if field.String() == "" { From 016c5a6ef15282cf6fe675057cd075ffb429ed9c Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Wed, 1 Nov 2023 23:26:59 +0300 Subject: [PATCH 04/28] :pencil2: typo: empty line and wrong test run comment fix Signed-off-by: Murat Mirgun Ercan --- ctx.go | 5 +++-- ctx_test.go | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ctx.go b/ctx.go index 2be304d2214..35ddf19bc41 100644 --- a/ctx.go +++ b/ctx.go @@ -1278,7 +1278,7 @@ func (c *Ctx) QueryParser(out interface{}) error { return err } - c.setDefaultValues(out) + setDefaultValues(out) return c.parseToStruct(queryTag, out, data) } @@ -1379,6 +1379,7 @@ func tagHandlers(field reflect.Value, tagValue string) { func setDefaultForSlice(field reflect.Value, tagValue string, kind reflect.Kind) { items := strings.Split(tagValue, ",") for _, item := range items { + //nolint:exhaustive // We don't need to handle all types switch kind { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if i, err := strconv.ParseInt(item, 10, 64); err == nil { @@ -1408,7 +1409,7 @@ func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { return fields } -func (c *Ctx) setDefaultValues(out interface{}) { +func setDefaultValues(out interface{}) { val := reflect.ValueOf(out).Elem() typ := val.Type() diff --git a/ctx_test.go b/ctx_test.go index cd512e85893..9784cdd88e4 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -5159,7 +5159,7 @@ func Benchmark_Ctx_QueryParser(b *testing.B) { utils.AssertEqual(b, nil, c.QueryParser(q)) } -// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser -benchmem -count=4 +// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValues -benchmem -count=4 func Benchmark_Ctx_QueryParserWithDefaultValues(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -5185,7 +5185,6 @@ func Benchmark_Ctx_QueryParserWithDefaultValues(b *testing.B) { utils.AssertEqual(b, nil, err) utils.AssertEqual(b, nil, c.QueryParser(q)) - } // go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValuesForSlices -benchmem -count=4 From 4d557ee3c293fe0faf378c527bc8c8227247b953 Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Fri, 3 Nov 2023 19:12:36 +0300 Subject: [PATCH 05/28] move: query default parser main functions to utils Signed-off-by: Murat Mirgun Ercan --- app.go | 6 ++++ ctx.go | 77 +-------------------------------------------- utils/default.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 76 deletions(-) create mode 100644 utils/default.go diff --git a/app.go b/app.go index ebf1025167f..a6d37d3667f 100644 --- a/app.go +++ b/app.go @@ -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 diff --git a/ctx.go b/ctx.go index 35ddf19bc41..1094580bb0c 100644 --- a/ctx.go +++ b/ctx.go @@ -1278,7 +1278,7 @@ func (c *Ctx) QueryParser(out interface{}) error { return err } - setDefaultValues(out) + utils.SetDefaultValues(out) return c.parseToStruct(queryTag, out, data) } @@ -1346,81 +1346,6 @@ func (*Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]st return nil } -func tagHandlers(field reflect.Value, tagValue string) { - //nolint:exhaustive // We don't need to handle all types - switch field.Kind() { - case reflect.String: - if field.String() == "" { - field.SetString(tagValue) - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if field.Int() == 0 { - if i, err := strconv.ParseInt(tagValue, 10, 64); err == nil { - field.SetInt(i) - } - } - case reflect.Float32, reflect.Float64: - if field.Float() == 0.0 { - if f, err := strconv.ParseFloat(tagValue, 64); err == nil { - field.SetFloat(f) - } - } - case reflect.Bool: - if !field.Bool() { - if b, err := strconv.ParseBool(tagValue); err == nil { - field.SetBool(b) - } - } - case reflect.Slice: - setDefaultForSlice(field, tagValue, field.Type().Elem().Kind()) - } -} - -func setDefaultForSlice(field reflect.Value, tagValue string, kind reflect.Kind) { - items := strings.Split(tagValue, ",") - for _, item := range items { - //nolint:exhaustive // We don't need to handle all types - switch kind { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if i, err := strconv.ParseInt(item, 10, 64); err == nil { - field.Set(reflect.Append(field, reflect.ValueOf(int(i)))) - } - case reflect.String: - field.Set(reflect.Append(field, reflect.ValueOf(item))) - } - } -} - -var structCache = make(map[reflect.Type][]reflect.StructField) - -func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { - if fields, ok := structCache[t]; ok { - return fields - } - - var fields []reflect.StructField - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - if _, ok := field.Tag.Lookup("default"); ok { - fields = append(fields, field) - } - } - structCache[t] = fields - return fields -} - -func setDefaultValues(out interface{}) { - val := reflect.ValueOf(out).Elem() - typ := val.Type() - - fields := getFieldsWithDefaultTag(typ) - for _, fieldInfo := range fields { - field := val.FieldByName(fieldInfo.Name) - tagValue := fieldInfo.Tag.Get("default") - tagHandlers(field, tagValue) - } -} - func equalFieldType(out interface{}, kind reflect.Kind, key, tag string) bool { // Get type of interface outTyp := reflect.TypeOf(out).Elem() diff --git a/utils/default.go b/utils/default.go new file mode 100644 index 00000000000..0459031aea2 --- /dev/null +++ b/utils/default.go @@ -0,0 +1,82 @@ +package utils + +import ( + "reflect" + "strconv" + "strings" +) + +func tagHandlers(field reflect.Value, tagValue string) { + //nolint:exhaustive // We don't need to handle all types + switch field.Kind() { + case reflect.String: + if field.String() == "" { + field.SetString(tagValue) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if field.Int() == 0 { + if i, err := strconv.ParseInt(tagValue, 10, 64); err == nil { + field.SetInt(i) + } + } + case reflect.Float32, reflect.Float64: + if field.Float() == 0.0 { + if f, err := strconv.ParseFloat(tagValue, 64); err == nil { + field.SetFloat(f) + } + } + case reflect.Bool: + if !field.Bool() { + if b, err := strconv.ParseBool(tagValue); err == nil { + field.SetBool(b) + } + } + case reflect.Slice: + setDefaultForSlice(field, tagValue, field.Type().Elem().Kind()) + } +} + +func setDefaultForSlice(field reflect.Value, tagValue string, kind reflect.Kind) { + items := strings.Split(tagValue, ",") + for _, item := range items { + //nolint:exhaustive // We don't need to handle all types + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if i, err := strconv.ParseInt(item, 10, 64); err == nil { + field.Set(reflect.Append(field, reflect.ValueOf(int(i)))) + } + case reflect.String: + field.Set(reflect.Append(field, reflect.ValueOf(item))) + } + } +} + +var structCache = make(map[reflect.Type][]reflect.StructField) + +func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { + if fields, ok := structCache[t]; ok { + return fields + } + + var fields []reflect.StructField + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if _, ok := field.Tag.Lookup("default"); ok { + fields = append(fields, field) + } + } + structCache[t] = fields + return fields +} + +func SetDefaultValues(out interface{}) { + val := reflect.ValueOf(out).Elem() + typ := val.Type() + + fields := getFieldsWithDefaultTag(typ) + for _, fieldInfo := range fields { + field := val.FieldByName(fieldInfo.Name) + tagValue := fieldInfo.Tag.Get("default") + tagHandlers(field, tagValue) + } +} From 06a8cf856b8fa1a6eec21ff6b651dd78d51eeb6b Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Fri, 3 Nov 2023 19:37:20 +0300 Subject: [PATCH 06/28] :test_tube: test: write basic tests for util/default Signed-off-by: Murat Mirgun Ercan --- ctx.go | 4 +- ctx_test.go | 2 +- utils/default_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 utils/default_test.go diff --git a/ctx.go b/ctx.go index 1094580bb0c..45f3cfe0d8a 100644 --- a/ctx.go +++ b/ctx.go @@ -1278,7 +1278,9 @@ func (c *Ctx) QueryParser(out interface{}) error { return err } - utils.SetDefaultValues(out) + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } return c.parseToStruct(queryTag, out, data) } diff --git a/ctx_test.go b/ctx_test.go index 9784cdd88e4..5ba2df17902 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -5161,7 +5161,7 @@ func Benchmark_Ctx_QueryParser(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValues -benchmem -count=4 func Benchmark_Ctx_QueryParserWithDefaultValues(b *testing.B) { - app := New() + app := New(Config{DefaultValueParser: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) diff --git a/utils/default_test.go b/utils/default_test.go new file mode 100644 index 00000000000..3bd614c6aeb --- /dev/null +++ b/utils/default_test.go @@ -0,0 +1,125 @@ +package utils + +import ( + "reflect" + "testing" +) + +type TestStruct struct { + Name string `default:"John"` + Age int `default:"25"` + Height float64 `default:"5.9"` + IsActive bool `default:"true"` + Friends []string `default:"Alice,Bob"` +} + +func TestSetDefaultValues(t *testing.T) { + tests := []struct { + name string + input *TestStruct + expected *TestStruct + }{ + { + name: "All fields empty", + input: &TestStruct{}, + expected: &TestStruct{Name: "John", Age: 25, Height: 5.9, IsActive: true, Friends: []string{"Alice", "Bob"}}, + }, + { + name: "Some fields set", + input: &TestStruct{Name: "Doe", Age: 0, Height: 0.0, IsActive: false, Friends: nil}, + expected: &TestStruct{Name: "Doe", Age: 25, Height: 5.9, IsActive: true, Friends: []string{"Alice", "Bob"}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + SetDefaultValues(tt.input) + if !reflect.DeepEqual(tt.input, tt.expected) { + t.Errorf("got %v, want %v", tt.input, tt.expected) + } + }) + } +} + +func TestTagHandlers(t *testing.T) { + tests := []struct { + name string + field reflect.Value + tagValue string + expected interface{} + }{ + { + name: "String field with default value", + field: reflect.ValueOf(new(string)).Elem(), + tagValue: "test", + expected: "test", + }, + { + name: "Int field with default value", + field: reflect.ValueOf(new(int)).Elem(), + tagValue: "42", + expected: 42, + }, + { + name: "Float64 field with default value", + field: reflect.ValueOf(new(float64)).Elem(), + tagValue: "3.14", + expected: 3.14, + }, + { + name: "Bool field with default value", + field: reflect.ValueOf(new(bool)).Elem(), + tagValue: "true", + expected: true, + }, + { + name: "Slice of strings with default value", + field: reflect.ValueOf(new([]string)).Elem(), + tagValue: "apple,banana", + expected: []string{"apple", "banana"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tagHandlers(tt.field, tt.tagValue) + if !reflect.DeepEqual(tt.field.Interface(), tt.expected) { + t.Errorf("got %v, want %v", tt.field.Interface(), tt.expected) + } + }) + } +} + +func TestSetDefaultForSlice(t *testing.T) { + tests := []struct { + name string + field reflect.Value + tagValue string + kind reflect.Kind + expected interface{} + }{ + { + name: "Slice of strings with default value", + field: reflect.ValueOf(new([]string)).Elem(), + tagValue: "apple,banana", + kind: reflect.String, + expected: []string{"apple", "banana"}, + }, + { + name: "Slice of ints with default value", + field: reflect.ValueOf(new([]int)).Elem(), + tagValue: "1,2,3", + kind: reflect.Int, + expected: []int{1, 2, 3}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setDefaultForSlice(tt.field, tt.tagValue, tt.kind) + if !reflect.DeepEqual(tt.field.Interface(), tt.expected) { + t.Errorf("got %v, want %v", tt.field.Interface(), tt.expected) + } + }) + } +} From 257b399719e7cf11de1c1d822708b3c57a53274b Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Fri, 3 Nov 2023 19:44:45 +0300 Subject: [PATCH 07/28] :memo: docs: add DefaultValueParser config field description Signed-off-by: Murat Mirgun Ercan --- docs/api/fiber.md | 83 ++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 1f9a91b8bf6..63033f45b31 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -39,48 +39,49 @@ app := fiber.New(fiber.Config{ **Config fields** -| Property | Type | Description | Default | -| ---------------------------- | --------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --------------------- | -| AppName | `string` | This allows to setup app name for the app | `""` | -| BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | -| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | +| Property | Type | Description | Default | +|------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| AppName | `string` | This allows to setup app name for the app | `""` | +| BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | +| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | | ColorScheme | [`Colors`](https://github.com/gofiber/fiber/blob/master/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares. | [`DefaultColors`](https://github.com/gofiber/fiber/blob/master/color.go) | -| CompressedFileSuffix | `string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `".fiber.gz"` | -| Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | -| DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | -| DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | -| DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | -| DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | -| DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | -| DisableStartupMessage | `bool` | When set to true, it will not print out debug information | `false` | -| ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | -| EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma seperated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | -| EnablePrintRoutes | `bool` | EnablePrintRoutes enables print all routes with their method, path, name and handler.. | `false` | -| EnableSplittingOnParsers | `bool` | EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.

For example, you can use it to parse multiple values from a query parameter like this: `/api?foo=bar,baz == foo[]=bar&foo[]=baz` | `false` | -| EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | -| ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | -| GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | -| IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | -| Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | -| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | -| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | -| Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | -| PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | -| Prefork | `bool` | Enables use of the[`SO_REUSEPORT`](https://lwn.net/Articles/542629/)socket option. This will spawn multiple Go processes listening on the same port. learn more about [socket sharding](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/). **NOTE: if enabled, the application will need to be ran through a shell because prefork mode sets environment variables. If you're using Docker, make sure the app is ran with `CMD ./app` or `CMD ["sh", "-c", "/app"]`. For more info, see** [**this**](https://github.com/gofiber/fiber/issues/1021#issuecomment-730537971) **issue comment.** | `false` | -| ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | -| ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | -| ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | -| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | -| ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | -| StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger then the current limit. | `false` | -| StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | -| TrustedProxies | `[]string` | Contains the list of trusted proxy IP's. Look at `EnableTrustedProxyCheck` doc.

It can take IP or IP range addresses. If it gets IP range, it iterates all possible addresses. | `[]string*__*` | -| UnescapePath | `bool` | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters | `false` | -| Views | `Views` | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines. | `nil` | -| ViewsLayout | `string` | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines. | `""` | -| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | -| WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | -| XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | +| CompressedFileSuffix | `string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `".fiber.gz"` | +| Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | +| DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | +| DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | +| DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | +| DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | +| DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | +| DisableStartupMessage | `bool` | When set to true, it will not print out debug information | `false` | +| ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | +| EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma seperated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | +| EnablePrintRoutes | `bool` | EnablePrintRoutes enables print all routes with their method, path, name and handler.. | `false` | +| EnableSplittingOnParsers | `bool` | EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.

For example, you can use it to parse multiple values from a query parameter like this: `/api?foo=bar,baz == foo[]=bar&foo[]=baz` | `false` | +| EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | +| ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | +| GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | +| IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | +| Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | +| DefaultValueParser | `bool` | 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. | `false` | +| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | +| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | +| Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | +| PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | +| Prefork | `bool` | Enables use of the[`SO_REUSEPORT`](https://lwn.net/Articles/542629/)socket option. This will spawn multiple Go processes listening on the same port. learn more about [socket sharding](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/). **NOTE: if enabled, the application will need to be ran through a shell because prefork mode sets environment variables. If you're using Docker, make sure the app is ran with `CMD ./app` or `CMD ["sh", "-c", "/app"]`. For more info, see** [**this**](https://github.com/gofiber/fiber/issues/1021#issuecomment-730537971) **issue comment.** | `false` | +| ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | +| ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | +| ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | +| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | +| ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | +| StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger then the current limit. | `false` | +| StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | +| TrustedProxies | `[]string` | Contains the list of trusted proxy IP's. Look at `EnableTrustedProxyCheck` doc.

It can take IP or IP range addresses. If it gets IP range, it iterates all possible addresses. | `[]string*__*` | +| UnescapePath | `bool` | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters | `false` | +| Views | `Views` | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines. | `nil` | +| ViewsLayout | `string` | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines. | `""` | +| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | +| WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | +| XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | ## NewError From ca1943e9e448bfd6a3c0b09a830b9946c7b34273 Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Fri, 3 Nov 2023 20:16:33 +0300 Subject: [PATCH 08/28] :hammer: refactor: slice default parser optimization --- utils/default.go | 24 ++++++++++++++++++------ utils/default_test.go | 10 ++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/utils/default.go b/utils/default.go index 0459031aea2..df411bb4ea2 100644 --- a/utils/default.go +++ b/utils/default.go @@ -38,17 +38,29 @@ func tagHandlers(field reflect.Value, tagValue string) { func setDefaultForSlice(field reflect.Value, tagValue string, kind reflect.Kind) { items := strings.Split(tagValue, ",") - for _, item := range items { - //nolint:exhaustive // We don't need to handle all types - switch kind { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + + if len(items) == 0 { + return + } + + // if first item is string, then all items are string type + if kind == reflect.String { + for _, item := range items { + field.Set(reflect.Append(field, reflect.ValueOf(item))) + } + return + } + + // if first item is int, then all items are int type + if kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 { + for _, item := range items { if i, err := strconv.ParseInt(item, 10, 64); err == nil { field.Set(reflect.Append(field, reflect.ValueOf(int(i)))) } - case reflect.String: - field.Set(reflect.Append(field, reflect.ValueOf(item))) } + return } + } var structCache = make(map[reflect.Type][]reflect.StructField) diff --git a/utils/default_test.go b/utils/default_test.go index 3bd614c6aeb..e1ea3800117 100644 --- a/utils/default_test.go +++ b/utils/default_test.go @@ -75,8 +75,14 @@ func TestTagHandlers(t *testing.T) { { name: "Slice of strings with default value", field: reflect.ValueOf(new([]string)).Elem(), - tagValue: "apple,banana", - expected: []string{"apple", "banana"}, + tagValue: "apple,1", + expected: []string{"apple", "1"}, + }, + { + name: "Slice of ints with default value", + field: reflect.ValueOf(new([]int)).Elem(), + tagValue: "1,2,deneme", + expected: []int{1, 2}, }, } From b85aecc72b267dd12ee6989dd9afcc018452726f Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Mon, 6 Nov 2023 20:30:48 +0300 Subject: [PATCH 09/28] :rocket: feat: add pointer slice support for parser and test case update Signed-off-by: Murat Mirgun Ercan --- ctx.go | 4 ++++ ctx_test.go | 2 +- utils/default.go | 48 +++++++++++++++++++++++++------------------ utils/default_test.go | 32 ++++++++++++++++++++++++----- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/ctx.go b/ctx.go index 45f3cfe0d8a..3907d76cdd1 100644 --- a/ctx.go +++ b/ctx.go @@ -1327,6 +1327,10 @@ func (c *Ctx) ReqHeaderParser(out interface{}) error { } }) + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + return c.parseToStruct(reqHeaderTag, out, data) } diff --git a/ctx_test.go b/ctx_test.go index 5ba2df17902..f1dd5099591 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -5189,7 +5189,7 @@ func Benchmark_Ctx_QueryParserWithDefaultValues(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValuesForSlices -benchmem -count=4 func Benchmark_Ctx_QueryParserWithDefaultValuesForSlices(b *testing.B) { - app := New() + app := New(Config{DefaultValueParser: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) diff --git a/utils/default.go b/utils/default.go index df411bb4ea2..6099925d925 100644 --- a/utils/default.go +++ b/utils/default.go @@ -32,35 +32,43 @@ func tagHandlers(field reflect.Value, tagValue string) { } } case reflect.Slice: - setDefaultForSlice(field, tagValue, field.Type().Elem().Kind()) + setDefaultForSlice(field, tagValue, field.Type().Elem()) } } -func setDefaultForSlice(field reflect.Value, tagValue string, kind reflect.Kind) { +func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.Type) { items := strings.Split(tagValue, ",") + slice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, len(items)) + for _, item := range items { + var val reflect.Value + switch elemType.Kind() { + case reflect.Ptr: + elemKind := elemType.Elem().Kind() + switch elemKind { + case reflect.String: + strVal := item + val = reflect.ValueOf(&strVal) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { + intPtr := reflect.New(elemType.Elem()) + intPtr.Elem().SetInt(intVal) + val = intPtr + } - if len(items) == 0 { - return - } - - // if first item is string, then all items are string type - if kind == reflect.String { - for _, item := range items { - field.Set(reflect.Append(field, reflect.ValueOf(item))) - } - return - } - - // if first item is int, then all items are int type - if kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 { - for _, item := range items { - if i, err := strconv.ParseInt(item, 10, 64); err == nil { - field.Set(reflect.Append(field, reflect.ValueOf(int(i)))) } + case reflect.String: + val = reflect.ValueOf(item) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { + val = reflect.ValueOf(int(intVal)) + } + } + if val.IsValid() { + slice = reflect.Append(slice, val) } - return } + field.Set(slice) } var structCache = make(map[reflect.Type][]reflect.StructField) diff --git a/utils/default_test.go b/utils/default_test.go index e1ea3800117..8c1c3b0175f 100644 --- a/utils/default_test.go +++ b/utils/default_test.go @@ -95,37 +95,59 @@ func TestTagHandlers(t *testing.T) { }) } } - func TestSetDefaultForSlice(t *testing.T) { tests := []struct { name string field reflect.Value tagValue string - kind reflect.Kind + elemType reflect.Type expected interface{} }{ { name: "Slice of strings with default value", field: reflect.ValueOf(new([]string)).Elem(), tagValue: "apple,banana", - kind: reflect.String, + elemType: reflect.TypeOf(""), expected: []string{"apple", "banana"}, }, { name: "Slice of ints with default value", field: reflect.ValueOf(new([]int)).Elem(), tagValue: "1,2,3", - kind: reflect.Int, + elemType: reflect.TypeOf(0), expected: []int{1, 2, 3}, }, + { + name: "Slice of string pointers with default value", + field: reflect.ValueOf(new([]*string)).Elem(), + tagValue: "apple,banana", + elemType: reflect.TypeOf(new(*string)).Elem(), + expected: []*string{str("apple"), str("banana")}, + }, + { + name: "Slice of int pointers with default value", + field: reflect.ValueOf(new([]*int)).Elem(), + tagValue: "1,2,3", + elemType: reflect.TypeOf(new(*int)).Elem(), + expected: []*int{ptr(1), ptr(2), ptr(3)}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - setDefaultForSlice(tt.field, tt.tagValue, tt.kind) + setDefaultForSlice(tt.field, tt.tagValue, tt.elemType) if !reflect.DeepEqual(tt.field.Interface(), tt.expected) { t.Errorf("got %v, want %v", tt.field.Interface(), tt.expected) } }) } } + +// ptr is a helper function to take the address of a string. +func ptr(s int) *int { + return &s +} + +func str(s string) *string { + return &s +} From ba8eb0d9c366780fc7bbfaa96df5a515238911de Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Tue, 7 Nov 2023 03:17:16 +0300 Subject: [PATCH 10/28] :test_tube: update header parser test files --- ctx_test.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++ utils/default.go | 5 +- 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/ctx_test.go b/ctx_test.go index f1dd5099591..23675419083 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4789,6 +4789,72 @@ func Test_Ctx_ReqHeaderParser(t *testing.T) { utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) } +// go test -run Test_Ctx_ReqHeaderParserWithDefaultValues -v +func Test_Ctx_ReqHeaderParserWithDefaultValues(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header struct { + ID int `default:"10"` + Name string `default:"defaultStr"` + Hobby []string `default:"golang,fiber,go"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + q := new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 3, 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)) + + type Header2 struct { + Bool bool `default:"true"` + ID int `default:"10"` + Name string `default:"defaultStr"` + Hobby string `default:"golang,fiber,go"` + FavouriteDrinks []string `default:"milo,coke,pepsi"` + Empty []string `default:""` + Alloc []string `default:""` + No []int64 `default:"1"` + } + + h2 := new(Header2) + h2.Bool = true + h2.Name = "hello world 3" + utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) + utils.AssertEqual(t, "golang,fiber,go", h2.Hobby) + utils.AssertEqual(t, true, h2.Bool) + utils.AssertEqual(t, "hello world 3", h2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) + var nilSlice []string + /* + here is deep equal issue with empty slice and nil slice + ctx_test.go:4837: + Test: Test_Ctx_ReqHeaderParserWithDefaultValues + Trace: ctx_test.go:4837 + Description: empty slice should be nil + Expect: [] ([]string) + Result: [] ([]string) + */ + reflect.DeepEqual(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, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) +} + // go test -run Test_Ctx_ReqHeaderParserUsingTag -v func Test_Ctx_ReqHeaderParserUsingTag(t *testing.T) { t.Parallel() @@ -4864,6 +4930,72 @@ func Test_Ctx_ReqHeaderParserUsingTag(t *testing.T) { utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) } +// go test -run Test_Ctx_ReqHeaderParserUsingTagWithDefaultValues -v +func Test_Ctx_ReqHeaderParserUsingTagWithDefaultValues(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header struct { + ID int `reqHeader:"id" default:"1"` + Name string `reqHeader:"name" default:"John Doe"` + Hobby []string `reqHeader:"hobby" default:"golang,fiber,go"` + Address []string `reqHeader:"x-secure-address" default:"1st,2st"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + q := new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + utils.AssertEqual(t, 2, len(q.Address)) + + type Header2 struct { + Bool bool `reqHeader:"bool" default:"true"` + ID int `reqHeader:"id" default:"2"` + Name string `reqHeader:"name" default:"John Doe"` + Hobby string `reqHeader:"hobby" default:"go,fiber"` + FavouriteDrinks []string `reqHeader:"favouriteDrinks" default:"milo,coke,pepsi"` + Empty []string `reqHeader:"empty" default:""` + Alloc []string `reqHeader:"alloc" default:""` + No []int64 `reqHeader:"no" default:"1"` + } + + 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 4" + 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 + /* + here is deep equal issue with empty slice and nil slice + Description: empty slice should be nil + Expect: [] ([]string) + Result: [] ([]string) + */ + reflect.DeepEqual(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, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) +} + // go test -run Test_Ctx_ReqHeaderParser -v func Test_Ctx_ReqHeaderParser_WithoutSplitting(t *testing.T) { t.Parallel() @@ -5300,6 +5432,35 @@ func Benchmark_Ctx_ReqHeaderParser(b *testing.B) { utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) } +// go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParserWithDefaultValues -benchmem -count=4 +func Benchmark_Ctx_ReqHeaderParserWithDefaultValues(b *testing.B) { + app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type ReqHeader struct { + ID int `default:"4"` + Name string `default:"john"` + Hobby []string `default:"basketball,football"` + } + 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() + + var err error + for n := 0; n < b.N; n++ { + err = c.ReqHeaderParser(q) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) +} + // go test -run Test_Ctx_BodyStreamWriter func Test_Ctx_BodyStreamWriter(t *testing.T) { t.Parallel() diff --git a/utils/default.go b/utils/default.go index 6099925d925..2347b7caceb 100644 --- a/utils/default.go +++ b/utils/default.go @@ -41,9 +41,11 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T slice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, len(items)) for _, item := range items { var val reflect.Value + //nolint:exhaustive // We don't need to handle all types switch elemType.Kind() { case reflect.Ptr: elemKind := elemType.Elem().Kind() + //nolint:exhaustive // We don't need to handle all types switch elemKind { case reflect.String: strVal := item @@ -54,13 +56,12 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T intPtr.Elem().SetInt(intVal) val = intPtr } - } case reflect.String: val = reflect.ValueOf(item) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { - val = reflect.ValueOf(int(intVal)) + val = reflect.ValueOf(int64(intVal)) } } if val.IsValid() { From 8394b30b9e13c3e3bc856638b316bea4c2fd6be9 Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Tue, 7 Nov 2023 03:17:24 +0300 Subject: [PATCH 11/28] :memo: docs: add header parser usage with default values Signed-off-by: Murat Mirgun Ercan --- docs/api/ctx.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 82e89515912..cb2954c94f4 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -1554,6 +1554,35 @@ app.Get("/", func(c *fiber.Ctx) error { // curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" ``` +### Default Values with ReqHeaderParser +You can also assign default values to struct fields if the request header is not provided in the request. To do this, use the default struct tag alongside the reqHeader tag. + +```go title="WithDefaultValues" +type PersonWithDefaults struct { + Name string `reqHeader:"name" default:"DefaultName"` + Pass string `reqHeader:"pass" default:"DefaultPass"` + Products []string `reqHeader:"products" default:"defaultProduct1,defaultProduct2"` +} + +app.Get("/defaults", func(c *fiber.Ctx) error { + p := new(PersonWithDefaults) + + if err := c.ReqHeaderParser(p); err != nil { + return err + } + + log.Println(p.Name) // Will print "DefaultName" if name is not provided in the request header + log.Println(p.Pass) // Will print "DefaultPass" if pass is not provided in the request header + log.Println(p.Products) // Will print [defaultProduct1, defaultProduct2] if products is not provided in the request header + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/defaults" +// This will use the default values since no request headers are provided + +``` ## Response Response return the [\*fasthttp.Response](https://godoc.org/github.com/valyala/fasthttp#Response) pointer From 662f9d6bfd139cd755a7b9abfb7b8a4c510b405b Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Thu, 9 Nov 2023 20:11:15 +0300 Subject: [PATCH 12/28] lint: delete redundant conversion Signed-off-by: Murat Mirgun Ercan --- utils/default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/default.go b/utils/default.go index 2347b7caceb..4f53884219c 100644 --- a/utils/default.go +++ b/utils/default.go @@ -61,7 +61,7 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T val = reflect.ValueOf(item) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { - val = reflect.ValueOf(int64(intVal)) + val = reflect.ValueOf(intVal) } } if val.IsValid() { From 00e47614adeacda95b18f618d5d92b0a71b9e34c Mon Sep 17 00:00:00 2001 From: Murat Mirgun Ercan Date: Thu, 9 Nov 2023 21:50:01 +0300 Subject: [PATCH 13/28] test: fix int assign issue on default slice parser Signed-off-by: Murat Mirgun Ercan --- utils/default.go | 14 +++++++++++++- utils/default_test.go | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/utils/default.go b/utils/default.go index 4f53884219c..94890b61f2d 100644 --- a/utils/default.go +++ b/utils/default.go @@ -61,10 +61,22 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T val = reflect.ValueOf(item) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { - val = reflect.ValueOf(intVal) + switch elemType.Kind() { + case reflect.Int: + val = reflect.ValueOf(int(intVal)) + case reflect.Int8: + val = reflect.ValueOf(int8(intVal)) + case reflect.Int16: + val = reflect.ValueOf(int16(intVal)) + case reflect.Int32: + val = reflect.ValueOf(int32(intVal)) + case reflect.Int64: + val = reflect.ValueOf(intVal) + } } } if val.IsValid() { + slice = reflect.Append(slice, val) } } diff --git a/utils/default_test.go b/utils/default_test.go index 8c1c3b0175f..aa5f7df32d7 100644 --- a/utils/default_test.go +++ b/utils/default_test.go @@ -81,7 +81,7 @@ func TestTagHandlers(t *testing.T) { { name: "Slice of ints with default value", field: reflect.ValueOf(new([]int)).Elem(), - tagValue: "1,2,deneme", + tagValue: "1,2", expected: []int{1, 2}, }, } From 253b4c9dc3bee86672f85b2c4e7b7290433fa1db Mon Sep 17 00:00:00 2001 From: Murat mirgun Ercan Date: Tue, 28 Nov 2023 22:22:05 +0300 Subject: [PATCH 14/28] feat: add default value parser to cookie parser with tests Signed-off-by: Murat mirgun Ercan --- ctx.go | 20 ++++++----- ctx_test.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/ctx.go b/ctx.go index 3907d76cdd1..d996b7363f7 100644 --- a/ctx.go +++ b/ctx.go @@ -506,6 +506,10 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string { // CookieParser is used to bind cookies to a struct func (c *Ctx) CookieParser(out interface{}) error { + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + data := make(map[string][]string) var err error @@ -1249,6 +1253,10 @@ func (c *Ctx) QueryFloat(key string, defaultValue ...float64) float64 { // QueryParser binds the query string to a struct. func (c *Ctx) QueryParser(out interface{}) error { + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + data := make(map[string][]string) var err error @@ -1278,10 +1286,6 @@ func (c *Ctx) QueryParser(out interface{}) error { return err } - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } - return c.parseToStruct(queryTag, out, data) } @@ -1312,6 +1316,10 @@ func parseParamSquareBrackets(k string) (string, error) { // ReqHeaderParser binds the request header strings to a struct. func (c *Ctx) ReqHeaderParser(out interface{}) error { + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + data := make(map[string][]string) c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { k := c.app.getString(key) @@ -1327,10 +1335,6 @@ func (c *Ctx) ReqHeaderParser(out interface{}) error { } }) - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } - return c.parseToStruct(reqHeaderTag, out, data) } diff --git a/ctx_test.go b/ctx_test.go index 23675419083..e015a901d38 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -961,6 +961,36 @@ func Test_Ctx_CookieParser(t *testing.T) { utils.AssertEqual(t, 0, len(empty.Courses)) } +func Test_Ctx_CookieParserWithDefaultParser(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Cookie struct { + Name string ` default:"doe"` + Class int `default:"10"` + Courses []string + } + c.Request().Header.Set("Cookie", "courses=maths,english") + cookie := new(Cookie) + + // correct test cases + utils.AssertEqual(t, nil, c.CookieParser(cookie)) + utils.AssertEqual(t, "doe", cookie.Name) + utils.AssertEqual(t, 10, cookie.Class) + utils.AssertEqual(t, 2, len(cookie.Courses)) + + // wrong test cases + empty := new(Cookie) + c.Request().Header.Set("Cookie", "name") + c.Request().Header.Set("Cookie", "class") + c.Request().Header.Set("Cookie", "courses") + utils.AssertEqual(t, nil, c.CookieParser(cookie)) + utils.AssertEqual(t, "", empty.Name) + utils.AssertEqual(t, 0, empty.Class) + utils.AssertEqual(t, 0, len(empty.Courses)) +} + // go test -run Test_Ctx_CookieParserUsingTag -v func Test_Ctx_CookieParserUsingTag(t *testing.T) { t.Parallel() @@ -1007,6 +1037,48 @@ func Test_Ctx_CookieParserUsingTag(t *testing.T) { utils.AssertEqual(t, 3, len(ac.Dates)) } +func Test_Ctx_CookieParserUsingTagWithDefaultValues(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Cook struct { + ID int `cookie:"id" default:"10"` + Name string `cookie:"name"` + Courses []string `cookie:"courses"` + Enrolled bool `cookie:"student" default:"true"` + Fees float32 `cookie:"fee" default:"45.78"` + Grades []uint8 `cookie:"score"` + } + cookie1 := new(Cook) + cookie1.Name = "Joseph" + utils.AssertEqual(t, "Joseph", cookie1.Name) + + c.Request().Header.Set("Cookie", "name=Joey") + c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics") + c.Request().Header.Set("Cookie", "score=7,6,10") + utils.AssertEqual(t, nil, c.CookieParser(cookie1)) + utils.AssertEqual(t, "Joey", cookie1.Name) + utils.AssertEqual(t, true, cookie1.Enrolled) + utils.AssertEqual(t, float32(45.78), cookie1.Fees) + utils.AssertEqual(t, []uint8{7, 6, 10}, cookie1.Grades) + + type RequiredCookie struct { + House string `cookie:"house,required"` + } + rc := new(RequiredCookie) + utils.AssertEqual(t, "failed to decode: house is empty", c.CookieParser(rc).Error()) + + type ArrayCookie struct { + Dates []int + } + + ac := new(ArrayCookie) + c.Request().Header.Set("Cookie", "dates[]=7,6,10") + utils.AssertEqual(t, nil, c.CookieParser(ac)) + utils.AssertEqual(t, 3, len(ac.Dates)) +} + // go test -run Test_Ctx_CookieParserSchema -v func Test_Ctx_CookieParser_Schema(t *testing.T) { t.Parallel() @@ -1074,6 +1146,34 @@ func Benchmark_Ctx_CookieParser(b *testing.B) { utils.AssertEqual(b, nil, err) } +func Benchmark_Ctx_CookieParserWithDefaultValues(b *testing.B) { + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Cook struct { + ID int `cookie:"id" default:"10"` + Name string `cookie:"name"` + Courses []string `cookie:"courses" default:"maths,english, chemistry, physics"` + Enrolled bool `cookie:"student"` + Fees float32 `cookie:"fee"` + Grades []uint8 `cookie:"score"` + } + cookie1 := new(Cook) + cookie1.Name = "Joseph" + + c.Request().Header.Set("Cookie", "name=Joey") + c.Request().Header.Set("Cookie", "student=true") + c.Request().Header.Set("Cookie", "fee=45.78") + c.Request().Header.Set("Cookie", "score=7,6,10") + + var err error + // Run the function b.N times + for i := 0; i < b.N; i++ { + err = c.CookieParser(cookie1) + } + utils.AssertEqual(b, nil, err) +} + // go test -run Test_Ctx_Cookies func Test_Ctx_Cookies(t *testing.T) { t.Parallel() From 8b99f134d355c0cae4122364426384d212b6afbd Mon Sep 17 00:00:00 2001 From: Murat mirgun Ercan Date: Tue, 28 Nov 2023 22:29:43 +0300 Subject: [PATCH 15/28] fix: codeql security fix Signed-off-by: Murat mirgun Ercan --- utils/default.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/utils/default.go b/utils/default.go index 94890b61f2d..efbf69826eb 100644 --- a/utils/default.go +++ b/utils/default.go @@ -1,6 +1,7 @@ package utils import ( + "math" "reflect" "strconv" "strings" @@ -63,13 +64,21 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { switch elemType.Kind() { case reflect.Int: - val = reflect.ValueOf(int(intVal)) + if intVal >= int64(math.MinInt) && intVal <= int64(math.MaxInt) { + val = reflect.ValueOf(int(intVal)) + } case reflect.Int8: - val = reflect.ValueOf(int8(intVal)) + if intVal >= int64(math.MinInt8) && intVal <= int64(math.MaxInt8) { + val = reflect.ValueOf(int8(intVal)) + } case reflect.Int16: - val = reflect.ValueOf(int16(intVal)) + if intVal >= int64(math.MinInt16) && intVal <= int64(math.MaxInt16) { + val = reflect.ValueOf(int16(intVal)) + } case reflect.Int32: - val = reflect.ValueOf(int32(intVal)) + if intVal >= int64(math.MinInt32) && intVal <= int64(math.MaxInt32) { + val = reflect.ValueOf(int32(intVal)) + } case reflect.Int64: val = reflect.ValueOf(intVal) } From a869138cd7fe5967a30a3b776c501856e6001022 Mon Sep 17 00:00:00 2001 From: Murat mirgun Ercan Date: Tue, 28 Nov 2023 22:43:06 +0300 Subject: [PATCH 16/28] security: int value conversion check change for security Signed-off-by: Murat mirgun Ercan --- utils/default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/default.go b/utils/default.go index efbf69826eb..51256064594 100644 --- a/utils/default.go +++ b/utils/default.go @@ -64,7 +64,7 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { switch elemType.Kind() { case reflect.Int: - if intVal >= int64(math.MinInt) && intVal <= int64(math.MaxInt) { + if strconv.IntSize == 64 || (intVal >= int64(math.MinInt32) && intVal <= int64(math.MaxInt32)) { val = reflect.ValueOf(int(intVal)) } case reflect.Int8: From ad89ced1a32fb033133ef1afe3c219a5cfeea702 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 23 Dec 2023 22:42:26 +0300 Subject: [PATCH 17/28] add mutex --- utils/default.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/default.go b/utils/default.go index 51256064594..bdf8a9053d4 100644 --- a/utils/default.go +++ b/utils/default.go @@ -5,6 +5,7 @@ import ( "reflect" "strconv" "strings" + "sync" ) func tagHandlers(field reflect.Value, tagValue string) { @@ -93,6 +94,7 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T field.Set(slice) } +var mu sync.Mutex var structCache = make(map[reflect.Type][]reflect.StructField) func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { @@ -107,7 +109,11 @@ func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { fields = append(fields, field) } } + + mu.Lock() structCache[t] = fields + mu.Unlock() + return fields } From 413f42a3fe895c4adf8d03d1f19a2718c1eafc36 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 23 Dec 2023 22:45:19 +0300 Subject: [PATCH 18/28] fix linter --- utils/default.go | 7 ++++--- utils/default_test.go | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/utils/default.go b/utils/default.go index bdf8a9053d4..c97149d8880 100644 --- a/utils/default.go +++ b/utils/default.go @@ -86,7 +86,6 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T } } if val.IsValid() { - slice = reflect.Append(slice, val) } } @@ -94,8 +93,10 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T field.Set(slice) } -var mu sync.Mutex -var structCache = make(map[reflect.Type][]reflect.StructField) +var ( + mu sync.Mutex + structCache = make(map[reflect.Type][]reflect.StructField) +) func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { if fields, ok := structCache[t]; ok { diff --git a/utils/default_test.go b/utils/default_test.go index aa5f7df32d7..f3b241dc040 100644 --- a/utils/default_test.go +++ b/utils/default_test.go @@ -95,6 +95,7 @@ func TestTagHandlers(t *testing.T) { }) } } + func TestSetDefaultForSlice(t *testing.T) { tests := []struct { name string From 26dd70815a391f555dbf5be40f1b8de94cc9272b Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 23 Dec 2023 22:55:45 +0300 Subject: [PATCH 19/28] fix tests --- ctx_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctx_test.go b/ctx_test.go index e015a901d38..c5536019316 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4544,7 +4544,7 @@ func Test_Ctx_QueryParserUsingTag(t *testing.T) { // go test -run Test_Ctx_QueryParserWithDefaultValues -v func Test_Ctx_QueryParserWithDefaultValues(t *testing.T) { t.Parallel() - app := New(Config{EnableSplittingOnParsers: true}) + app := New(Config{DefaultValueParser: true, EnableSplittingOnParsers: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) type Query struct { From daaed0d243ad07839e1b1d9da82e0a03fdfd7f3e Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 23 Dec 2023 23:17:51 +0300 Subject: [PATCH 20/28] fix sec --- utils/default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/default.go b/utils/default.go index c97149d8880..4dfa8da3952 100644 --- a/utils/default.go +++ b/utils/default.go @@ -65,7 +65,7 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T if intVal, err := strconv.ParseInt(item, 10, 64); err == nil { switch elemType.Kind() { case reflect.Int: - if strconv.IntSize == 64 || (intVal >= int64(math.MinInt32) && intVal <= int64(math.MaxInt32)) { + if strconv.IntSize == 64 && (intVal >= int64(math.MinInt32) && intVal <= int64(math.MaxInt32)) { val = reflect.ValueOf(int(intVal)) } case reflect.Int8: From 721c28460c66e3d5b6c0f74f9d00d7357a9b7e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Thu, 4 Jan 2024 14:23:12 +0100 Subject: [PATCH 21/28] Query, Header Default Value Tag Feature #2699 :green_heart: fix lint :bulb: add default parser to BodyParser and ParamsParser --- ctx.go | 7 +++++++ ctx_test.go | 18 +++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/ctx.go b/ctx.go index fb3fe8485b2..347013e4d47 100644 --- a/ctx.go +++ b/ctx.go @@ -376,6 +376,9 @@ func decoderBuilder(parserConfig ParserConfig) interface{} { // All JSON extenstion mime types are supported (eg. application/problem+json) // If none of the content types above are matched, it will return a ErrUnprocessableEntity error func (c *Ctx) BodyParser(out interface{}) error { + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } // Get content-type ctype := utils.ToLower(c.app.getString(c.fasthttp.Request.Header.ContentType())) @@ -1088,6 +1091,10 @@ func (c *Ctx) AllParams() map[string]string { // ParamsParser binds the param string to a struct. func (c *Ctx) ParamsParser(out interface{}) error { + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + params := make(map[string][]string, len(c.route.Params)) for _, param := range c.route.Params { params[param] = append(params[param], c.Params(param)) diff --git a/ctx_test.go b/ctx_test.go index 044de055223..fb3c5e6e8dc 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -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() @@ -1032,8 +1036,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") @@ -1077,8 +1081,8 @@ func Test_Ctx_CookieParserUsingTagWithDefaultValues(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", "name=Joey") c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics") @@ -1121,7 +1125,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, @@ -1155,7 +1159,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") @@ -1185,7 +1189,7 @@ func Benchmark_Ctx_CookieParserWithDefaultValues(b *testing.B) { Grades []uint8 `cookie:"score"` } cookie1 := new(Cook) - cookie1.Name = "Joseph" + cookie1.Name = cookieName c.Request().Header.Set("Cookie", "name=Joey") c.Request().Header.Set("Cookie", "student=true") From ce98c499e944b150d6cf0f8bde9060afe9c02802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Thu, 4 Jan 2024 16:55:40 +0100 Subject: [PATCH 22/28] =?UTF-8?q?Query,=20Header=20Default=20Value=20Tag?= =?UTF-8?q?=20Feature=20#2699=20=E2=9A=A1=EF=B8=8F=20improve=20code=20and?= =?UTF-8?q?=20provide=20better=20unittests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ctx.go | 65 +++--- ctx_test.go | 571 ++++++++++++++++------------------------------------ 2 files changed, 203 insertions(+), 433 deletions(-) diff --git a/ctx.go b/ctx.go index 347013e4d47..bcb9a830c75 100644 --- a/ctx.go +++ b/ctx.go @@ -376,9 +376,6 @@ func decoderBuilder(parserConfig ParserConfig) interface{} { // All JSON extenstion mime types are supported (eg. application/problem+json) // If none of the content types above are matched, it will return a ErrUnprocessableEntity error func (c *Ctx) BodyParser(out interface{}) error { - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } // Get content-type ctype := utils.ToLower(c.app.getString(c.fasthttp.Request.Header.ContentType())) @@ -391,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) { @@ -413,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"): + return c.app.config.JSONDecoder(c.Body(), out) } - return nil } + // No suitable content type found return ErrUnprocessableEntity } @@ -519,10 +523,6 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string { // CookieParser is used to bind cookies to a struct func (c *Ctx) CookieParser(out interface{}) error { - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } - data := make(map[string][]string) var err error @@ -1091,10 +1091,6 @@ func (c *Ctx) AllParams() map[string]string { // ParamsParser binds the param string to a struct. func (c *Ctx) ParamsParser(out interface{}) error { - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } - params := make(map[string][]string, len(c.route.Params)) for _, param := range c.route.Params { params[param] = append(params[param], c.Params(param)) @@ -1276,10 +1272,6 @@ func (c *Ctx) QueryFloat(key string, defaultValue ...float64) float64 { // QueryParser binds the query string to a struct. func (c *Ctx) QueryParser(out interface{}) error { - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } - data := make(map[string][]string) var err error @@ -1339,10 +1331,6 @@ func parseParamSquareBrackets(k string) (string, error) { // ReqHeaderParser binds the request header strings to a struct. func (c *Ctx) ReqHeaderParser(out interface{}) error { - if c.app.config.DefaultValueParser { - utils.SetDefaultValues(out) - } - data := make(map[string][]string) c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { k := c.app.getString(key) @@ -1361,7 +1349,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 { diff --git a/ctx_test.go b/ctx_test.go index fb3c5e6e8dc..d31fcee9711 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -991,34 +991,170 @@ func Test_Ctx_CookieParser(t *testing.T) { utils.AssertEqual(t, 0, len(empty.Courses)) } -func Test_Ctx_CookieParserWithDefaultParser(t *testing.T) { +func Test_Ctx_ParserWithDefaultValues(t *testing.T) { t.Parallel() + // setup app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - type Cookie struct { - Name string ` default:"doe"` - Class int `default:"10"` - Courses []string + + 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"` } - c.Request().Header.Set("Cookie", "courses=maths,english") - cookie := new(Cookie) - // correct test cases - utils.AssertEqual(t, nil, c.CookieParser(cookie)) - utils.AssertEqual(t, "doe", cookie.Name) - utils.AssertEqual(t, 10, cookie.Class) - utils.AssertEqual(t, 2, len(cookie.Courses)) + withValues := func(tt *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + testStruct := new(TestStruct) - // wrong test cases - empty := new(Cookie) - c.Request().Header.Set("Cookie", "name") - c.Request().Header.Set("Cookie", "class") - c.Request().Header.Set("Cookie", "courses") - utils.AssertEqual(t, nil, c.CookieParser(cookie)) - utils.AssertEqual(t, "", empty.Name) - utils.AssertEqual(t, 0, empty.Class) - utils.AssertEqual(t, 0, len(empty.Courses)) + utils.AssertEqual(tt, nil, actionFn(c, testStruct)) + utils.AssertEqual(tt, "foo", testStruct.Name) + utils.AssertEqual(tt, 111, testStruct.Class) + utils.AssertEqual(tt, "bar", testStruct.NameWithDefault) + utils.AssertEqual(tt, 222, testStruct.ClassWithDefault) + } + withoutValues := func(tt *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + testStruct := new(TestStruct) + + utils.AssertEqual(tt, nil, actionFn(c, testStruct)) + utils.AssertEqual(tt, "", testStruct.Name) + utils.AssertEqual(tt, 0, testStruct.Class) + utils.AssertEqual(tt, "doe", testStruct.NameWithDefault) + utils.AssertEqual(tt, 10, testStruct.ClassWithDefault) + } + + t.Run("BodyParser:xml", func(tt *testing.T) { + tt.Run("withValues", func(ttt *testing.T) { + withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().SetBody([]byte(`foo111bar222`)) + return c.BodyParser(testStruct) + }) + }) + tt.Run("withoutValues", func(ttt *testing.T) { + withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().SetBody([]byte(``)) + return c.BodyParser(testStruct) + }) + }) + }) + t.Run("BodyParser:form", func(tt *testing.T) { + tt.Run("withValues", func(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, 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(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, 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(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, 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(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + return c.CookieParser(testStruct) + }) + }) + }) + t.Run("QueryParser", func(tt *testing.T) { + tt.Run("withValues", func(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + return c.QueryParser(testStruct) + }) + }) + }) + t.Run("ParamsParser", func(tt *testing.T) { + tt.Run("withValues", func(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + c.route = &Route{Params: []string{}} + c.values = [30]string{} + return c.ParamsParser(testStruct) + }) + }) + }) + t.Run("ReqHeaderParser", func(tt *testing.T) { + tt.Run("withValues", func(ttt *testing.T) { + withValues(ttt, 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(ttt *testing.T) { + withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + return c.ReqHeaderParser(testStruct) + }) + }) + }) } // go test -run Test_Ctx_CookieParserUsingTag -v @@ -1067,48 +1203,6 @@ func Test_Ctx_CookieParserUsingTag(t *testing.T) { utils.AssertEqual(t, 3, len(ac.Dates)) } -func Test_Ctx_CookieParserUsingTagWithDefaultValues(t *testing.T) { - t.Parallel() - app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - type Cook struct { - ID int `cookie:"id" default:"10"` - Name string `cookie:"name"` - Courses []string `cookie:"courses"` - Enrolled bool `cookie:"student" default:"true"` - Fees float32 `cookie:"fee" default:"45.78"` - Grades []uint8 `cookie:"score"` - } - cookie1 := new(Cook) - cookie1.Name = cookieName - utils.AssertEqual(t, cookieName, cookie1.Name) - - c.Request().Header.Set("Cookie", "name=Joey") - c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics") - c.Request().Header.Set("Cookie", "score=7,6,10") - utils.AssertEqual(t, nil, c.CookieParser(cookie1)) - utils.AssertEqual(t, "Joey", cookie1.Name) - utils.AssertEqual(t, true, cookie1.Enrolled) - utils.AssertEqual(t, float32(45.78), cookie1.Fees) - utils.AssertEqual(t, []uint8{7, 6, 10}, cookie1.Grades) - - type RequiredCookie struct { - House string `cookie:"house,required"` - } - rc := new(RequiredCookie) - utils.AssertEqual(t, "failed to decode: house is empty", c.CookieParser(rc).Error()) - - type ArrayCookie struct { - Dates []int - } - - ac := new(ArrayCookie) - c.Request().Header.Set("Cookie", "dates[]=7,6,10") - utils.AssertEqual(t, nil, c.CookieParser(ac)) - utils.AssertEqual(t, 3, len(ac.Dates)) -} - // go test -run Test_Ctx_CookieParserSchema -v func Test_Ctx_CookieParser_Schema(t *testing.T) { t.Parallel() @@ -1176,32 +1270,31 @@ func Benchmark_Ctx_CookieParser(b *testing.B) { utils.AssertEqual(b, nil, err) } -func Benchmark_Ctx_CookieParserWithDefaultValues(b *testing.B) { - app := New(Config{EnableSplittingOnParsers: true}) +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 Cook struct { - ID int `cookie:"id" default:"10"` - Name string `cookie:"name"` - Courses []string `cookie:"courses" default:"maths,english, chemistry, physics"` - Enrolled bool `cookie:"student"` - Fees float32 `cookie:"fee"` - Grades []uint8 `cookie:"score"` + type TestStruct struct { + Name string + NameWithDefault string `json:"name2" default:"doe"` + NameWithDefaultNoValue string `json:"name3" default:"doe"` } - cookie1 := new(Cook) - cookie1.Name = cookieName - - c.Request().Header.Set("Cookie", "name=Joey") - c.Request().Header.Set("Cookie", "student=true") - c.Request().Header.Set("Cookie", "fee=45.78") - c.Request().Header.Set("Cookie", "score=7,6,10") + 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.CookieParser(cookie1) + 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 @@ -4633,104 +4726,6 @@ func Test_Ctx_QueryParserUsingTag(t *testing.T) { utils.AssertEqual(t, 2, len(aq.Data)) } -// go test -run Test_Ctx_QueryParserWithDefaultValues -v -func Test_Ctx_QueryParserWithDefaultValues(t *testing.T) { - t.Parallel() - app := New(Config{DefaultValueParser: true, EnableSplittingOnParsers: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - type Query struct { - IntValue int `query:"intValue" default:"10"` - IntSlice []int `query:"intSlice"` - StringValue string `query:"stringValue" default:"defaultStr"` - StringSlice []string `query:"stringSlice"` - BoolValue bool `query:"boolValue" default:"true"` - BoolSlice []bool `query:"boolSlice"` - FloatValue float64 `query:"floatValue" default:"3.14"` - FloatSlice []float64 `query:"floatSlice"` - } - - // Test with multiple values - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("intValue=20&intSlice=1,2,3&stringValue=test&stringSlice=a,b,c&boolValue=false&boolSlice=true,false,true&floatValue=6.28&floatSlice=1.1,2.2,3.3") - cq := new(Query) - utils.AssertEqual(t, nil, c.QueryParser(cq)) - utils.AssertEqual(t, 20, cq.IntValue) - utils.AssertEqual(t, 3, len(cq.IntSlice)) - utils.AssertEqual(t, "test", cq.StringValue) - utils.AssertEqual(t, 3, len(cq.StringSlice)) - utils.AssertEqual(t, false, cq.BoolValue) - utils.AssertEqual(t, 3, len(cq.BoolSlice)) - utils.AssertEqual(t, 6.28, cq.FloatValue) - utils.AssertEqual(t, 3, len(cq.FloatSlice)) - - // Test with missing values (should use defaults) - c.Request().URI().SetQueryString("intSlice=4,5,6&stringSlice=d,e,f&boolSlice=false,true,false&floatSlice=4.4,5.5,6.6") - cq = new(Query) - utils.AssertEqual(t, nil, c.QueryParser(cq)) - utils.AssertEqual(t, 10, cq.IntValue) // default value - utils.AssertEqual(t, 3, len(cq.IntSlice)) - utils.AssertEqual(t, "defaultStr", cq.StringValue) // default value - utils.AssertEqual(t, 3, len(cq.StringSlice)) - utils.AssertEqual(t, true, cq.BoolValue) // default value - utils.AssertEqual(t, 3, len(cq.BoolSlice)) - utils.AssertEqual(t, 3.14, cq.FloatValue) // default value - utils.AssertEqual(t, 3, len(cq.FloatSlice)) - - // Test with empty query string - empty := new(Query) - c.Request().URI().SetQueryString("") - utils.AssertEqual(t, nil, c.QueryParser(empty)) - utils.AssertEqual(t, 10, empty.IntValue) // default value - utils.AssertEqual(t, "defaultStr", empty.StringValue) // default value - utils.AssertEqual(t, true, empty.BoolValue) // default value - utils.AssertEqual(t, 3.14, empty.FloatValue) // default value - utils.AssertEqual(t, 0, len(empty.IntSlice)) - utils.AssertEqual(t, 0, len(empty.StringSlice)) - utils.AssertEqual(t, 0, len(empty.BoolSlice)) - utils.AssertEqual(t, 0, len(empty.FloatSlice)) - type Query2 struct { - Bool bool `query:"bool"` - ID int `query:"id"` - Name string `query:"name"` - Hobby string `query:"hobby"` - FavouriteDrinks []string `query:"favouriteDrinks"` - Empty []string `query:"empty"` - Alloc []string `query:"alloc"` - No []int64 `query:"no"` - } - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") - q2 := new(Query2) - q2.Bool = true - q2.Name = "hello world 2" - utils.AssertEqual(t, nil, c.QueryParser(q2)) - utils.AssertEqual(t, "basketball,football", q2.Hobby) - utils.AssertEqual(t, true, q2.Bool) - utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten - utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) - var nilSlice []string - utils.AssertEqual(t, nilSlice, q2.Empty) - utils.AssertEqual(t, []string{""}, q2.Alloc) - utils.AssertEqual(t, []int64{1}, q2.No) - - type RequiredQuery struct { - Name string `query:"name,required"` - } - rq := new(RequiredQuery) - c.Request().URI().SetQueryString("") - utils.AssertEqual(t, "failed to decode: name is empty", c.QueryParser(rq).Error()) - - type ArrayQuery struct { - Data []string - } - aq := new(ArrayQuery) - c.Request().URI().SetQueryString("data[]=john&data[]=doe") - utils.AssertEqual(t, nil, c.QueryParser(aq)) - utils.AssertEqual(t, 2, len(aq.Data)) -} - // go test -run Test_Ctx_QueryParser -v func Test_Ctx_QueryParser_WithoutSplitting(t *testing.T) { t.Parallel() @@ -4980,72 +4975,6 @@ func Test_Ctx_ReqHeaderParser(t *testing.T) { utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) } -// go test -run Test_Ctx_ReqHeaderParserWithDefaultValues -v -func Test_Ctx_ReqHeaderParserWithDefaultValues(t *testing.T) { - t.Parallel() - app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - type Header struct { - ID int `default:"10"` - Name string `default:"defaultStr"` - Hobby []string `default:"golang,fiber,go"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - q := new(Header) - utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) - utils.AssertEqual(t, 3, 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)) - - type Header2 struct { - Bool bool `default:"true"` - ID int `default:"10"` - Name string `default:"defaultStr"` - Hobby string `default:"golang,fiber,go"` - FavouriteDrinks []string `default:"milo,coke,pepsi"` - Empty []string `default:""` - Alloc []string `default:""` - No []int64 `default:"1"` - } - - h2 := new(Header2) - h2.Bool = true - h2.Name = "hello world 3" - utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) - utils.AssertEqual(t, "golang,fiber,go", h2.Hobby) - utils.AssertEqual(t, true, h2.Bool) - utils.AssertEqual(t, "hello world 3", h2.Name) // check value get overwritten - utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - /* - here is deep equal issue with empty slice and nil slice - ctx_test.go:4837: - Test: Test_Ctx_ReqHeaderParserWithDefaultValues - Trace: ctx_test.go:4837 - Description: empty slice should be nil - Expect: [] ([]string) - Result: [] ([]string) - */ - reflect.DeepEqual(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, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) -} - // go test -run Test_Ctx_ReqHeaderParserUsingTag -v func Test_Ctx_ReqHeaderParserUsingTag(t *testing.T) { t.Parallel() @@ -5121,72 +5050,6 @@ func Test_Ctx_ReqHeaderParserUsingTag(t *testing.T) { utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) } -// go test -run Test_Ctx_ReqHeaderParserUsingTagWithDefaultValues -v -func Test_Ctx_ReqHeaderParserUsingTagWithDefaultValues(t *testing.T) { - t.Parallel() - app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - type Header struct { - ID int `reqHeader:"id" default:"1"` - Name string `reqHeader:"name" default:"John Doe"` - Hobby []string `reqHeader:"hobby" default:"golang,fiber,go"` - Address []string `reqHeader:"x-secure-address" default:"1st,2st"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - q := new(Header) - utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) - utils.AssertEqual(t, 3, len(q.Hobby)) - utils.AssertEqual(t, 2, len(q.Address)) - - type Header2 struct { - Bool bool `reqHeader:"bool" default:"true"` - ID int `reqHeader:"id" default:"2"` - Name string `reqHeader:"name" default:"John Doe"` - Hobby string `reqHeader:"hobby" default:"go,fiber"` - FavouriteDrinks []string `reqHeader:"favouriteDrinks" default:"milo,coke,pepsi"` - Empty []string `reqHeader:"empty" default:""` - Alloc []string `reqHeader:"alloc" default:""` - No []int64 `reqHeader:"no" default:"1"` - } - - 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 4" - 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 - /* - here is deep equal issue with empty slice and nil slice - Description: empty slice should be nil - Expect: [] ([]string) - Result: [] ([]string) - */ - reflect.DeepEqual(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, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) -} - // go test -run Test_Ctx_ReqHeaderParser -v func Test_Ctx_ReqHeaderParser_WithoutSplitting(t *testing.T) { t.Parallel() @@ -5481,61 +5344,6 @@ func Benchmark_Ctx_QueryParser(b *testing.B) { utils.AssertEqual(b, nil, c.QueryParser(q)) } -// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValues -benchmem -count=4 -func Benchmark_Ctx_QueryParserWithDefaultValues(b *testing.B) { - app := New(Config{DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - - // Query struct with 2 default values - type Query struct { - ID int `default:"4"` - Name string `default:"john"` - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("hobby=basketball&hobby=football") - q := new(Query) - b.ReportAllocs() - b.ResetTimer() - - var err error - for n := 0; n < b.N; n++ { - err = c.QueryParser(q) - } - - utils.AssertEqual(b, nil, err) - utils.AssertEqual(b, nil, c.QueryParser(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParserWithDefaultValuesForSlices -benchmem -count=4 -func Benchmark_Ctx_QueryParserWithDefaultValuesForSlices(b *testing.B) { - app := New(Config{DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - - // Query struct slices with default values - type Query2 struct { - ID int - Name string - Hobby []int `default:"1,2,3"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("hobby=1&hobby=2") - q2 := new(Query2) - b.ReportAllocs() - b.ResetTimer() - - var err2 error - for n := 0; n < b.N; n++ { - err2 = c.QueryParser(q2) - } - utils.AssertEqual(b, nil, err2) - utils.AssertEqual(b, nil, c.QueryParser(q2)) -} - // go test -v -run=^$ -bench=Benchmark_Ctx_parseQuery -benchmem -count=4 func Benchmark_Ctx_parseQuery(b *testing.B) { app := New() @@ -5622,35 +5430,6 @@ func Benchmark_Ctx_ReqHeaderParser(b *testing.B) { utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) } -// go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParserWithDefaultValues -benchmem -count=4 -func Benchmark_Ctx_ReqHeaderParserWithDefaultValues(b *testing.B) { - app := New(Config{EnableSplittingOnParsers: true, DefaultValueParser: true}) - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - type ReqHeader struct { - ID int `default:"4"` - Name string `default:"john"` - Hobby []string `default:"basketball,football"` - } - 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() - - var err error - for n := 0; n < b.N; n++ { - err = c.ReqHeaderParser(q) - } - utils.AssertEqual(b, nil, err) - utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) -} - // go test -run Test_Ctx_BodyStreamWriter func Test_Ctx_BodyStreamWriter(t *testing.T) { t.Parallel() From a163888108026e9de9b7c891df470fe66941be79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Thu, 4 Jan 2024 17:14:24 +0100 Subject: [PATCH 23/28] Query, Header Default Value Tag Feature #2699 :green_heart: fix lint --- ctx_test.go | 92 +++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/ctx_test.go b/ctx_test.go index d31fcee9711..9a3534ab07f 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1003,39 +1003,41 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" reqHeader:"class2" default:"10"` } - withValues := func(tt *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) { + 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(tt, nil, actionFn(c, testStruct)) - utils.AssertEqual(tt, "foo", testStruct.Name) - utils.AssertEqual(tt, 111, testStruct.Class) - utils.AssertEqual(tt, "bar", testStruct.NameWithDefault) - utils.AssertEqual(tt, 222, testStruct.ClassWithDefault) + 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(tt *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) { + 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(tt, nil, actionFn(c, testStruct)) - utils.AssertEqual(tt, "", testStruct.Name) - utils.AssertEqual(tt, 0, testStruct.Class) - utils.AssertEqual(tt, "doe", testStruct.NameWithDefault) - utils.AssertEqual(tt, 10, testStruct.ClassWithDefault) + 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(tt *testing.T) { - tt.Run("withValues", func(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(`foo111bar222`)) return c.BodyParser(testStruct) }) }) - tt.Run("withoutValues", func(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + t.Run("withoutValues", func(t *testing.T) { + withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationXML) c.Request().SetBody([]byte(``)) return c.BodyParser(testStruct) @@ -1043,15 +1045,15 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) t.Run("BodyParser:form", func(tt *testing.T) { - tt.Run("withValues", func(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + tt.Run("withoutValues", func(t *testing.T) { + withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationForm) c.Request().SetBody([]byte(``)) @@ -1060,15 +1062,15 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) t.Run("BodyParser:json", func(tt *testing.T) { - tt.Run("withValues", func(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + tt.Run("withoutValues", func(t *testing.T) { + withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationJSON) c.Request().SetBody([]byte(`{}`)) @@ -1077,8 +1079,8 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) t.Run("BodyParser:multiform", func(tt *testing.T) { - tt.Run("withValues", func(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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"`) @@ -1086,8 +1088,8 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { return c.BodyParser(testStruct) }) }) - tt.Run("withoutValues", func(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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"`) @@ -1098,41 +1100,41 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) t.Run("CookieParser", func(tt *testing.T) { - tt.Run("withValues", func(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + tt.Run("withoutValues", func(t *testing.T) { + withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.route = &Route{Params: []string{}} c.values = [30]string{} return c.ParamsParser(testStruct) @@ -1140,8 +1142,8 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) t.Run("ReqHeaderParser", func(tt *testing.T) { - tt.Run("withValues", func(ttt *testing.T) { - withValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + 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") @@ -1149,8 +1151,8 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { return c.ReqHeaderParser(testStruct) }) }) - tt.Run("withoutValues", func(ttt *testing.T) { - withoutValues(ttt, func(c *Ctx, testStruct *TestStruct) error { + tt.Run("withoutValues", func(t *testing.T) { + withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { return c.ReqHeaderParser(testStruct) }) }) From 96c39d433b77c21b9d01d4e2d2cc634e8e099c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Thu, 4 Jan 2024 17:26:26 +0100 Subject: [PATCH 24/28] Query, Header Default Value Tag Feature #2699 :green_heart: fix ParamsParser for empty route params --- ctx.go | 5 ++++- ctx_test.go | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ctx.go b/ctx.go index bcb9a830c75..bc23c234374 100644 --- a/ctx.go +++ b/ctx.go @@ -1093,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) } diff --git a/ctx_test.go b/ctx_test.go index 9a3534ab07f..0e657e284e4 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1134,11 +1134,18 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) 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) { From d1e34877fdb12f236ad9c4fb8703da4254d44ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Thu, 4 Jan 2024 17:38:14 +0100 Subject: [PATCH 25/28] Query, Header Default Value Tag Feature #2699 :memo: update Config docs --- docs/api/fiber.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 63033f45b31..49ec9819cd0 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -47,22 +47,22 @@ app := fiber.New(fiber.Config{ | ColorScheme | [`Colors`](https://github.com/gofiber/fiber/blob/master/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares. | [`DefaultColors`](https://github.com/gofiber/fiber/blob/master/color.go) | | CompressedFileSuffix | `string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `".fiber.gz"` | | Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | +| DefaultValueParser | `bool` | When set to true, this will enable support for default value tags across all parsers, allowing the specification of default values for inputs in query, body, route, request header, and cookie parsers. | `false` | | DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | | DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | | DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | | DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | | DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | | DisableStartupMessage | `bool` | When set to true, it will not print out debug information | `false` | -| ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | | EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma seperated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | | EnablePrintRoutes | `bool` | EnablePrintRoutes enables print all routes with their method, path, name and handler.. | `false` | | EnableSplittingOnParsers | `bool` | EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.

For example, you can use it to parse multiple values from a query parameter like this: `/api?foo=bar,baz == foo[]=bar&foo[]=baz` | `false` | | EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | | ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | +| ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | | GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | | IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | | Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | -| DefaultValueParser | `bool` | 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. | `false` | | JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | | JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | | Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | From a091b239be0dd1a1590eedb12779a0898c92e01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Thu, 4 Jan 2024 18:04:16 +0100 Subject: [PATCH 26/28] Query, Header Default Value Tag Feature #2699 :memo: update docs --- docs/api/ctx.md | 85 ++++++++++++++++------------------------------- docs/api/fiber.md | 39 +++++++++++++++++++++- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 600aac1ad87..fdc2fc64399 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -313,6 +313,10 @@ app.Post("/", func(c *fiber.Ctx) error { // curl -X POST "http://localhost:3000/?name=john&pass=doe" ``` +:::note +It supports the `DefaultValueParser` feature, which allows for the assignment of default values to struct fields when specific request parameters are missing. For more details, see the [default values documentation](./fiber.md#default-values). +::: + > _Returned value is only valid within the handler. Do not store any references. > Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) @@ -466,6 +470,13 @@ app.Get("/", func(c *fiber.Ctx) error { // curl.exe --cookie "name=Joseph; age=23; job=true" http://localhost:8000/ ``` +:::note +It supports the `DefaultValueParser` feature, which allows for the assignment of default values to struct fields when specific request parameters are missing. For more details, see the [default values documentation](./fiber.md#default-values). +::: + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + ## Cookies Get cookie value by key, you could pass an optional default value that will be returned if the cookie key does not exist. @@ -1126,7 +1137,7 @@ This method is equivalent of using `atoi` with ctx.Params ## ParamsParser -This method is similar to BodyParser, but for path parameters. It is important to use the struct tag "params". For example, if you want to parse a path parameter with a field called Pass, you would use a struct field of params:"pass" +This method is similar to [BodyParser](ctx.md#bodyparser), but for path parameters. It is important to use the struct tag "params". For example, if you want to parse a path parameter with a field called Pass, you would use a struct field of params:"pass" ```go title="Signature" func (c *Ctx) ParamsParser(out interface{}) error @@ -1144,6 +1155,13 @@ app.Get("/user/:id", func(c *fiber.Ctx) error { ``` +:::note +It supports the `DefaultValueParser` feature, which allows for the assignment of default values to struct fields when specific request parameters are missing. For more details, see the [default values documentation](./fiber.md#default-values). +::: + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + ## Path Contains the path part of the request URL. Optionally, you could override the path by passing a string. For internal redirects, you might want to call [RestartRouting](ctx.md#restartrouting) instead of [Next](ctx.md#next). @@ -1398,35 +1416,12 @@ app.Get("/", func(c *fiber.Ctx) error { // curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" ``` -### Default Values with QueryParser -You can also assign default values to struct fields if the query parameter is not provided in the request. To do this, use the default struct tag alongside the query tag. - -```go title="WithDefaultValues" -type PersonWithDefaults struct { - Name string `query:"name" default:"DefaultName"` - Pass string `query:"pass" default:"DefaultPass"` - Products []string `query:"products" default:"defaultProduct1,defaultProduct2"` -} - -app.Get("/defaults", func(c *fiber.Ctx) error { - p := new(PersonWithDefaults) - - if err := c.QueryParser(p); err != nil { - return err - } - - log.Println(p.Name) // Will print "DefaultName" if name is not provided in the query - log.Println(p.Pass) // Will print "DefaultPass" if pass is not provided in the query - log.Println(p.Products) // Will print [defaultProduct1, defaultProduct2] if products is not provided in the query - - // ... -}) -// Run tests with the following curl command - -// curl "http://localhost:3000/defaults" -// This will use the default values since no query parameters are provided +:::note +It supports the `DefaultValueParser` feature, which allows for the assignment of default values to struct fields when specific request parameters are missing. For more details, see the [default values documentation](./fiber.md#default-values). +::: -``` +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) ## Range @@ -1601,35 +1596,13 @@ app.Get("/", func(c *fiber.Ctx) error { // curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" ``` -### Default Values with ReqHeaderParser -You can also assign default values to struct fields if the request header is not provided in the request. To do this, use the default struct tag alongside the reqHeader tag. - -```go title="WithDefaultValues" -type PersonWithDefaults struct { - Name string `reqHeader:"name" default:"DefaultName"` - Pass string `reqHeader:"pass" default:"DefaultPass"` - Products []string `reqHeader:"products" default:"defaultProduct1,defaultProduct2"` -} - -app.Get("/defaults", func(c *fiber.Ctx) error { - p := new(PersonWithDefaults) - - if err := c.ReqHeaderParser(p); err != nil { - return err - } - - log.Println(p.Name) // Will print "DefaultName" if name is not provided in the request header - log.Println(p.Pass) // Will print "DefaultPass" if pass is not provided in the request header - log.Println(p.Products) // Will print [defaultProduct1, defaultProduct2] if products is not provided in the request header - - // ... -}) -// Run tests with the following curl command +:::note +It supports the `DefaultValueParser` feature, which allows for the assignment of default values to struct fields when specific request parameters are missing. For more details, see the [default values documentation](./fiber.md#default-values). +::: -// curl "http://localhost:3000/defaults" -// This will use the default values since no request headers are provided +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) -``` ## Response Response return the [\*fasthttp.Response](https://godoc.org/github.com/valyala/fasthttp#Response) pointer diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 49ec9819cd0..d811d8ef110 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -37,7 +37,7 @@ app := fiber.New(fiber.Config{ // ... ``` -**Config fields** +### Config fields | Property | Type | Description | Default | |------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| @@ -83,6 +83,43 @@ app := fiber.New(fiber.Config{ | WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | | XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | + +#### Default Values + +This [config](#config-fields) feature `DefaultValueParser` enables you to assign default values to struct fields for situations where certain parameters are missing in requests. The `default` struct tag can be utilized across various parsers, including [BodyParser](ctx.md#BodyParser), [CookieParser](ctx.md#CookieParser), [QueryParser](ctx.md#QueryParser), [ParamsParser](ctx.md#ParamsParser), and [ReqHeaderParser](ctx.md#ReqHeaderParser). This tag works in tandem with the respective parser tag to ensure default values are set when specific parameters are not provided. + +```go title="WithDefaultValues" +// Example demonstrating the use of default values in query parser +type PersonWithDefaults struct { + Name string `query:"name" default:"DefaultName"` + Pass string `query:"pass" default:"DefaultPass"` + Products []string `query:"products" default:"defaultProduct1,defaultProduct2"` +} + +app := fiber.New(fiber.Config{ + DefaultValueParser: true, +}) + +app.Get("/defaults", func(c *fiber.Ctx) error { + p := new(PersonWithDefaults) + + if err := c.QueryParser(p); err != nil { + return err + } + + log.Println(p.Name) // Will print "DefaultName" if name is not provided in the query + log.Println(p.Pass) // Will print "DefaultPass" if pass is not provided in the query + log.Println(p.Products) // Will print [defaultProduct1, defaultProduct2] if products is not provided in the query + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/defaults" +// This will use the default values since no query parameters are provided + +``` + ## NewError NewError creates a new HTTPError instance with an optional message. From fc9ed0312e630192bf888832db5bfff4fa8bf22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Fri, 5 Jan 2024 11:59:33 +0100 Subject: [PATCH 27/28] Query, Header Default Value Tag Feature #2699 add more test Cases --- ctx.go | 31 ++++++++++---------- ctx_test.go | 66 +++++++++++++++++++++++++++++-------------- utils/default.go | 43 ++++++++++++++++++---------- utils/default_test.go | 59 ++++++++++++++++++++++++++++++++------ 4 files changed, 139 insertions(+), 60 deletions(-) diff --git a/ctx.go b/ctx.go index bc23c234374..31752132b2b 100644 --- a/ctx.go +++ b/ctx.go @@ -389,6 +389,21 @@ func (c *Ctx) BodyParser(out interface{}) error { // Parse body accordingly switch { + case strings.HasSuffix(ctype, "json"): + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + return c.app.config.JSONDecoder(c.Body(), out) + + case strings.HasPrefix(ctype, MIMETextXML), strings.HasPrefix(ctype, MIMEApplicationXML): + if c.app.config.DefaultValueParser { + utils.SetDefaultValues(out) + } + if err := xml.Unmarshal(c.Body(), out); err != nil { + return fmt.Errorf("failed to unmarshal: %w", err) + } + return nil + case strings.HasPrefix(ctype, MIMEApplicationForm): formBody := make(map[string][]string) var err error @@ -414,29 +429,15 @@ func (c *Ctx) BodyParser(out interface{}) error { formBody[k] = append(formBody[k], v) } }) - 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, 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"): - return c.app.config.JSONDecoder(c.Body(), out) - } } // No suitable content type found diff --git a/ctx_test.go b/ctx_test.go index 0e657e284e4..c153754a1ad 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1029,7 +1029,9 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { } t.Run("BodyParser:xml", func(t *testing.T) { + t.Parallel() t.Run("withValues", func(t *testing.T) { + t.Parallel() withValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationXML) c.Request().SetBody([]byte(`foo111bar222`)) @@ -1037,6 +1039,7 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) t.Run("withoutValues", func(t *testing.T) { + t.Parallel() withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationXML) c.Request().SetBody([]byte(``)) @@ -1044,15 +1047,18 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) }) - t.Run("BodyParser:form", func(tt *testing.T) { - tt.Run("withValues", func(t *testing.T) { + t.Run("BodyParser:form", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationForm) c.Request().SetBody([]byte(``)) @@ -1061,15 +1067,18 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) }) - t.Run("BodyParser:json", func(tt *testing.T) { - tt.Run("withValues", func(t *testing.T) { + t.Run("BodyParser:json", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.SetContentType(MIMEApplicationJSON) c.Request().SetBody([]byte(`{}`)) @@ -1078,8 +1087,10 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) }) - t.Run("BodyParser:multiform", func(tt *testing.T) { - tt.Run("withValues", func(t *testing.T) { + t.Run("BodyParser:multiform", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() 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) @@ -1088,7 +1099,8 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { return c.BodyParser(testStruct) }) }) - tt.Run("withoutValues", func(t *testing.T) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { body := []byte("--b\n\n--b--") c.Request().SetBody(body) @@ -1099,41 +1111,50 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) }) - t.Run("CookieParser", func(tt *testing.T) { - tt.Run("withValues", func(t *testing.T) { + t.Run("CookieParser", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("QueryParser", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("ParamsParser", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() 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) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() // no params declared in route withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { c.route = &Route{Params: []string{}} @@ -1148,8 +1169,10 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { }) }) }) - t.Run("ReqHeaderParser", func(tt *testing.T) { - tt.Run("withValues", func(t *testing.T) { + t.Run("ReqHeaderParser", func(t *testing.T) { + t.Parallel() + t.Run("withValues", func(t *testing.T) { + t.Parallel() withValues(t, func(c *Ctx, testStruct *TestStruct) error { c.Request().Header.Add("name", "foo") c.Request().Header.Add("name2", "bar") @@ -1158,7 +1181,8 @@ func Test_Ctx_ParserWithDefaultValues(t *testing.T) { return c.ReqHeaderParser(testStruct) }) }) - tt.Run("withoutValues", func(t *testing.T) { + t.Run("withoutValues", func(t *testing.T) { + t.Parallel() withoutValues(t, func(c *Ctx, testStruct *TestStruct) error { return c.ReqHeaderParser(testStruct) }) diff --git a/utils/default.go b/utils/default.go index 4dfa8da3952..7ec51b19658 100644 --- a/utils/default.go +++ b/utils/default.go @@ -8,7 +8,19 @@ import ( "sync" ) +var ( + mu sync.RWMutex + structCache = make(map[reflect.Type][]reflect.StructField) +) + +const ( + tagName = "default" +) + func tagHandlers(field reflect.Value, tagValue string) { + mu.Lock() + defer mu.Unlock() + //nolint:exhaustive // We don't need to handle all types switch field.Kind() { case reflect.String: @@ -39,6 +51,9 @@ func tagHandlers(field reflect.Value, tagValue string) { } func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.Type) { + mu.Lock() + defer mu.Unlock() + items := strings.Split(tagValue, ",") slice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, len(items)) for _, item := range items { @@ -93,39 +108,37 @@ func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.T field.Set(slice) } -var ( - mu sync.Mutex - structCache = make(map[reflect.Type][]reflect.StructField) -) - func getFieldsWithDefaultTag(t reflect.Type) []reflect.StructField { - if fields, ok := structCache[t]; ok { + mu.RLock() + fields, ok := structCache[t] + mu.RUnlock() + if ok { return fields } - var fields []reflect.StructField + var newFields []reflect.StructField for i := 0; i < t.NumField(); i++ { field := t.Field(i) - if _, ok := field.Tag.Lookup("default"); ok { - fields = append(fields, field) + if _, ok := field.Tag.Lookup(tagName); ok { + newFields = append(newFields, field) } } mu.Lock() - structCache[t] = fields + structCache[t] = newFields mu.Unlock() - return fields + return newFields } func SetDefaultValues(out interface{}) { - val := reflect.ValueOf(out).Elem() - typ := val.Type() + elem := reflect.ValueOf(out).Elem() + typ := elem.Type() fields := getFieldsWithDefaultTag(typ) for _, fieldInfo := range fields { - field := val.FieldByName(fieldInfo.Name) - tagValue := fieldInfo.Tag.Get("default") + field := elem.FieldByName(fieldInfo.Name) + tagValue := fieldInfo.Tag.Get(tagName) tagHandlers(field, tagValue) } } diff --git a/utils/default_test.go b/utils/default_test.go index f3b241dc040..9fbc469ab29 100644 --- a/utils/default_test.go +++ b/utils/default_test.go @@ -33,15 +33,57 @@ func TestSetDefaultValues(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() SetDefaultValues(tt.input) - if !reflect.DeepEqual(tt.input, tt.expected) { - t.Errorf("got %v, want %v", tt.input, tt.expected) - } + AssertEqual(t, tt.input, tt.expected, "SetDefaultValues failed") }) } } +type TestSecondLevelStruct struct { + Word string `default:"Bar"` + Number int `default:"42"` +} + +type TestFirstLevelStruct struct { + Word string `default:"Foo"` + Number int `default:"42"` + DeepStruct *TestSecondLevelStruct + DeepSlice []*TestSecondLevelStruct +} + +func TestDeepSetDefaultValues(t *testing.T) { + t.Parallel() + subject := &TestFirstLevelStruct{DeepStruct: &TestSecondLevelStruct{}, DeepSlice: []*TestSecondLevelStruct{&TestSecondLevelStruct{}, &TestSecondLevelStruct{}}} + SetDefaultValues(subject) + + AssertEqual( + t, + &TestFirstLevelStruct{ + Word: "Foo", + Number: 42, + DeepStruct: &TestSecondLevelStruct{ + Word: "Bar", + Number: 42, + }, + DeepSlice: []*TestSecondLevelStruct{ + &TestSecondLevelStruct{ + Word: "Bar", + Number: 42, + }, + &TestSecondLevelStruct{ + Word: "Bar", + Number: 42, + }, + }, + }, + subject, + "SetDefaultValues failed", + ) +} + func TestTagHandlers(t *testing.T) { + t.Parallel() tests := []struct { name string field reflect.Value @@ -88,15 +130,15 @@ func TestTagHandlers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tagHandlers(tt.field, tt.tagValue) - if !reflect.DeepEqual(tt.field.Interface(), tt.expected) { - t.Errorf("got %v, want %v", tt.field.Interface(), tt.expected) - } + AssertEqual(t, tt.field.Interface(), tt.expected, "tagHandlers failed") }) } } func TestSetDefaultForSlice(t *testing.T) { + t.Parallel() tests := []struct { name string field reflect.Value @@ -136,10 +178,9 @@ func TestSetDefaultForSlice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() setDefaultForSlice(tt.field, tt.tagValue, tt.elemType) - if !reflect.DeepEqual(tt.field.Interface(), tt.expected) { - t.Errorf("got %v, want %v", tt.field.Interface(), tt.expected) - } + AssertEqual(t, tt.field.Interface(), tt.expected, "setDefaultForSlice failed") }) } } From 01026005301a5e398755e9c8fca232e28f0711ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Fri, 5 Jan 2024 12:03:22 +0100 Subject: [PATCH 28/28] Query, Header Default Value Tag Feature #2699 add more test Cases --- utils/default.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/utils/default.go b/utils/default.go index 7ec51b19658..44721a915a8 100644 --- a/utils/default.go +++ b/utils/default.go @@ -51,8 +51,9 @@ func tagHandlers(field reflect.Value, tagValue string) { } func setDefaultForSlice(field reflect.Value, tagValue string, elemType reflect.Type) { - mu.Lock() - defer mu.Unlock() + // TODO: produce deadlock because of mutex hirachy + //mu.Lock() + //defer mu.Unlock() items := strings.Split(tagValue, ",") slice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, len(items))