Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Query, Header Default Value Tag Feature #2699

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 77 additions & 0 deletions ctx.go
Expand Up @@ -1278,6 +1278,8 @@
return err
}

setDefaultValues(out)

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

Expand Down Expand Up @@ -1344,6 +1346,81 @@
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))))
Fixed Show fixed Hide fixed
}
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()
Expand Down
153 changes: 153 additions & 0 deletions ctx_test.go
Expand Up @@ -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()
Expand Down Expand Up @@ -5061,6 +5159,61 @@ 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()
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()
Expand Down
30 changes: 30 additions & 0 deletions docs/api/ctx.md
Expand Up @@ -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.
Expand Down