-
-
Notifications
You must be signed in to change notification settings - Fork 2k
🔥 feat: Add URL() method to Route for generating URLs with parameters #4195
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
base: main
Are you sure you want to change the base?
Changes from all commits
daa7dc9
db31cdf
26faa1d
6b37d8d
434c6f2
9fef978
be74496
6d6740e
6e56f99
2e241ed
a7f3f1f
9b47da0
389a0f6
3384a5b
afc8c17
5ac4c3f
3c20541
a44230b
55afb07
9f828e7
77044e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
| ) | ||
|
|
||
|
|
@@ -58,11 +59,122 @@ 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 | ||
| } | ||
|
|
||
| var ( | ||
| defaultGreedyParameterKeys = []string{"*", "+"} | ||
| preferredWildcardGreedyParameters = []string{"*", "+"} | ||
| preferredPlusGreedyParameters = []string{"+", "*"} | ||
| ) | ||
|
|
||
| // 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") | ||
| // url, err := app.GetRoute("user").URL(Map{"name": "john", "id": "123"}) | ||
| // // Returns: "/user/john/123" | ||
| // | ||
| //nolint:gocritic // hugeParam: app.GetRoute returns a value, so URL must be callable on that value directly. | ||
| func (r Route) URL(params Map) (string, error) { | ||
| if r.Path == "" { | ||
| return "", ErrNotFound | ||
| } | ||
|
|
||
| return buildRouteURL(&r, params) | ||
| } | ||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // buildRouteURL generates a URL from route segments and parameters. | ||
| // This shared helper is used by both Route.URL() and DefaultRes.getLocationFromRoute() | ||
| // to ensure consistent URL generation behavior across APIs. | ||
| // | ||
| // Parameter resolution uses a deterministic three-step lookup: | ||
| // 1. Exact key match on segment.ParamName | ||
| // 2. Case-insensitive fallback picking the lexicographically-smallest matching key (when !caseSensitive) | ||
| // 3. Greedy parameter fallback for wildcard (*) and plus (+) parameters | ||
| func buildRouteURL(route *Route, params Map) (string, error) { | ||
| if len(route.routeParser.segs) == 0 { | ||
| return route.Path, nil | ||
| } | ||
|
|
||
| buf := bytebufferpool.Get() | ||
| defer bytebufferpool.Put(buf) | ||
|
|
||
| for _, segment := range route.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 && !route.caseSensitive { | ||
| // Fall back to a case-insensitive match using a deterministic winner | ||
| var matchedKey string | ||
| foundMatch := false | ||
| for key := range params { | ||
| if utils.EqualFold(key, segment.ParamName) && (!foundMatch || key < matchedKey) { | ||
| matchedKey = key | ||
| foundMatch = true | ||
| } | ||
| } | ||
| if foundMatch { | ||
| val = params[matchedKey] | ||
| found = true | ||
| } | ||
|
Comment on lines
+128
to
+140
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a bug in the case-insensitive parameter matching logic. The To fix this, you should use a local boolean flag within this block to track if a match has been found. // Fall back to a case-insensitive match using a deterministic winner
var matchedKey string
foundMatch := false
for key := range params {
if utils.EqualFold(key, segment.ParamName) && (!foundMatch || key < matchedKey) {
matchedKey = key
foundMatch = true
}
}
if foundMatch {
val = params[matchedKey]
found = true
}
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot Analyse and apply this fix if it makes sense.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // For greedy parameters, fall back to generic greedy keys | ||
| if !found && segment.IsGreedy { | ||
| for _, greedyKey := range preferredGreedyParameters(segment.ParamName) { | ||
| if val, found = params[greedyKey]; found { | ||
| break | ||
| } | ||
| } | ||
| } | ||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if found { | ||
| _, err := buf.WriteString(utils.ToString(val)) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to write string: %w", err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return buf.String(), nil | ||
| } | ||
|
|
||
| // preferredGreedyParameters returns the generic greedy fallback lookup order | ||
| // for a route parameter name. | ||
| // Parameter names starting with '+' prefer '+' before '*', names starting with | ||
| // '*' prefer '*' before '+', and all other names fall back to the default order. | ||
| func preferredGreedyParameters(paramName string) []string { | ||
| if paramName != "" { | ||
| switch paramName[0] { | ||
| case plusParam: | ||
| return preferredPlusGreedyParameters | ||
| case wildcardParam: | ||
| return defaultGreedyParameterKeys | ||
| } | ||
| } | ||
|
|
||
| return defaultGreedyParameterKeys | ||
| } | ||
|
|
||
| func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { | ||
|
|
@@ -369,18 +481,20 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { | |
| route.routeParser = parseRoute(prettyPath, app.customConstraints...) | ||
| route.root = false | ||
| route.star = false | ||
| route.caseSensitive = app.config.CaseSensitive | ||
|
|
||
| return 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, | ||
|
|
||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Path data | ||
| path: route.path, | ||
|
|
@@ -554,10 +668,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, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.