Skip to content

Commit

Permalink
Cookie parser (#2656)
Browse files Browse the repository at this point in the history
* prep for branching

* feature: added a cookie parser and tests appropriate tests

* ✨ feature: added a cookie parser and appropriate tests

* made correction to docs

* linted using gofumpt

* ctx_test linted, cookieParser schema added

* fix lint errors (Cookie parser #2656)

* removed extra lines, tested return values

---------

Co-authored-by: René Werner <rene.werner@verivox.com>
  • Loading branch information
joey1123455 and René Werner committed Oct 12, 2023
1 parent bb90fc1 commit e70b2e2
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 3 deletions.
41 changes: 38 additions & 3 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,23 @@ const (
// maxParams defines the maximum number of parameters per route.
const maxParams = 30

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

// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx
const userContextKey = "__local_user_context__"

var (
// decoderPoolMap helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance
// decoderPoolMap helps to improve BodyParser's, QueryParser's, CookieParser's and ReqHeaderParser's performance
decoderPoolMap = map[string]*sync.Pool{}
// tags is used to classify parser's pool
tags = []string{queryTag, bodyTag, reqHeaderTag, paramsTag}
tags = []string{queryTag, bodyTag, reqHeaderTag, paramsTag, cookieTag}
)

func init() {
Expand Down Expand Up @@ -502,6 +503,40 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string {
return defaultString(c.app.getString(c.fasthttp.Request.Header.Cookie(key)), defaultValue)
}

// CookieParser is used to bind cookies to a struct
func (c *Ctx) CookieParser(out interface{}) error {
data := make(map[string][]string)
var err error

// loop through all cookies
c.fasthttp.Request.Header.VisitAllCookie(func(key, val []byte) {
if err != nil {
return
}

k := c.app.getString(key)
v := c.app.getString(val)

if strings.Contains(k, "[") {
k, err = parseParamSquareBrackets(k)
}

if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, cookieTag) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
})
if err != nil {
return err
}

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

// Download transfers the file from path as an attachment.
// Typically, browsers will prompt the user for download.
// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog).
Expand Down
146 changes: 146 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,152 @@ func Benchmark_Ctx_Cookie(b *testing.B) {
utils.AssertEqual(b, "John=Doe; path=/; SameSite=Lax", app.getString(c.Response().Header.Peek("Set-Cookie")))
}

// go test -run Test_Ctx_CookieParser -v
func Test_Ctx_CookieParser(t *testing.T) {
t.Parallel()
app := New(Config{EnableSplittingOnParsers: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Cookie struct {
Name string
Class int
Courses []string
}
c.Request().Header.Set("Cookie", "name=doe")
c.Request().Header.Set("Cookie", "class=100")
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, 100, 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()
app := New(Config{EnableSplittingOnParsers: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Cook struct {
ID int `cookie:"id"`
Name string `cookie:"name"`
Courses []string `cookie:"courses"`
Enrolled bool `cookie:"student"`
Fees float32 `cookie:"fee"`
Grades []uint8 `cookie:"score"`
}
cookie1 := new(Cook)
cookie1.Name = "Joseph"
utils.AssertEqual(t, "Joseph", cookie1.Name)

c.Request().Header.Set("Cookie", "id=1")
c.Request().Header.Set("Cookie", "name=Joey")
c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics")
c.Request().Header.Set("Cookie", "student=true")
c.Request().Header.Set("Cookie", "fee=45.78")
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()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type result struct {
Maths int `cookie:"maths"`
English int `cookie:"english"`
}
type resStruct struct {
Name string `cookie:"name"`
Age int `cookie:"age"`
Result result `cookie:"result"`
}
res := &resStruct{
Name: "Joseph",
Age: 10,
Result: result{
Maths: 10,
English: 10,
},
}

// set cookie
c.Request().Header.Set("Cookie", "name=Joseph")
c.Request().Header.Set("Cookie", "age=10")
c.Request().Header.Set("Cookie", "result.maths=10")
c.Request().Header.Set("Cookie", "result.english=10")
hR := new(resStruct)
r := c.CookieParser(hR)

utils.AssertEqual(t, nil, r)
utils.AssertEqual(t, *res, *hR)
}

// go test -run Benchmark_Ctx_CookieParser -v
func Benchmark_Ctx_CookieParser(b *testing.B) {
app := New(Config{EnableSplittingOnParsers: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Cook struct {
ID int `cookie:"id"`
Name string `cookie:"name"`
Courses []string `cookie:"courses"`
Enrolled bool `cookie:"student"`
Fees float32 `cookie:"fee"`
Grades []uint8 `cookie:"score"`
}
cookie1 := new(Cook)
cookie1.Name = "Joseph"

c.Request().Header.Set("Cookie", "id=1")
c.Request().Header.Set("Cookie", "name=Joey")
c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics")
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()
Expand Down
32 changes: 32 additions & 0 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,38 @@ app.Get("/", func(c *fiber.Ctx) error {
})
```

## CookieParser

This method is similar to [BodyParser](ctx.md#bodyparser), but for cookie parameters.
It is important to use the struct tag "cookie". For example, if you want to parse a cookie with a field called Age, you would use a struct field of `cookie:"age"`.

```go title="Signature"
func (c *Ctx) CookieParser(out interface{}) error
```

```go title="Example"
// Field names should start with an uppercase letter
type Person struct {
Name string `cookie:"name"`
Age int `cookie:"age"`
Job bool `cookie:"job"`
}

app.Get("/", func(c *fiber.Ctx) error {
p := new(Person)

if err := c.CookieParser(p); err != nil {
return err
}

log.Println(p.Name) // Joseph
log.Println(p.Age) // 23
log.Println(p.Job) // true
})
// Run tests with the following curl command
// curl.exe --cookie "name=Joseph; age=23; job=true" http://localhost:8000/
```

## Cookies

Get cookie value by key, you could pass an optional default value that will be returned if the cookie key does not exist.
Expand Down

0 comments on commit e70b2e2

Please sign in to comment.