Support action API requests

In some cases, the application needs to expose APIs that do not map to
the standard HTTP methods.  We support these requests using POST with
a mandatory 'action' query parameter that maps to the associated call in
the associated resource model.

Change-Id: I43bfaa954a85fa434beb1c5cf7b1ae18036f9ede
This commit is contained in:
Peter Balland 2014-07-24 14:46:04 -07:00
parent 8a4cc97862
commit 35b93f512f
2 changed files with 88 additions and 3 deletions

View File

@ -138,8 +138,8 @@ class ElementHandler(AbstractApiHandler):
"""
def __init__(self, path_regex, model,
collection_handler=None, allow_read=True, allow_replace=True,
allow_update=True, allow_delete=True):
collection_handler=None, allow_read=True, allow_actions=True,
allow_replace=True, allow_update=True, allow_delete=True):
"""Initialize an element handler.
Args:
@ -160,6 +160,7 @@ class ElementHandler(AbstractApiHandler):
self.model = model
self.collection_handler = collection_handler
self.allow_read = allow_read
self.allow_actions = allow_actions
self.allow_replace = allow_replace
self.allow_update = allow_update
self.allow_delete = allow_delete
@ -181,7 +182,8 @@ class ElementHandler(AbstractApiHandler):
"""
if request.method == 'GET' and self.allow_read:
return self.read(request)
#TODO(pballand): POST for controller semantics
elif request.method == 'POST' and self.allow_actions:
return self.action(request)
elif request.method == 'PUT' and self.allow_replace:
return self.replace(request)
elif request.method == 'PATCH' and self.allow_update:
@ -203,6 +205,30 @@ class ElementHandler(AbstractApiHandler):
status=httplib.OK,
content_type='application/json')
def action(self, request):
# Non-CRUD operations must specify an 'action' parameter
action = request.params.getall('action')
if len(action) != 1:
if len(action) > 1:
errstr = "Action parameter may not be provided multiple times."
else:
errstr = "Missing required action parameter."
return error_response(httplib.BAD_REQUEST, 400, errstr)
model_method = "%s_action" % action[0]
f = getattr(self.model, model_method, None)
if f is None:
return NOT_SUPPORTED_RESPONSE
try:
response = f(request.params, context=self._get_context(request),
request=request)
if isinstance(response, webob.Response):
return response
return webob.Response(body="%s\n" % json.dumps(response),
status=httplib.OK,
content_type='application/json')
except TypeError:
return NOT_SUPPORTED_RESPONSE
def replace(self, request):
if not hasattr(self.model, 'update_item'):
return NOT_SUPPORTED_RESPONSE

View File

@ -16,6 +16,7 @@ import httplib
import json
import unittest
import uuid
import webob
from congress.api import webservice
from congress.tests import base
@ -145,6 +146,64 @@ class TestSimpleDataModel(unittest.TestCase):
model.delete_item(self.UNADDED_ID, {}, context=context),
class TestElementHandler(base.TestCase):
def test_read(self):
# TODO(pballand): write tests
pass
def test_action(self):
element_handler = webservice.ElementHandler(r'/', '')
element_handler.model = webservice.SimpleDataModel("test")
request = MagicMock()
request.path = "/"
response = element_handler.action(request)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertEqual(json.loads(response.body)['description'],
"Missing required action parameter.")
request.params = MagicMock()
request.params.getall.return_value = ['do_test']
request.params["action"] = "do_test"
request.path = "/"
response = element_handler.action(request)
self.assertEqual(501, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertEqual(json.loads(response.body)['description'],
"Method not supported")
# test action impl returning python primitives
simple_data = [1, 2]
element_handler.model.do_test_action = lambda *a, **kwa: simple_data
response = element_handler.action(request)
self.assertEqual(200, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertEqual(json.loads(response.body), simple_data)
# test action impl returning custom webob response
custom_data = webob.Response(body="test", status=599,
content_type="custom/test")
element_handler.model.do_test_action = lambda *a, **kwa: custom_data
response = element_handler.action(request)
self.assertEqual(599, response.status_code)
self.assertEqual('custom/test', response.content_type)
self.assertEqual(response.body, "test")
def test_replace(self):
# TODO(pballand): write tests
pass
def test_update(self):
# TODO(pballand): write tests
pass
def test_delete(self):
# TODO(pballand): write tests
pass
class TestCollectionHandler(base.TestCase):
def test_get_action_type(self):