Repository: gorilla/mux
Pull request #676 (2022-05-18 16:15:43)

Add GetVarNames()
**Summary of Changes**

1. Added `r.GetVarNames()` function to list all vars a route might need in order to call `r.URL()`

amustaque97: I will review PR by this weekend. 
eh-steve: The reason for this is where routes are built up programmatically from a configuration, not hard coded by a developer. In this case, the application needs to know the registered vars to be able to ensure they are available to be injected (or retrieve them if necessary)

This may not be a common use case but we're using mux in this way.
eh-steve: I'm not sure I follow what you mean about choices - right now, there's no way of knowing which vars a route captures (on the basis that the caller might not be the one who built the route), and this adds a fairly non-intrusive way to expose that information.

An alternative choice for would be to force users to make another struct which embeds the mux route and keeps track of all var names by duplicating/wrapping a lot of functionality?

If we wanna freeze the API of this library that's fine, you can close this PR and I can use the fork?
amustaque97: > If we wanna freeze the API of this library that's fine

Not want to make any premature decision here. Could you please share the minimal code(building of URL programmatically) where the current PR method `GetVarNames` comes into play? Later we can discuss whether we should merge it or not.
eh-steve: Sure, so:

Imagine an application that makes HTTP requests based on variables coming from an inbound messages (e.g. from a kafka topic). It could take a set of YAML configurations like:

```yaml
endpoint:
  host_pattern: "www.{domain}.com"
  path_pattern: "/some/prefix/{group}/{item_id:[0-9]+}"
  method: "POST"
  queries:
    blah: "{some_data1}"
    something: "{some_data2}"
```
and builds up mux routes programatically using these configured endpoints.

Then when a payload comes in from an inbound message like:
```json
{
  "domain": "example",
  "group": "my_group",
  "item_id": 75,
  "some_data1": "123",
  "some_data2": "456",
  "other_irrelevant_data": {
    "massive_and_expensive_to_stringify": "..."
  }
}
```
It would be able to extract keys from the JSON payload (parsed as a `map[string]interface{}`) and use them as URL vars. Mux vars values need to be strings, so any non-string values would need to be `fmt.Sprint()`'ed (e.g. `item_id` in this example).

If your JSON payload contains any additional fields which aren't needed as route vars (e.g. `other_irrelevant_data` in this example, which contains the field `massive_and_expensive_to_stringify`), we would still need to stringify them all (which involves a needless allocation) and pass them into `route.URL(pairs...)`, where they would be ignored anyway.

By exposing `GetVarNames()`, it allows us to only pass variables which we know are required by the route, and to error early if those variables are not available, or don't adhere to a schema etc.

This is a simplified example compared to how we're actually using it, but it illustrates the point.

eh-steve: > To remove curly braces {} and slash / writing half of the function identical to newRouteRegexp: https://github.com/gorilla/mux/blob/master/regexp.go#L41 doesn't sounds like a good idea 

That's how I first started (on the calling side) before I realised it was a bad idea!
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gorilla/mux/pull/676?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) Report
> Merging [#676](https://app.codecov.io/gh/gorilla/mux/pull/676?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) (21c25ef) into [main](https://app.codecov.io/gh/gorilla/mux/commit/85123bf20e069b156415b871dea10517f6a8938a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) (85123bf) will **increase** coverage by `0.03%`.
> The diff coverage is `80.00%`.

```diff
@@            Coverage Diff             @@
##             main     #676      +/-   ##
==========================================
+ Coverage   78.01%   78.04%   +0.03%     
==========================================
  Files           5        5              
  Lines         887      902      +15     
==========================================
+ Hits          692      704      +12     
- Misses        140      142       +2     
- Partials       55       56       +1     
```


| [Files Changed](https://app.codecov.io/gh/gorilla/mux/pull/676?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) | Coverage Δ | |
|---|---|---|
| [route.go](https://app.codecov.io/gh/gorilla/mux/pull/676?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla#diff-cm91dGUuZ28=) | `68.93% <80.00%> (+0.47%)` | :arrow_up: |


coreydaley: @apoorvajagtap Can you please take a look at this an approve if it looks good?

Patch:
diff --git a/README.md b/README.md
index 9e49b24..87ada8e 100644
--- a/README.md
+++ b/README.md
@@ -366,6 +366,19 @@ url, err := r.Get("article").URL("subdomain", "news",
                                  "id", "42")
 ```
 
+To find all the required variables for a given route when calling `URL()`, the method `GetVarNames()` is available:
+```go
+r := mux.NewRouter()
+r.Host("{domain}").
+    Path("/{group}/{item_id}").
+    Queries("some_data1", "{some_data1}").
+    Queries("some_data2", "{some_data2}").
+    Name("article")
+
+// Will print [domain group item_id some_data1 some_data2] <nil>
+fmt.Println(r.Get("article").GetVarNames())
+
+```
 ### Walking Routes
 
 The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example,
diff --git a/route.go b/route.go
index cd85f4b..e8f11df 100644
--- a/route.go
+++ b/route.go
@@ -728,6 +728,25 @@ func (r *Route) GetHostTemplate() (string, error) {
 	return r.regexp.host.template, nil
 }
 
+// GetVarNames returns the names of all variables added by regexp matchers
+// These can be used to know which route variables should be passed into r.URL()
+func (r *Route) GetVarNames() ([]string, error) {
+	if r.err != nil {
+		return nil, r.err
+	}
+	var varNames []string
+	if r.regexp.host != nil {
+		varNames = append(varNames, r.regexp.host.varsN...)
+	}
+	if r.regexp.path != nil {
+		varNames = append(varNames, r.regexp.path.varsN...)
+	}
+	for _, regx := range r.regexp.queries {
+		varNames = append(varNames, regx.varsN...)
+	}
+	return varNames, nil
+}
+
 // prepareVars converts the route variable pairs into a map. If the route has a
 // BuildVarsFunc, it is invoked.
 func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {


Test patch:
diff --git a/example_route_vars_test.go b/example_route_vars_test.go
new file mode 100644
index 0000000..428c3b6
--- /dev/null
+++ b/example_route_vars_test.go
@@ -0,0 +1,35 @@
+package mux_test
+
+import (
+	"fmt"
+	"github.com/gorilla/mux"
+)
+
+// This example demonstrates building a dynamic URL using
+// required vars and values retrieve from another source
+func ExampleRoute_GetVarNames() {
+	r := mux.NewRouter()
+
+	route := r.Host("{domain}").
+		Path("/{group}/{item_id}").
+		Queries("some_data1", "{some_data1}").
+		Queries("some_data2_and_3", "{some_data2}.{some_data3}")
+
+	dataSource := func(key string) string {
+		return "my_value_for_" + key
+	}
+
+	varNames, _ := route.GetVarNames()
+
+	pairs := make([]string, 0, len(varNames)*2)
+
+	for _, varName := range varNames {
+		pairs = append(pairs, varName, dataSource(varName))
+	}
+
+	url, err := route.URL(pairs...)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(url.String())
+}
diff --git a/mux_test.go b/mux_test.go
index bd97d33..5898688 100644
--- a/mux_test.go
+++ b/mux_test.go
@@ -2879,6 +2879,40 @@ func TestContextMiddleware(t *testing.T) {
 	r.ServeHTTP(rec, req)
 }
 
+func TestGetVarNames(t *testing.T) {
+	r := NewRouter()
+
+	route := r.Host("{domain}").
+		Path("/{group}/{item_id}").
+		Queries("some_data1", "{some_data1}").
+		Queries("some_data2_and_3", "{some_data2}.{some_data3}")
+
+	// Order of vars in the slice is not guaranteed, so just check for existence
+	expected := map[string]bool{
+		"domain":     true,
+		"group":      true,
+		"item_id":    true,
+		"some_data1": true,
+		"some_data2": true,
+		"some_data3": true,
+	}
+
+	varNames, err := route.GetVarNames()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(varNames) != len(expected) {
+		t.Fatalf("expected %d names, got %d", len(expected), len(varNames))
+	}
+
+	for _, varName := range varNames {
+		if !expected[varName] {
+			t.Fatalf("got unexpected %s", varName)
+		}
+	}
+}
+
 // mapToPairs converts a string map to a slice of string pairs
 func mapToPairs(m map[string]string) []string {
 	var i int