From 9d7b2895a5b4d9899dd06d199618c38df401373e Mon Sep 17 00:00:00 2001
From: Christophe de Vienne <cdevienne@gmail.com>
Date: Wed, 11 Jul 2012 17:14:14 +0200
Subject: [PATCH] Rest protocols now make use of the http method to select the
 function is needed

---
 doc/changes.rst             |   2 +
 doc/protocols.rst           |  11 ++--
 wsme/protocols/rest.py      |  18 +++++-
 wsme/tests/test_restjson.py | 114 +++++++++++++++++++++++++++++++++---
 4 files changed, 133 insertions(+), 12 deletions(-)

diff --git a/doc/changes.rst b/doc/changes.rst
index ac084fc..d83b56c 100644
--- a/doc/changes.rst
+++ b/doc/changes.rst
@@ -14,6 +14,8 @@ Changes
 
 *   Tests code coverage is now over 95%.
 
+*   RESTful protocol can now use the http method.
+
 0.3 (2012-04-20)
 ----------------
 
diff --git a/doc/protocols.rst b/doc/protocols.rst
index 809a18b..70c1760 100644
--- a/doc/protocols.rst
+++ b/doc/protocols.rst
@@ -23,11 +23,14 @@ following paths :
 -   ``/ws/persons/update``
 -   ``/ws/persons/destroy``
 
-In a near future, an additional expose option `restverb` will allow
-to use the HTTP verb to select the function, in which case the path
-will not containt the function name.
+In addition to this trivial function mapping, a `method` option can
+be given to the `expose` decorator. In such a case, the function
+name can be omitted by the caller, and the dispatch will look at the
+http method used in the request to select the correct function.
 
-The function parameters can be transmitted in two ways :
+The function parameters can be transmitted in two ways (is using
+the http method to select the function, one way or the other
+may be usable) :
 
 #.  As a GET query string or POST form parameters.
 
diff --git a/wsme/protocols/rest.py b/wsme/protocols/rest.py
index e3a4ea1..5e84df9 100644
--- a/wsme/protocols/rest.py
+++ b/wsme/protocols/rest.py
@@ -24,6 +24,22 @@ class RestProtocol(Protocol):
         if path[-1].endswith('.' + self.dataformat):
             path[-1] = path[-1][:-len(self.dataformat) - 1]
 
+        # Check if the path is actually a function, and if not
+        # see if the http method make a difference
+        # TODO Re-think the function lookup phases. Here we are
+        # doing the job that will be done in a later phase, which
+        # is sub-optimal
+        for p, fdef in self.root.getapi():
+            if p == path:
+                return path
+
+        # No function at this path. Now check for function that have
+        # this path as a prefix, and declared an http method
+        for p, fdef in self.root.getapi():
+            if len(p) == len(path) + 1 and p[:len(path)] == path and \
+                    fdef.extra_options.get('method') == context.request.method:
+                return p
+
         return path
 
     def read_arguments(self, context):
@@ -35,7 +51,7 @@ class RestProtocol(Protocol):
                     request.headers['Content-Type']:
             # The params were read from the body, ignoring the body then
             pass
-        elif len(request.params) and request.body:
+        elif len(request.params) and request.content_length:
             log.warning("The request has both a body and params.")
             log.debug("Params: %s" % request.params)
             log.debug("Body: %s" % request.body)
diff --git a/wsme/tests/test_restjson.py b/wsme/tests/test_restjson.py
index 79f67a7..31e0c99 100644
--- a/wsme/tests/test_restjson.py
+++ b/wsme/tests/test_restjson.py
@@ -14,6 +14,7 @@ import wsme.protocols.restjson
 from wsme.protocols.restjson import fromjson, tojson
 from wsme.utils import parse_isodatetime, parse_isotime, parse_isodate
 from wsme.types import isusertype, register_type
+from wsme.api import expose, validate
 
 
 import six
@@ -79,6 +80,52 @@ def prepare_result(value, datatype):
     return value
 
 
+class Obj(wsme.types.Base):
+    id = int
+    name = wsme.types.text
+
+
+class CRUDResult(object):
+    data = Obj
+    message = wsme.types.text
+
+    def __init__(self, data=wsme.types.Unset, message=wsme.types.Unset):
+        self.data = data
+        self.message = message
+
+
+class MiniCrud(object):
+    @expose(CRUDResult, method='PUT')
+    @validate(Obj)
+    def create(self, data):
+        print(repr(data))
+        return CRUDResult(data, u('create'))
+
+    @expose(CRUDResult, method='GET')
+    @validate(Obj)
+    def read(self, ref):
+        print(repr(ref))
+        if ref.id == 1:
+            ref.name = u('test')
+        return CRUDResult(ref, u('read'))
+
+    @expose(CRUDResult, method='POST')
+    @validate(Obj)
+    def update(self, data):
+        print(repr(data))
+        return CRUDResult(data, u('update'))
+
+    @expose(CRUDResult, method='DELETE')
+    @validate(Obj)
+    def delete(self, ref):
+        print(repr(ref))
+        if ref.id == 1:
+            ref.name = u('test')
+        return CRUDResult(ref, u('delete'))
+
+wsme.tests.protocol.WSTestRoot.crud = MiniCrud()
+
+
 class TestRestJson(wsme.tests.protocol.ProtocolTestCase):
     protocol = 'restjson'
 
@@ -256,13 +303,6 @@ class TestRestJson(wsme.tests.protocol.ProtocolTestCase):
         assert r[1] == '''{
     "a": 2
 }''', r[1]
-        
-    def test_encode_sample_result(self):
-        r = self.root.protocols[0].encode_sample_result(
-            int, 2, True
-        )
-        assert r[0] == 'javascript', r[0]
-        assert r[1] == '''2'''
 
     def test_encode_sample_result(self):
         r = self.root.protocols[0].encode_sample_result(
@@ -278,3 +318,63 @@ class TestRestJson(wsme.tests.protocol.ProtocolTestCase):
         assert r[1] == '''{
     "result": 2
 }'''
+
+    def test_PUT(self):
+        data = {"id": 1, "name": u("test")}
+        content = json.dumps(dict(data=data))
+        headers = {
+            'Content-Type': 'application/json',
+        }
+        res = self.app.put(
+            '/crud',
+            content,
+            headers=headers,
+            expect_errors=False)
+        print("Received:", res.body)
+        result = json.loads(res.text)
+        print(result)
+        assert result['data']['id'] == 1
+        assert result['data']['name'] == u("test")
+        assert result['message'] == "create"
+
+    def test_GET(self):
+        headers = {
+            'Content-Type': 'application/json',
+        }
+        res = self.app.get(
+            '/crud?ref.id=1',
+            headers=headers,
+            expect_errors=False)
+        print("Received:", res.body)
+        result = json.loads(res.text)
+        print(result)
+        assert result['data']['id'] == 1
+        assert result['data']['name'] == u("test")
+        assert result['message'] == "read"
+
+    def test_POST(self):
+        headers = {
+            'Content-Type': 'application/json',
+        }
+        res = self.app.post(
+            '/crud',
+            json.dumps(dict(data=dict(id=1, name=u('test')))),
+            headers=headers,
+            expect_errors=False)
+        print("Received:", res.body)
+        result = json.loads(res.text)
+        print(result)
+        assert result['data']['id'] == 1
+        assert result['data']['name'] == u("test")
+        assert result['message'] == "update"
+
+    def test_DELETE(self):
+        res = self.app.delete(
+            '/crud.json?ref.id=1',
+            expect_errors=False)
+        print("Received:", res.body)
+        result = json.loads(res.text)
+        print(result)
+        assert result['data']['id'] == 1
+        assert result['data']['name'] == u("test")
+        assert result['message'] == "delete"