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"