Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into multipart-nested
Browse files Browse the repository at this point in the history
# Conflicts:
#	binder/mapping.go
  • Loading branch information
ReneWerner87 committed Dec 31, 2024
2 parents 8c28b9a + d0e767f commit eeffa22
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 111 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
uses: golangci/golangci-lint-action@v6
with:
# NOTE: Keep this in sync with the version from .golangci.yml
version: v1.62.0
version: v1.62.2
2 changes: 1 addition & 1 deletion .github/workflows/markdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4

- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@v18
uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: |
**/*.md
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ markdown:
## lint: 🚨 Run lint checks
.PHONY: lint
lint:
go run github.com/golangci/golangci-lint/cmd/[email protected].0 run ./...
go run github.com/golangci/golangci-lint/cmd/[email protected].2 run ./...

## test: 🚦 Execute all tests
.PHONY: test
Expand Down
16 changes: 14 additions & 2 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,10 @@ func (app *App) handleTrustedProxy(ipAddress string) {
// Note: It doesn't allow adding new methods, only customizing exist methods.
func (app *App) NewCtxFunc(function func(app *App) CustomCtx) {
app.newCtxFunc = function

if app.server != nil {
app.server.Handler = app.customRequestHandler
}
}

// RegisterCustomConstraint allows to register custom constraint.
Expand Down Expand Up @@ -868,7 +872,11 @@ func (app *App) Config() Config {
func (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476
// prepare the server for the start
app.startupProcess()
return app.requestHandler

if app.newCtxFunc != nil {
return app.customRequestHandler
}
return app.defaultRequestHandler
}

// Stack returns the raw router stack.
Expand Down Expand Up @@ -1057,7 +1065,11 @@ func (app *App) init() *App {
}

// fasthttp server settings
app.server.Handler = app.requestHandler
if app.newCtxFunc != nil {
app.server.Handler = app.customRequestHandler
} else {
app.server.Handler = app.defaultRequestHandler
}
app.server.Name = app.config.ServerHeader
app.server.Concurrency = app.config.Concurrency
app.server.NoDefaultDate = app.config.DisableDefaultDate
Expand Down
39 changes: 29 additions & 10 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,32 +581,51 @@ func Test_App_Use_StrictRouting(t *testing.T) {

func Test_App_Add_Method_Test(t *testing.T) {
t.Parallel()
defer func() {
if err := recover(); err != nil {
require.Equal(t, "add: invalid http method JANE\n", fmt.Sprintf("%v", err))
}
}()

methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
app := New(Config{
RequestMethods: methods,
})

app.Add([]string{"JOHN"}, "/doe", testEmptyHandler)
app.Add([]string{"JOHN"}, "/john", testEmptyHandler)

resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
resp, err := app.Test(httptest.NewRequest("JOHN", "/john", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")

resp, err = app.Test(httptest.NewRequest(MethodGet, "/doe", nil))
resp, err = app.Test(httptest.NewRequest(MethodGet, "/john", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusMethodNotAllowed, resp.StatusCode, "Status code")

resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/doe", nil))
resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/john", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusNotImplemented, resp.StatusCode, "Status code")

app.Add([]string{"JANE"}, "/doe", testEmptyHandler)
// Add a new method
require.Panics(t, func() {
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
})
}

func Test_App_All_Method_Test(t *testing.T) {
t.Parallel()

methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
app := New(Config{
RequestMethods: methods,
})

// Add a new method with All
app.All("/doe", testEmptyHandler)

resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")

// Add a new method
require.Panics(t, func() {
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
})
}

// go test -run Test_App_GETOnly
Expand Down
1 change: 0 additions & 1 deletion binder/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ func parseToMap(ptr any, data map[string][]string) error {
newMap[k] = ""
continue
}

newMap[k] = v[len(v)-1]
}
default:
Expand Down
3 changes: 3 additions & 0 deletions ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,35 @@ func Test_Ctx_CustomCtx(t *testing.T) {
require.Equal(t, "prefix_v3", string(body))
}

// go test -run Test_Ctx_CustomCtx
func Test_Ctx_CustomCtx_and_Method(t *testing.T) {
t.Parallel()

// Create app with custom request methods
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
app := New(Config{
RequestMethods: methods,
})

// Create custom context
app.NewCtxFunc(func(app *App) CustomCtx {
return &customCtx{
DefaultCtx: *NewDefaultCtx(app),
}
})

// Add route with custom method
app.Add([]string{"JOHN"}, "/doe", testEmptyHandler)
resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")

// Add a new method
require.Panics(t, func() {
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
})
}

// go test -run Test_Ctx_Accepts_EmptyAccept
func Test_Ctx_Accepts_EmptyAccept(t *testing.T) {
t.Parallel()
Expand Down
2 changes: 1 addition & 1 deletion docs/middleware/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
id: session
---

# Session Middleware for [Fiber](https://github.com/gofiber/fiber)
# Session

The `session` middleware provides session management for Fiber applications, utilizing the [Storage](https://github.com/gofiber/storage) package for multi-database support via a unified interface. By default, session data is stored in memory, but custom storage options are easily configurable (see examples below).

Expand Down
34 changes: 25 additions & 9 deletions middleware/idempotency/locker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,58 @@ type Locker interface {
Unlock(key string) error
}

type countedLock struct {
mu sync.Mutex
locked int
}

type MemoryLock struct {
keys map[string]*sync.Mutex
keys map[string]*countedLock
mu sync.Mutex
}

func (l *MemoryLock) Lock(key string) error {
l.mu.Lock()
mu, ok := l.keys[key]
lock, ok := l.keys[key]
if !ok {
mu = new(sync.Mutex)
l.keys[key] = mu
lock = new(countedLock)
l.keys[key] = lock
}
lock.locked++
l.mu.Unlock()

mu.Lock()
lock.mu.Lock()

return nil
}

func (l *MemoryLock) Unlock(key string) error {
l.mu.Lock()
mu, ok := l.keys[key]
l.mu.Unlock()
lock, ok := l.keys[key]
if !ok {
// This happens if we try to unlock an unknown key
l.mu.Unlock()
return nil
}
l.mu.Unlock()

mu.Unlock()
lock.mu.Unlock()

l.mu.Lock()
lock.locked--
if lock.locked <= 0 {
// This happens if countedLock is used to Lock and Unlock the same number of times
// So, we can delete the key to prevent memory leak
delete(l.keys, key)
}
l.mu.Unlock()

return nil
}

func NewMemoryLock() *MemoryLock {
return &MemoryLock{
keys: make(map[string]*sync.Mutex),
keys: make(map[string]*countedLock),
}
}

Expand Down
66 changes: 66 additions & 0 deletions middleware/idempotency/locker_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package idempotency_test

import (
"strconv"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -59,3 +61,67 @@ func Test_MemoryLock(t *testing.T) {
require.NoError(t, err)
}
}

func Benchmark_MemoryLock(b *testing.B) {
keys := make([]string, b.N)
for i := range keys {
keys[i] = strconv.Itoa(i)
}

lock := idempotency.NewMemoryLock()

b.ResetTimer()

for i := 0; i < b.N; i++ {
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
}

func Benchmark_MemoryLock_Parallel(b *testing.B) {
// In order to prevent using repeated keys I pre-allocate keys
keys := make([]string, 1_000_000)
for i := range keys {
keys[i] = strconv.Itoa(i)
}

b.Run("UniqueKeys", func(b *testing.B) {
lock := idempotency.NewMemoryLock()
var keyI atomic.Int32
b.RunParallel(func(p *testing.PB) {
for p.Next() {
i := int(keyI.Add(1)) % len(keys)
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
})
})

b.Run("RepeatedKeys", func(b *testing.B) {
lock := idempotency.NewMemoryLock()
var keyI atomic.Int32
b.RunParallel(func(p *testing.PB) {
for p.Next() {
// Division by 3 ensures that index will be repreated exactly 3 times
i := int(keyI.Add(1)) / 3 % len(keys)
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
})
})
}
12 changes: 9 additions & 3 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,10 +620,16 @@ func GetTrimmedParam(param string) string {

// RemoveEscapeChar remove escape characters
func RemoveEscapeChar(word string) string {
if strings.IndexByte(word, escapeChar) != -1 {
return strings.ReplaceAll(word, string(escapeChar), "")
b := []byte(word)
dst := 0
for src := 0; src < len(b); src++ {
if b[src] == '\\' {
continue
}
b[dst] = b[src]
dst++
}
return word
return string(b[:dst])
}

func getParamConstraintType(constraintPart string) TypeConstraint {
Expand Down
Loading

0 comments on commit eeffa22

Please sign in to comment.