Repository: labstack/echo
Pull request #2416 (2023-03-12 22:28:47)

Proper colon support in reverse
A small follow-up for https://github.com/labstack/echo/pull/1988 - add support of `\\:` into Reverse method of the router

codecov[bot]: ## [Codecov](https://codecov.io/gh/labstack/echo/pull/2416?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
> :exclamation: No coverage uploaded for pull request base (`master@ec642f7`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#section-missing-base-commit).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##             master    #2416   +/-   ##
=========================================
  Coverage          ?   92.87%           
=========================================
  Files             ?       39           
  Lines             ?     4521           
  Branches          ?        0           
=========================================
  Hits              ?     4199           
  Misses            ?      234           
  Partials          ?       88           
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/2416?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) | Coverage Δ | |
|---|---|---|
| [router.go](https://codecov.io/gh/labstack/echo/pull/2416?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-cm91dGVyLmdv) | `97.80% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/labstack/echo/pull/2416?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).

mihard: Hi @aldas , 
Could you please review my PR?
mihard: @aldas pls check again

Patch:
diff --git a/router.go b/router.go
index 597660d..50a6385 100644
--- a/router.go
+++ b/router.go
@@ -159,7 +159,12 @@ func (r *Router) Reverse(name string, params ...interface{}) string {
 	for _, route := range r.routes {
 		if route.Name == name {
 			for i, l := 0, len(route.Path); i < l; i++ {
-				if (route.Path[i] == ':' || route.Path[i] == '*') && n < ln {
+				hasBackslash := route.Path[i] == '\\'
+				if hasBackslash && i+1 < l && route.Path[i+1] == ':' {
+					i++ // backslash before colon escapes that colon. in that case skip backslash
+				}
+				if n < ln && (route.Path[i] == '*' || (!hasBackslash && route.Path[i] == ':')) {
+					// in case of `*` wildcard or `:` (unescaped colon) param we replace everything till next slash or end of path
 					for ; i < l && route.Path[i] != '/'; i++ {
 					}
 					uri.WriteString(fmt.Sprintf("%v", params[n]))


Test patch:
diff --git a/echo_test.go b/echo_test.go
index 2f66c8c..eab25db 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -1517,26 +1517,97 @@ func TestEcho_OnAddRouteHandler(t *testing.T) {
 }
 
 func TestEchoReverse(t *testing.T) {
-	e := New()
-	dummyHandler := func(Context) error { return nil }
+	var testCases = []struct {
+		name          string
+		whenRouteName string
+		whenParams    []interface{}
+		expect        string
+	}{
+		{
+			name:          "ok,static with no params",
+			whenRouteName: "/static",
+			expect:        "/static",
+		},
+		{
+			name:          "ok,static with non existent param",
+			whenRouteName: "/static",
+			whenParams:    []interface{}{"missing param"},
+			expect:        "/static",
+		},
+		{
+			name:          "ok, wildcard with no params",
+			whenRouteName: "/static/*",
+			expect:        "/static/*",
+		},
+		{
+			name:          "ok, wildcard with params",
+			whenRouteName: "/static/*",
+			whenParams:    []interface{}{"foo.txt"},
+			expect:        "/static/foo.txt",
+		},
+		{
+			name:          "ok, single param without param",
+			whenRouteName: "/params/:foo",
+			expect:        "/params/:foo",
+		},
+		{
+			name:          "ok, single param with param",
+			whenRouteName: "/params/:foo",
+			whenParams:    []interface{}{"one"},
+			expect:        "/params/one",
+		},
+		{
+			name:          "ok, multi param without params",
+			whenRouteName: "/params/:foo/bar/:qux",
+			expect:        "/params/:foo/bar/:qux",
+		},
+		{
+			name:          "ok, multi param with one param",
+			whenRouteName: "/params/:foo/bar/:qux",
+			whenParams:    []interface{}{"one"},
+			expect:        "/params/one/bar/:qux",
+		},
+		{
+			name:          "ok, multi param with all params",
+			whenRouteName: "/params/:foo/bar/:qux",
+			whenParams:    []interface{}{"one", "two"},
+			expect:        "/params/one/bar/two",
+		},
+		{
+			name:          "ok, multi param + wildcard with all params",
+			whenRouteName: "/params/:foo/bar/:qux/*",
+			whenParams:    []interface{}{"one", "two", "three"},
+			expect:        "/params/one/bar/two/three",
+		},
+		{
+			name:          "ok, backslash is not escaped",
+			whenRouteName: "/backslash",
+			whenParams:    []interface{}{"test"},
+			expect:        `/a\b/test`,
+		},
+		{
+			name:          "ok, escaped colon verbs",
+			whenRouteName: "/params:customVerb",
+			whenParams:    []interface{}{"PATCH"},
+			expect:        `/params:PATCH`,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := New()
+			dummyHandler := func(Context) error { return nil }
+
+			e.GET("/static", dummyHandler).Name = "/static"
+			e.GET("/static/*", dummyHandler).Name = "/static/*"
+			e.GET("/params/:foo", dummyHandler).Name = "/params/:foo"
+			e.GET("/params/:foo/bar/:qux", dummyHandler).Name = "/params/:foo/bar/:qux"
+			e.GET("/params/:foo/bar/:qux/*", dummyHandler).Name = "/params/:foo/bar/:qux/*"
+			e.GET("/a\\b/:x", dummyHandler).Name = "/backslash"
+			e.GET("/params\\::customVerb", dummyHandler).Name = "/params:customVerb"
 
-	e.GET("/static", dummyHandler).Name = "/static"
-	e.GET("/static/*", dummyHandler).Name = "/static/*"
-	e.GET("/params/:foo", dummyHandler).Name = "/params/:foo"
-	e.GET("/params/:foo/bar/:qux", dummyHandler).Name = "/params/:foo/bar/:qux"
-	e.GET("/params/:foo/bar/:qux/*", dummyHandler).Name = "/params/:foo/bar/:qux/*"
-
-	assert.Equal(t, "/static", e.Reverse("/static"))
-	assert.Equal(t, "/static", e.Reverse("/static", "missing param"))
-	assert.Equal(t, "/static/*", e.Reverse("/static/*"))
-	assert.Equal(t, "/static/foo.txt", e.Reverse("/static/*", "foo.txt"))
-
-	assert.Equal(t, "/params/:foo", e.Reverse("/params/:foo"))
-	assert.Equal(t, "/params/one", e.Reverse("/params/:foo", "one"))
-	assert.Equal(t, "/params/:foo/bar/:qux", e.Reverse("/params/:foo/bar/:qux"))
-	assert.Equal(t, "/params/one/bar/:qux", e.Reverse("/params/:foo/bar/:qux", "one"))
-	assert.Equal(t, "/params/one/bar/two", e.Reverse("/params/:foo/bar/:qux", "one", "two"))
-	assert.Equal(t, "/params/one/bar/two/three", e.Reverse("/params/:foo/bar/:qux/*", "one", "two", "three"))
+			assert.Equal(t, tc.expect, e.Reverse(tc.whenRouteName, tc.whenParams...))
+		})
+	}
 }
 
 func TestEchoReverseHandleHostProperly(t *testing.T) {