From 4eabbc609a80cb141524af51c088cb44414a0894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= Date: Sun, 5 Jun 2016 11:00:13 +0200 Subject: [PATCH 1/4] resource/Site: Add regular expression support for path names --- aiocoap/resource.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/aiocoap/resource.py b/aiocoap/resource.py index 69db8c45..4e8395ec 100644 --- a/aiocoap/resource.py +++ b/aiocoap/resource.py @@ -26,6 +26,7 @@ dispatch requests based on the Uri-Path header. """ +import re import hashlib import asyncio @@ -161,6 +162,9 @@ def render_get(self, request): response.opt.content_format = self.ct return response +class PathRegex(str): + """Regular expression match in resource path components""" + class Site(interfaces.ObservableResource): """Typical root element that gets passed to a :class:`Context` and contains all the resources that can be found when the endpoint gets accessed as a @@ -185,12 +189,24 @@ def needs_blockwise_assembly(self, request): @asyncio.coroutine def render(self, request): - try: - child = self._resources[request.opt.uri_path] - except KeyError: + path = tuple(request.opt.uri_path) + # Only compare against resources with the same number of path components + matches = [(key, res) for key, res in self._resources.items() if len(key) == len(path)] + for level in range(len(path)): + # Filter routes matching the path, one level at a time + matches = [ + (key, res) for key, res in matches \ + if isinstance(key[level], PathRegex) and \ + re.fullmatch(key[level], path[level]) or \ + key[level] == path[level] + ] + if len(matches) == 0: raise error.NoResource() - else: - return child.render(request) + elif len(matches) > 1: + raise error.RenderableError( + "Ambiguous matches: {} = {}".format(repr(path), repr(matches))) + child = matches[0][1] + return child.render(request) @asyncio.coroutine def add_observation(self, request, serverobservation): From 74d8314f5e304b8ee4b31aaa7dc264eac769a793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= Date: Sun, 5 Jun 2016 11:29:29 +0200 Subject: [PATCH 2/4] server.py: Add path pattern match example resources --- server.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server.py b/server.py index 207c9201..5da6b1f8 100755 --- a/server.py +++ b/server.py @@ -13,6 +13,7 @@ import asyncio +from aiocoap.resource import PathRegex import aiocoap.resource as resource import aiocoap @@ -90,6 +91,15 @@ def render_get(self, request): payload = datetime.datetime.now().strftime("%Y-%m-%d %H:%M").encode('ascii') return aiocoap.Message(code=aiocoap.CONTENT, payload=payload) +class RegexResource(resource.Resource): + """Example wildcard matching resource, use request.opt.uri_path to find the + actual target of the request""" + + @asyncio.coroutine + def render_get(self, request): + payload = 'Regular expression match example\nrequest.opt.uri_path = {}'.format(repr(request.opt.uri_path)) + return aiocoap.Message(code=aiocoap.CONTENT, payload=payload.encode('utf-8')) + #class CoreResource(resource.Resource): # """ # Example Resource that provides list of links hosted by a server. @@ -129,6 +139,10 @@ def main(): root.add_resource(('other', 'separate'), SeparateLargeResource()) + root.add_resource(('regex', 'anything', PathRegex(r'.*')), RegexResource()) + root.add_resource(('regex', 'numbers', PathRegex(r'[0-9]*')), RegexResource()) + root.add_resource(('regex', 'pattern', PathRegex(r'[a-zA-Z][0-9]')), RegexResource()) + asyncio.async(aiocoap.Context.create_server_context(root)) asyncio.get_event_loop().run_forever() From a631a6864f40d2546c56d2503540aee7cfb4cfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= Date: Sun, 5 Jun 2016 11:32:52 +0200 Subject: [PATCH 3/4] resource/Site: Improve regex matching to separate string and pattern matches --- aiocoap/resource.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/aiocoap/resource.py b/aiocoap/resource.py index 4e8395ec..e6ae6a24 100644 --- a/aiocoap/resource.py +++ b/aiocoap/resource.py @@ -192,13 +192,23 @@ def render(self, request): path = tuple(request.opt.uri_path) # Only compare against resources with the same number of path components matches = [(key, res) for key, res in self._resources.items() if len(key) == len(path)] + # Filter routes matching the path, one level at a time for level in range(len(path)): - # Filter routes matching the path, one level at a time + # Try to find exact string matches + string_matches = [ + (key, res) for key, res in matches \ + if not isinstance(key[level], PathRegex) and \ + key[level] == path[level] + ] + if string_matches: + # string matches have a higher priority than regex matches + matches = string_matches + continue + # Try to find any regex matches matches = [ (key, res) for key, res in matches \ if isinstance(key[level], PathRegex) and \ - re.fullmatch(key[level], path[level]) or \ - key[level] == path[level] + re.fullmatch(key[level], path[level]) ] if len(matches) == 0: raise error.NoResource() From 1daec774b7942759fc4d61e37b26e49d1d0b5714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= Date: Sun, 5 Jun 2016 12:54:20 +0200 Subject: [PATCH 4/4] tests: Add resources for pattern matching to the test server --- tests/server.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/server.py b/tests/server.py index c5b711ff..86b53fd0 100644 --- a/tests/server.py +++ b/tests/server.py @@ -77,6 +77,12 @@ def render_post(self, request): response = request.payload.replace(b'0', b'O') return aiocoap.Message(code=aiocoap.CONTENT, payload=response) +class PathResource(aiocoap.resource.Resource): + @asyncio.coroutine + def render_get(self, request): + payload = repr(request.opt.uri_path).encode('utf-8') + return aiocoap.Message(code=aiocoap.CONTENT, payload=payload) + class TestingSite(aiocoap.resource.Site): def __init__(self): super(TestingSite, self).__init__() @@ -86,6 +92,8 @@ def __init__(self): self.add_resource(('big',), BigResource()) self.add_resource(('slowbig',), SlowBigResource()) self.add_resource(('replacing',), ReplacingResource()) + self.add_resource(('wildcard', aiocoap.resource.PathRegex('.*')), PathResource()) + self.add_resource(('4digits', aiocoap.resource.PathRegex('[0-9]{4}')), PathResource()) # helpers