From cc4fba96d9dd596f3b6c13d8ea71cc39b637498a Mon Sep 17 00:00:00 2001
From: Satoshi Kobayashi <satoshi-k@stratosphere.co.jp>
Date: Wed, 4 Sep 2013 14:49:10 +0900
Subject: [PATCH] Advanced WSGI API

It is like Flask based on decorator.

Signed-off-by: Satoshi Kobayashi <satoshi-k@stratosphere.co.jp>
---
 ryu/app/newwsgisample.py        |   40 +++++++++++++++++
 ryu/app/wsgi.py                 |   30 +++++++++++++
 ryu/tests/unit/app/test_wsgi.py |   89 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 159 insertions(+), 0 deletions(-)
 create mode 100644 ryu/app/newwsgisample.py
 create mode 100644 ryu/tests/unit/app/__init__.py
 create mode 100644 ryu/tests/unit/app/test_wsgi.py

diff --git a/ryu/app/newwsgisample.py b/ryu/app/newwsgisample.py
new file mode 100644
index 0000000..ea120a0
--- /dev/null
+++ b/ryu/app/newwsgisample.py
@@ -0,0 +1,40 @@
+# 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
+
+from ryu.base import app_manager
+from ryu.app.wsgi import WSGIApplication, ControllerBase, route
+from ryu.lib import dpid as dpidlib
+from webob.response import Response
+
+
+class NewWsgiApiApp(app_manager.RyuApp):
+    _CONTEXTS = {
+        'wsgi': WSGIApplication,
+    }
+
+    def __init__(self, *args, **kwargs):
+        super(NewWsgiApiApp, self).__init__(*args, **kwargs)
+        
+        wsgi = kwargs['wsgi']
+        wsgi.register(SampleController)
+
+class SampleController(ControllerBase):
+
+    @route('sample', '/sample/{dpid}',
+           methods=['GET'], requirements={'dpid': dpidlib.DPID_PATTERN})
+    def sample(self, req, dpid, **_kwargs):
+        return Response(status=200, body=dpid)
diff --git a/ryu/app/wsgi.py b/ryu/app/wsgi.py
index 28bada0..e041d1c 100644
--- a/ryu/app/wsgi.py
+++ b/ryu/app/wsgi.py
@@ -32,6 +32,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(func):
+        func.route = {
+            'name': name,
+            'path': path,
+            'methods': methods,
+            'requirements': requirements,
+        }
+        return func
+    return _route
+
+
 class ControllerBase(object):
     special_vars = ['action', 'controller']
 
@@ -79,6 +91,24 @@ class WSGIApplication(object):
         controller = match['controller'](req, link, data, **self.config)
         return controller(req)
 
+    def register(self, controller):
+        for attr_name in dir(controller):
+            attr = getattr(controller, attr_name)
+            if hasattr(attr, 'route'):
+                route = getattr(attr, 'route')
+                name = route['name']
+                path = route['path']
+                conditions = {}
+                if route['methods']:
+                    conditions['method'] = route['methods']
+                requirements = route.get('requirements') or {}
+                self.mapper.connect(name,
+                                    path,
+                                    controller=controller,
+                                    requirements=requirements,
+                                    action=attr_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 0000000..e69de29
diff --git a/ryu/tests/unit/app/test_wsgi.py b/ryu/tests/unit/app/test_wsgi.py
new file mode 100644
index 0000000..0e5b219
--- /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')
-- 
1.7.1

