Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
97 changes: 83 additions & 14 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

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

Expand Down Expand Up @@ -58,11 +59,77 @@ type Route struct {
routeParser routeParser // Parameter parser

// Data for routing
use bool // USE matches path prefixes
mount bool // Indicated a mounted app on a specific route
star bool // Path equals '*'
root bool // Path equals '/'
autoHead bool // Automatically generated HEAD route
use bool // USE matches path prefixes
mount bool // Indicated a mounted app on a specific route
star bool // Path equals '*'
root bool // Path equals '/'
autoHead bool // Automatically generated HEAD route
caseSensitive bool // Whether parameter matching is case-sensitive
}

// URL generates a URL from the route path and parameters.
// This method fills in the route parameters with the provided values.
// Parameter matching respects the app's CaseSensitive configuration:
// case-insensitive by default, case-sensitive when CaseSensitive is true.
//
// 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
}

var (
val any
found bool
)

// Prefer an exact parameter name match
if val, found = params[segment.ParamName]; !found && !r.caseSensitive {
// Fall back to a case-insensitive match
for key := range params {
if utils.EqualFold(key, segment.ParamName) {
val = params[key]
found = true
break
}
}
}

// For greedy parameters, fall back to generic greedy keys
if !found && segment.IsGreedy {
for _, greedyParam := range greedyParameters {
if val, found = params[string(greedyParam)]; found {
break
}
}
}

if found {
_, err := buf.WriteString(utils.ToString(val))
if err != nil {
return "", fmt.Errorf("failed to write string: %w", err)
}
}
}

return buf.String(), nil
}

func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
Expand Down Expand Up @@ -376,11 +443,12 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route {
func (*App) copyRoute(route *Route) *Route {
return &Route{
// Router booleans
use: route.use,
mount: route.mount,
star: route.star,
root: route.root,
autoHead: route.autoHead,
use: route.use,
mount: route.mount,
star: route.star,
root: route.root,
autoHead: route.autoHead,
caseSensitive: route.caseSensitive,

// Path data
path: route.path,
Expand Down Expand Up @@ -554,10 +622,11 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
isRoot := pathClean == "/"

route := Route{
use: isUse,
mount: isMount,
star: isStar,
root: isRoot,
use: isUse,
mount: isMount,
star: isStar,
root: isRoot,
caseSensitive: app.config.CaseSensitive,

path: pathClean,
routeParser: parsedPretty,
Expand Down
149 changes: 149 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2360,3 +2360,152 @@ 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 insensitive default", 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("case sensitive", func(t *testing.T) {
t.Parallel()
app := New(Config{CaseSensitive: true})
app.Get("/user/:name", emptyHandler).Name("User")

route := app.GetRoute("User")
// Exact case match succeeds
url, err := route.URL(Map{"name": "fiber"})
require.NoError(t, err)
require.Equal(t, "/user/fiber", url)
// Different case does not match when CaseSensitive is true
url, err = route.URL(Map{"Name": "fiber"})
require.NoError(t, err)
require.Equal(t, "/user/", 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