diff --git a/pecan/rest.py b/pecan/rest.py index a934109..bbd56d4 100644 --- a/pecan/rest.py +++ b/pecan/rest.py @@ -7,11 +7,13 @@ from routing import iscontroller, lookup_controller class RestController(object): + _custom_actions = {} + @expose() def _route(self, args): # convention uses "_method" to handle browser-unsupported methods - method = request.GET.get('_method', request.method).lower() + method = request.params.get('_method', request.method).lower() # make sure DELETE/PUT requests don't use GET if request.method == 'GET' and method in ('delete', 'put'): @@ -108,13 +110,13 @@ class RestController(object): return controller, remainder[:-1] # check for custom GET requests - if method_name in getattr(self, '_custom_actions', []): - controller = self._find_controller(method_name, 'get_%s' % method_name) + if method.upper() in self._custom_actions.get(method_name, []): + controller = self._find_controller('get_%s' % method_name, method_name) if controller: return controller, remainder[:-1] controller = getattr(self, remainder[0], None) if controller and not ismethod(controller): - return controller, remainder[1:] + return lookup_controller(controller, remainder[1:]) # finally, check for the regular get_one/get requests controller = self._find_controller('get_one', 'get') @@ -142,20 +144,23 @@ class RestController(object): abort(404) def _handle_post(self, method, remainder): - - # check for custom controllers or sub-controllers + + # check for custom POST/PUT requests if remainder: - controller = self._find_controller(remainder[0]) - if controller: - return controller, remainder[1:] - sub_controller = getattr(self, remainder[0], None) - if sub_controller and not ismethod(sub_controller): - return lookup_controller(sub_controller, remainder[1:]) + method_name = remainder[-1] + if method.upper() in self._custom_actions.get(method_name, []): + controller = self._find_controller('%s_%s' % (method, method_name), method_name) + if controller: + return controller, remainder[:-1] + controller = getattr(self, remainder[0], None) + if controller and not ismethod(controller): + return lookup_controller(controller, remainder[1:]) - # check for regular post/put requests + # check for regular POST/PUT requests controller = self._find_controller(method) if controller: return controller, remainder + abort(404) _handle_put = _handle_post diff --git a/tests/test_rest.py b/tests/test_rest.py index 87dd5a0..0707286 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -1,17 +1,30 @@ -from pecan import expose, make_app, request, response +from pecan import abort, expose, make_app, request, response from pecan.rest import RestController -from webob import exc -from webtest import TestApp, AppError -from py.test import raises +from webtest import TestApp from json import dumps, loads class TestRestController(object): def test_basic_rest(self): + + class OthersController(object): + + @expose() + def index(self): + return 'OTHERS' + + @expose() + def echo(self, value): + return str(value) + class ThingsController(RestController): data = ['zero', 'one', 'two', 'three'] + _custom_actions = {'count': ['GET'], 'length': ['GET', 'POST']} + + others = OthersController() + @expose() def get_one(self, id): return self.data[int(id)] @@ -20,6 +33,17 @@ class TestRestController(object): def get_all(self): return dict(items=self.data) + @expose() + def length(self, id, value=None): + length = len(self.data[int(id)]) + if value: + length += len(value) + return str(length) + + @expose() + def get_count(self): + return str(len(self.data)) + @expose() def new(self): return 'NEW' @@ -47,6 +71,22 @@ class TestRestController(object): def delete(self, id): del self.data[int(id)] return 'DELETED' + + @expose() + def reset(self): + return 'RESET' + + @expose() + def post_options(self): + return 'OPTIONS' + + @expose() + def options(self): + abort(500) + + @expose() + def other(self): + abort(500) class RootController(object): things = ThingsController() @@ -142,6 +182,73 @@ class TestRestController(object): r = app.get('/things') assert r.status_int == 200 assert len(loads(r.body)['items']) == 3 + + # test "RESET" custom action + r = app.request('/things', method='RESET') + assert r.status_int == 200 + assert r.body == 'RESET' + + # test "RESET" custom action with _method parameter + r = app.get('/things?_method=RESET') + assert r.status_int == 200 + assert r.body == 'RESET' + + # test the "OPTIONS" custom action + r = app.request('/things', method='OPTIONS') + assert r.status_int == 200 + assert r.body == 'OPTIONS' + + # test the "OPTIONS" custom action with the _method parameter + r = app.post('/things', {'_method': 'OPTIONS'}) + assert r.status_int == 200 + assert r.body == 'OPTIONS' + + # test the "other" custom action + r = app.request('/things/other', method='MISC', status=405) + assert r.status_int == 405 + + # test the "other" custom action with the _method parameter + r = app.post('/things/other', {'_method': 'MISC'}, status=405) + assert r.status_int == 405 + + # test the "others" custom action + r = app.request('/things/others', method='MISC') + assert r.status_int == 200 + assert r.body == 'OTHERS' + + # test the "others" custom action with the _method parameter + r = app.get('/things/others?_method=MISC') + assert r.status_int == 200 + assert r.body == 'OTHERS' + + # test an invalid custom action + r = app.get('/things?_method=BAD', status=404) + assert r.status_int == 404 + + # test custom "GET" request "count" + r = app.get('/things/count') + assert r.status_int == 200 + assert r.body == '3' + + # test custom "GET" request "length" + r = app.get('/things/1/length') + assert r.status_int == 200 + assert r.body == str(len('one')) + + # test custom "GET" request through subcontroller + r = app.get('/things/others/echo?value=test') + assert r.status_int == 200 + assert r.body == 'test' + + # test custom "POST" request "length" + r = app.post('/things/1/length', {'value':'test'}) + assert r.status_int == 200 + assert r.body == str(len('onetest')) + + # test custom "POST" request through subcontroller + r = app.post('/things/others/echo', {'value':'test'}) + assert r.status_int == 200 + assert r.body == 'test' def test_nested_rest(self): @@ -414,3 +521,159 @@ class TestRestController(object): r = app.get('/foos') assert r.status_int == 200 assert len(loads(r.body)['items']) == 1 + + def test_bad_rest(self): + + class ThingsController(RestController): + pass + + class RootController(object): + things = ThingsController() + + # create the app + app = TestApp(make_app(RootController())) + + # test get_all + r = app.get('/things', status=404) + assert r.status_int == 404 + + # test get_one + r = app.get('/things/1', status=404) + assert r.status_int == 404 + + # test post + r = app.post('/things', {'value':'one'}, status=404) + assert r.status_int == 404 + + # test edit + r = app.get('/things/1/edit', status=404) + assert r.status_int == 404 + + # test put + r = app.put('/things/1', {'value':'ONE'}, status=404) + + # test put with _method parameter and GET + r = app.get('/things/1?_method=put', {'value':'ONE!'}, status=405) + assert r.status_int == 405 + + # test put with _method parameter and POST + r = app.post('/things/1?_method=put', {'value':'ONE!'}, status=404) + assert r.status_int == 404 + + # test get delete + r = app.get('/things/1/delete', status=404) + assert r.status_int == 404 + + # test delete + r = app.delete('/things/1', status=404) + assert r.status_int == 404 + + # test delete with _method parameter and GET + r = app.get('/things/1?_method=DELETE', status=405) + assert r.status_int == 405 + + # test delete with _method parameter and POST + r = app.post('/things/1?_method=DELETE', status=404) + assert r.status_int == 404 + + # test "RESET" custom action + r = app.request('/things', method='RESET', status=404) + assert r.status_int == 404 + + def test_custom_delete(self): + + class OthersController(object): + + @expose() + def index(self): + return 'DELETE' + + @expose() + def reset(self, id): + return str(id) + + class ThingsController(RestController): + + others = OthersController() + + @expose() + def delete_fail(self): + abort(500) + + class RootController(object): + things = ThingsController() + + # create the app + app = TestApp(make_app(RootController())) + + # test bad delete + r = app.delete('/things/delete_fail', status=405) + assert r.status_int == 405 + + # test bad delete with _method parameter and GET + r = app.get('/things/delete_fail?_method=delete', status=405) + assert r.status_int == 405 + + # test bad delete with _method parameter and POST + r = app.post('/things/delete_fail', {'_method':'delete'}, status=405) + assert r.status_int == 405 + + # test custom delete without ID + r = app.delete('/things/others') + assert r.status_int == 200 + assert r.body == 'DELETE' + + # test custom delete without ID with _method parameter and GET + r = app.get('/things/others?_method=delete', status=405) + assert r.status_int == 405 + + # test custom delete without ID with _method parameter and POST + r = app.post('/things/others', {'_method':'delete'}) + assert r.status_int == 200 + assert r.body == 'DELETE' + + # test custom delete with ID + r = app.delete('/things/others/reset/1') + assert r.status_int == 200 + assert r.body == '1' + + # test custom delete with ID with _method parameter and GET + r = app.get('/things/others/reset/1?_method=delete', status=405) + assert r.status_int == 405 + + # test custom delete with ID with _method parameter and POST + r = app.post('/things/others/reset/1', {'_method':'delete'}) + assert r.status_int == 200 + assert r.body == '1' + + def test_get_with_var_args(self): + + class OthersController(object): + + @expose() + def index(self, one, two, three): + return 'NESTED: %s, %s, %s' % (one, two, three) + + class ThingsController(RestController): + + others = OthersController() + + @expose() + def get_one(self, *args): + return ', '.join(args) + + class RootController(object): + things = ThingsController() + + # create the app + app = TestApp(make_app(RootController())) + + # test get request + r = app.get('/things/one/two/three') + assert r.status_int == 200 + assert r.body == 'one, two, three' + + # test nested get request + r = app.get('/things/one/two/three/others') + assert r.status_int == 200 + assert r.body == 'NESTED: one, two, three'