From ec70b2dcc565b2eea90e086e2eba2ee40616a9c6 Mon Sep 17 00:00:00 2001 From: Satoshi Kobayashi Date: Tue, 24 Sep 2013 11:52:26 +0900 Subject: [PATCH] Advanced WSGI API HTTP routing and implements will be in the same place. This idea was inspired from Flask and Bottle of Python and JAX-RS of Java. This modification keeps backward compatibility. Signed-off-by: Satoshi Kobayashi Signed-off-by: FUJITA Tomonori --- ryu/app/wsgi.py | 33 ++++++++++++ ryu/tests/unit/app/__init__.py | 0 ryu/tests/unit/app/test_wsgi.py | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 ryu/tests/unit/app/__init__.py create mode 100644 ryu/tests/unit/app/test_wsgi.py diff --git a/ryu/app/wsgi.py b/ryu/app/wsgi.py index 28bada0b..c9a9f96b 100644 --- a/ryu/app/wsgi.py +++ b/ryu/app/wsgi.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect + from oslo.config import cfg import webob.dec @@ -32,6 +34,18 @@ HEX_PATTERN = r'0x[0-9a-z]+' DIGIT_PATTERN = r'[1-9][0-9]*' +def route(name, path, methods=None, requirements=None): + def _route(controller_method): + controller_method.routing_info = { + 'name': name, + 'path': path, + 'methods': methods, + 'requirements': requirements, + } + return controller_method + return _route + + class ControllerBase(object): special_vars = ['action', 'controller'] @@ -79,6 +93,25 @@ class WSGIApplication(object): controller = match['controller'](req, link, data, **self.config) return controller(req) + def register(self, controller): + methods = inspect.getmembers(controller, + lambda v: inspect.ismethod(v) and + hasattr(v, 'routing_info')) + for method_name, method in methods: + routing_info = getattr(method, 'routing_info') + name = routing_info['name'] + path = routing_info['path'] + conditions = {} + if routing_info.get('methods'): + conditions['method'] = routing_info['methods'] + requirements = routing_info.get('requirements') or {} + self.mapper.connect(name, + path, + controller=controller, + requirements=requirements, + action=method_name, + conditions=conditions) + class WSGIServer(hub.WSGIServer): def __init__(self, application, **config): diff --git a/ryu/tests/unit/app/__init__.py b/ryu/tests/unit/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ryu/tests/unit/app/test_wsgi.py b/ryu/tests/unit/app/test_wsgi.py new file mode 100644 index 00000000..0e5b2192 --- /dev/null +++ b/ryu/tests/unit/app/test_wsgi.py @@ -0,0 +1,89 @@ +# Copyright (C) 2013 Stratosphere Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import unittest +import logging +from nose.tools import * + +from ryu.app.wsgi import ControllerBase, WSGIApplication, route +from webob.response import Response +from ryu.lib import dpid as dpidlib + +LOG = logging.getLogger('test_wsgi') + + +class _TestController(ControllerBase): + + @route('test', '/test/{dpid}', + methods=['GET'], requirements={'dpid': dpidlib.DPID_PATTERN}) + def test_get_dpid(self, req, dpid, **_kwargs): + return Response(status=200, body=dpid) + + @route('test', '/test') + def test_root(self, req, **_kwargs): + return Response(status=200, body='root') + + +class Test_wsgi(unittest.TestCase): + + """ Test case for wsgi + """ + + def setUp(self): + self.wsgi_app = WSGIApplication() + self.wsgi_app.register(_TestController) + + def tearDown(self): + pass + + def test_wsgi_decorator_ok(self): + r = self.wsgi_app({'REQUEST_METHOD': 'GET', + 'PATH_INFO': '/test/0123456789abcdef'}, + lambda s, _: eq_(s, '200 OK')) + eq_(r[0], ('0123456789abcdef')) + + def test_wsgi_decorator_ng_path(self): + self.wsgi_app({'REQUEST_METHOD': 'GET', + 'PATH_INFO': '/'}, + lambda s, _: eq_(s, '404 Not Found')) + + def test_wsgi_decorator_ng_method(self): + # XXX: If response code is "405 Method Not Allowed", it is better. + self.wsgi_app({'REQUEST_METHOD': 'PUT', + 'PATH_INFO': '/test/0123456789abcdef'}, + lambda s, _: eq_(s, '404 Not Found')) + + def test_wsgi_decorator_ng_requirements(self): + # XXX: If response code is "400 Bad Request", it is better. + self.wsgi_app({'REQUEST_METHOD': 'GET', + 'PATH_INFO': '/test/hogehoge'}, + lambda s, _: eq_(s, '404 Not Found')) + + def test_wsgi_decorator_ok_any_method(self): + self.wsgi_app({'REQUEST_METHOD': 'GET', + 'PATH_INFO': '/test'}, + lambda s, _: eq_(s, '200 OK')) + self.wsgi_app({'REQUEST_METHOD': 'POST', + 'PATH_INFO': '/test'}, + lambda s, _: eq_(s, '200 OK')) + self.wsgi_app({'REQUEST_METHOD': 'PUT', + 'PATH_INFO': '/test'}, + lambda s, _: eq_(s, '200 OK')) + r = self.wsgi_app({'REQUEST_METHOD': 'DELETE', + 'PATH_INFO': '/test'}, + lambda s, _: eq_(s, '200 OK')) + eq_(r[0], 'root')