Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

3 changes: 3 additions & 0 deletions res_interface_gen.go

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

46 changes: 46 additions & 0 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
package fiber

import (
"bytes"
"fmt"
"slices"
"sync/atomic"

"github.com/gofiber/utils/v2"
utilsstrings "github.com/gofiber/utils/v2/strings"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
)

Expand Down Expand Up @@ -65,6 +67,50 @@ type Route struct {
autoHead bool // Automatically generated HEAD route
}

// URL generates a URL from the route path and parameters.
// This method fills in the route parameters with the provided values.
// Parameter matching is case-insensitive by default to match Fiber's default behavior.
//
// Example:
//
// app.Get("/user/:name/:id", handler).Name("user")
// route := app.GetRoute("user")
// url, err := route.URL(Map{"name": "john", "id": "123"})
// // Returns: "/user/john/123"
func (r *Route) URL(params Map) (string, error) {
if r.Path == "" {
return "", ErrNotFound
}

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

for _, segment := range r.routeParser.segs {
if !segment.IsParam {
_, err := buf.WriteString(segment.Const)
if err != nil {
return "", fmt.Errorf("failed to write string: %w", err)
}
continue
}

for key, val := range params {
// Use case-insensitive matching to support both CaseSensitive and non-CaseSensitive configs
isSame := key == segment.ParamName || utils.EqualFold(key, segment.ParamName)
isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0
if isSame || isGreedy {
_, err := buf.WriteString(utils.ToString(val))
if err != nil {
return "", fmt.Errorf("failed to write string: %w", err)
}
break
}
}
}

return buf.String(), nil
}

func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
// root detectionPath check
if r.root && len(detectionPath) == 1 && detectionPath[0] == '/' {
Expand Down
133 changes: 133 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2360,3 +2360,136 @@ func Benchmark_Router_GitHub_API_Parallel(b *testing.B) {
require.True(b, match)
}
}

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

t.Run("simple parameter", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/user/:name", emptyHandler).Name("User")

route := app.GetRoute("User")
url, err := route.URL(Map{"name": "fiber"})
require.NoError(t, err)
require.Equal(t, "/user/fiber", url)
})

t.Run("multiple parameters", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/user/:name/:id", emptyHandler).Name("UserID")

route := app.GetRoute("UserID")
url, err := route.URL(Map{"name": "john", "id": "123"})
require.NoError(t, err)
require.Equal(t, "/user/john/123", url)
})

t.Run("wildcard parameters", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/:phone/*/send/*", emptyHandler).Name("SendSms")

route := app.GetRoute("SendSms")
url, err := route.URL(Map{
"phone": "23456789",
"*1": "sms",
"*2": "test-msg",
})
require.NoError(t, err)
require.Equal(t, "/23456789/sms/send/test-msg", url)
})

t.Run("single wildcard parameter", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/:phone/*/send", emptyHandler).Name("SendSms")

route := app.GetRoute("SendSms")
url, err := route.URL(Map{
"phone": "23456789",
"*": "sms",
})
require.NoError(t, err)
require.Equal(t, "/23456789/sms/send", url)
})

t.Run("case sensitivity", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/user/:name", emptyHandler).Name("User")

route := app.GetRoute("User")
// Case-insensitive by default (matching Fiber's default behavior)
url, err := route.URL(Map{"Name": "fiber"})
require.NoError(t, err)
require.Equal(t, "/user/fiber", url)
})

t.Run("empty route", func(t *testing.T) {
t.Parallel()
route := Route{}
url, err := route.URL(Map{"name": "fiber"})
require.Error(t, err)
require.Equal(t, ErrNotFound, err)
require.Empty(t, url)
})

t.Run("no parameters", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/static/path", emptyHandler).Name("Static")

route := app.GetRoute("Static")
url, err := route.URL(Map{})
require.NoError(t, err)
require.Equal(t, "/static/path", url)
})

t.Run("missing parameter value", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/user/:name/:id", emptyHandler).Name("UserID")

route := app.GetRoute("UserID")
url, err := route.URL(Map{"name": "john"})
require.NoError(t, err)
// Missing id parameter results in empty placeholder
require.Equal(t, "/user/john/", url)
})

t.Run("extra parameters ignored", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/user/:name", emptyHandler).Name("User")

route := app.GetRoute("User")
url, err := route.URL(Map{"name": "fiber", "extra": "ignored"})
require.NoError(t, err)
require.Equal(t, "/user/fiber", url)
})

t.Run("numeric parameter values", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/item/:id", emptyHandler).Name("Item")

route := app.GetRoute("Item")
url, err := route.URL(Map{"id": 42})
require.NoError(t, err)
require.Equal(t, "/item/42", url)
})

t.Run("complex path with mixed content", func(t *testing.T) {
t.Parallel()
app := New()
app.Get("/api/v1/users/:userId/posts/:postId/comments", emptyHandler).Name("Comments")

route := app.GetRoute("Comments")
url, err := route.URL(Map{"userId": "user123", "postId": "post456"})
require.NoError(t, err)
require.Equal(t, "/api/v1/users/user123/posts/post456/comments", url)
})
}
Loading