Repository: gorilla/mux (go)

Issue:
[FEATURE] Route metadata
### Is there an existing feature request for this?

- [X] I have searched the existing feature requests

### Is your feature request related to a problem? Please describe.

Currently it is difficult to do authorization on routes based on claim requirements, unless we do the check inside the http handler.
Coming from a C# .NET world, there it is easy because it support attributes like an [Authorize] attribute.

As workaround i added a unique name to each route, and created a authorization mapping between the route name and the authorization policy.

**Middleware**
```
var routePolicies map[string]string
var authPolicies map[string]authorization.Policy

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		route := mux.CurrentRoute(r)
		policyName, ok := routePolicies[route.GetName()]
		if !ok {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}

		policy, ok := authPolicies[policyName]
		if !ok || !policy.MeetsRequirements(r.Context()) {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}

		next.ServeHTTP(w, r)
	})
}

```

**Setup**
```
// Register the route
r.HandleFunc("/v1/product", api.GetProductsHandlerV1).Methods(http.MethodGet).Name("product_api:GetProductsHandlerV1")
r.HandleFunc("/v1/product/{id}", api.GetProductByIdHandlerV1).Methods(http.MethodGet).Name("product_api:GetProductByIdHandlerV1")

// Set the route policies
routePolicies["product_api:GetProductsHandlerV1"] = "product.read"
routePolicies["product_api:GetProductByIdHandlerV1"] = "product.read"

// Create the policies
authPolicies["product.read"] = authorization.NewPolicy(
    "product.read",
    NewUserRequirement(),
    NewScopeRequirement("product.read"),
)
```

### Describe the solution that you would like.

To make it easy to create an authorization middleware with limited code, it would be nice to have metadata on a route.

Example:
```
r.HandleFunc("/v1/product", api.GetProductsHandlerV1).Methods(http.MethodGet).Metadata("authPolicy", "product.read")
r.HandleFunc("/v1/product/{id}", api.GetProductByIdHandlerV1).Methods(http.MethodGet).Metadata("authPolicy", "product.read")

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		route := mux.CurrentRoute(r)
		policy, ok := authPolicies[route.GetMetadata()["authPolicy"]]
		if !ok || !policy.MeetsRequirements(r.Context()) {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}

		next.ServeHTTP(w, r)
	})
}
```

Route metadata could be used for many other solutions, where u want to set some properties on a route to use inside the handler or middlewares

### Describe alternatives you have considered.

_No response_

### Anything else?

_No response_

Pull request:
Feature route metadata
<!--
     For Work In Progress Pull Requests, please use the Draft PR feature,
     see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.

     For a timely review/response, please avoid force-pushing additional
     commits if your PR already received reviews or comments.

     Before submitting a Pull Request, please ensure that you have:
     - 📖 Read the Contributing guide: https://github.com/gorilla/.github/blob/main/CONTRIBUTING.md
     - 📖 Read the Code of Conduct: https://github.com/gorilla/.github/blob/main/CODE_OF_CONDUCT.md

     - Provide tests for your changes.
     - Use descriptive commit messages.
	 - Comment your code where appropriate.
	 - Squash your commits
     - Update any related documentation.

     - Add gorilla/pull-request-reviewers as a Reviewer
-->

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [x] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Go Version Update
- [ ] Dependency Update

## Description
Added metadata to route. as proposed in #750 
Documentation will be added uppon request.

## Related Tickets & Documents
<!--
For pull requests that relate or close an issue, please include them
below.  We like to follow [Github's guidance on linking issues to pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).

For example having the text: "closes #1234" would connect the current pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #750 
- Closes #750 

## Added/updated tests?

- [x] Yes
- [ ] No, and this is why: _please replace this line with details on why tests
      have not been included_
- [ ] I need help with writing tests

## Run verifications and test

- [x] `make verify` is passing
- [x] `make test` is passing


Patch:
diff --git a/mux.go b/mux.go
index 24920ac..c9396e9 100644
--- a/mux.go
+++ b/mux.go
@@ -24,6 +24,8 @@ var (
 	// Do not run this function from `init()` in importable packages.
 	// Changing this value is not safe for concurrent use.
 	RegexpCompileFunc = regexp.Compile
+	// ErrMetadataKeyNotFound is returned when the specified metadata key is not present in the map
+	ErrMetadataKeyNotFound = errors.New("key not found in metadata")
 )
 
 // NewRouter returns a new router instance.
diff --git a/route.go b/route.go
index 3480419..d10401e 100644
--- a/route.go
+++ b/route.go
@@ -24,6 +24,9 @@ type Route struct {
 	// Error resulted from building a route.
 	err error
 
+	// The meta data associated with this route
+	metadata map[any]any
+
 	// "global" reference to all named routes
 	namedRoutes map[string]*Route
 
@@ -125,6 +128,49 @@ func (r *Route) BuildOnly() *Route {
 	return r
 }
 
+// MetaData -------------------------------------------------------------------
+
+// Metadata is used to set metadata on a route
+func (r *Route) Metadata(key any, value any) *Route {
+	if r.metadata == nil {
+		r.metadata = make(map[any]any)
+	}
+
+	r.metadata[key] = value
+	return r
+}
+
+// GetMetadata returns the metadata map for route
+func (r *Route) GetMetadata() map[any]any {
+	return r.metadata
+}
+
+// MetadataContains returns whether or not the key is present in the metadata map
+func (r *Route) MetadataContains(key any) bool {
+	_, ok := r.metadata[key]
+	return ok
+}
+
+// GetMetadataValue returns the value of a specific key in the metadata map. If the key is not present in the map mux.ErrMetadataKeyNotFound is returned
+func (r *Route) GetMetadataValue(key any) (any, error) {
+	value, ok := r.metadata[key]
+	if !ok {
+		return nil, ErrMetadataKeyNotFound
+	}
+
+	return value, nil
+}
+
+// GetMetadataValueOr returns the value of a specific key in the metadata map. If the key is not present in the metadata the fallback value is returned
+func (r *Route) GetMetadataValueOr(key any, fallbackValue any) any {
+	value, ok := r.metadata[key]
+	if !ok {
+		return fallbackValue
+	}
+
+	return value
+}
+
 // Handler --------------------------------------------------------------------
 
 // Handler sets a handler for the route.


Test patch:
diff --git a/route_test.go b/route_test.go
index 5aa35d1..283c3ad 100644
--- a/route_test.go
+++ b/route_test.go
@@ -1,7 +1,9 @@
 package mux
 
 import (
+	"errors"
 	"net/http"
+	"reflect"
 	"regexp"
 	"sync"
 	"testing"
@@ -64,3 +66,89 @@ func testNewRouter(_ testing.TB, handler http.Handler) {
 	r.Queries("orgID", "{orgID:[0-9]*?}")
 	r.Host("{subdomain}.domain.com")
 }
+
+func TestRouteMetadata(t *testing.T) {
+	router := NewRouter()
+	rw := NewRecorder()
+
+	expectedMap := make(map[any]any)
+	expectedMap["key"] = "value"
+
+	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		route := CurrentRoute(r)
+		metadata := route.GetMetadata()
+
+		if !reflect.DeepEqual(metadata, expectedMap) {
+			println(metadata)
+			t.Fatalf("Expected map does not equal the metadata map")
+		}
+
+	}).Metadata("key", "value")
+
+	router.HandleFunc("/single-value", func(w http.ResponseWriter, r *http.Request) {
+		route := CurrentRoute(r)
+		value, err := route.GetMetadataValue("key")
+		if err != nil {
+			t.Fatalf("Expected metadata value to be present, but gave error: %s", err)
+		}
+
+		stringValue, ok := value.(string)
+		if !ok {
+			t.Fatalf("Expected metadata value to be string, but was: %s", reflect.TypeOf(value))
+		}
+
+		if stringValue != "value" {
+			t.Fatalf("Expected metadata value to be '%s', but got '%s'", "value", stringValue)
+		}
+
+		_, err = route.GetMetadataValue("key2")
+		if err == nil {
+			t.Fatalf("Expected metadata key not to be present and error, but error was nil")
+		}
+
+		if !errors.Is(err, ErrMetadataKeyNotFound) {
+			t.Fatalf("Expected error to be ErrMetadataKeyNotFound but got: %s", err)
+		}
+
+	}).Metadata("key", "value")
+
+	router.HandleFunc("/single-value-fallback", func(w http.ResponseWriter, r *http.Request) {
+		route := CurrentRoute(r)
+		value := route.GetMetadataValueOr("key", "value-fallback")
+
+		stringValue, ok := value.(string)
+		if !ok {
+			t.Fatalf("Expected metadata value to be string, but was: %s", reflect.TypeOf(value))
+		}
+
+		if stringValue != "value" {
+			t.Fatalf("Expected metadata value to be '%s', but got '%s'", "value", stringValue)
+		}
+
+		fallbackValue := route.GetMetadataValueOr("key2", "value2")
+		fallbackStringValue, ok := fallbackValue.(string)
+		if !ok {
+			t.Fatalf("Expected metadata value to be string, but was: %s", reflect.TypeOf(value))
+		}
+
+		if fallbackStringValue != "value2" {
+			t.Fatalf("Expected metadata value to be '%s', but got '%s'", "value2", fallbackStringValue)
+		}
+
+	}).Metadata("key", "value")
+
+	t.Run("get metadata map", func(t *testing.T) {
+		req := newRequest("GET", "/")
+		router.ServeHTTP(rw, req)
+	})
+
+	t.Run("get metadata value", func(t *testing.T) {
+		req := newRequest("GET", "/single-value")
+		router.ServeHTTP(rw, req)
+	})
+
+	t.Run("get metadata value or fallback", func(t *testing.T) {
+		req := newRequest("GET", "/single-value-fallback")
+		router.ServeHTTP(rw, req)
+	})
+}