Repository: aio-libs/aiohttp

Issue:
Add canonical name for the route
## Long story short

For third party libraries sometimes it is useful to know canonical name of the route from request, for example https://github.com/aio-libs/aiozipkin/issues/115. 

Something  like `/foo/bar/{name:\d+}` could be exposed with property:  `request.match_info.route.canonical_name` 


<!-- What is the behaviour you expect? -->

## Actual behaviour

<!-- What's actually happening? -->

## Steps to reproduce

<!-- Please describe steps to reproduce the issue.
     If you have a script that does that please include it here within
     markdown code markup -->

## Your environment

<!-- Describe the environment you have that lead to your issue.
     This includes aiohttp version, OS, proxy server and other bits that
     are related to your case. 
     
     IMPORTANT: aiohttp is both server framework and client library.
     For getting rid of confusing please put 'server', 'client' or 'both'
     word here.
     -->



Discussion:
Let's use a formatter pattern from `.url_for()` implementation.

The issue is easy to implement, looking for a champion.

Resolving patch:
diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py
--- a/aiohttp/web_urldispatcher.py
+++ b/aiohttp/web_urldispatcher.py
@@ -45,6 +45,15 @@ def __init__(self, *, name=None):
     def name(self):
         return self._name
 
+    @property
+    @abc.abstractmethod
+    def canonical(self):
+        """Exposes the resource's canonical path.
+
+        For example '/foo/bar/{name}'
+
+        """
+
     @abc.abstractmethod  # pragma: no branch
     def url_for(self, **kwargs):
         """Construct url for resource with additional params."""
@@ -300,6 +309,10 @@ def __init__(self, path, *, name=None):
         assert not path or path.startswith('/')
         self._path = path
 
+    @property
+    def canonical(self):
+        return self._path
+
     def freeze(self):
         if not self._path:
             self._path = '/'
@@ -373,6 +386,10 @@ def __init__(self, path, *, name=None):
         self._pattern = compiled
         self._formatter = formatter
 
+    @property
+    def canonical(self):
+        return self._formatter
+
     def add_prefix(self, prefix):
         assert prefix.startswith('/')
         assert not prefix.endswith('/')
@@ -414,6 +431,10 @@ def __init__(self, prefix, *, name=None):
         super().__init__(name=name)
         self._prefix = URL.build(path=prefix).raw_path
 
+    @property
+    def canonical(self):
+        return self._prefix
+
     def add_prefix(self, prefix):
         assert prefix.startswith('/')
         assert not prefix.endswith('/')


Test patch:
diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py
--- a/tests/test_urldispatch.py
+++ b/tests/test_urldispatch.py
@@ -12,7 +12,9 @@
 from aiohttp.test_utils import make_mocked_request
 from aiohttp.web import HTTPMethodNotAllowed, HTTPNotFound, Response
 from aiohttp.web_urldispatcher import (PATH_SEP, AbstractResource,
-                                       ResourceRoute, SystemRoute, View,
+                                       DynamicResource, PlainResource,
+                                       ResourceRoute, StaticResource,
+                                       SystemRoute, View,
                                        _default_expect_handler)
 
 
@@ -1112,3 +1114,36 @@ def handler(request):
 
     with pytest.warns(DeprecationWarning):
         router.add_route('GET', '/handler', handler)
+
+
+def test_plain_resource_canonical():
+    canonical = '/plain/path'
+    res = PlainResource(path=canonical)
+    assert res.canonical == canonical
+
+
+def test_dynamic_resource_canonical():
+    canonicals = {
+        '/get/{name}': '/get/{name}',
+        '/get/{num:^\d+}': '/get/{num}',
+        r'/handler/{to:\d+}': r'/handler/{to}',
+        r'/{one}/{two:.+}': r'/{one}/{two}',
+    }
+    for pattern, canonical in canonicals.items():
+        res = DynamicResource(path=pattern)
+        assert res.canonical == canonical
+
+
+def test_static_resource_canonical():
+    prefix = '/prefix'
+    directory = str(os.path.dirname(aiohttp.__file__))
+    canonical = prefix
+    res = StaticResource(prefix=prefix, directory=directory)
+    assert res.canonical == canonical
+
+
+def test_prefixed_subapp_resource_canonical(app, loop):
+    canonical = '/prefix'
+    subapp = web.Application()
+    res = subapp.add_subapp(canonical, subapp)
+    assert res.canonical == canonical