Make API framework more flexible for various extensions

This patch adds a function to Neutron extension mechanism
so that we can add API methods to "/resources" path rather than
"/resources/action" path.

This patch also allow extentions to control correspondences
between action and status in API request.

Change-Id: I862086f7528583e65d7bee794f011ddff6ae8901
Partial-Implements: blueprint add-tags-to-core-resources
Related-Bug: #1489291
This commit is contained in:
Hirofumi Ichihara 2016-02-25 12:26:57 +09:00
parent b380b15d4c
commit 0ae3c172ae
4 changed files with 106 additions and 3 deletions

View File

@ -275,6 +275,16 @@ class ExtensionMiddleware(base.ConfigurableMiddleware):
submap.connect(path)
submap.connect("%s.:(format)" % path)
for action, method in resource.collection_methods.items():
conditions = dict(method=[method])
path = "/%s" % resource.collection
with mapper.submapper(controller=resource.controller,
action=action,
path_prefix=path_prefix,
conditions=conditions) as submap:
submap.connect(path)
submap.connect("%s.:(format)" % path)
mapper.resource(resource.collection, resource.collection,
controller=resource.controller,
member=resource.member_actions,
@ -632,14 +642,17 @@ class ResourceExtension(object):
"""Add top level resources to the OpenStack API in Neutron."""
def __init__(self, collection, controller, parent=None, path_prefix="",
collection_actions=None, member_actions=None, attr_map=None):
collection_actions=None, member_actions=None, attr_map=None,
collection_methods=None):
collection_actions = collection_actions or {}
collection_methods = collection_methods or {}
member_actions = member_actions or {}
attr_map = attr_map or {}
self.collection = collection
self.controller = controller
self.parent = parent
self.collection_actions = collection_actions
self.collection_methods = collection_methods
self.member_actions = member_actions
self.path_prefix = path_prefix
self.attr_map = attr_map

View File

@ -39,14 +39,15 @@ class Request(wsgi.Request):
pass
def Resource(controller, faults=None, deserializers=None, serializers=None):
def Resource(controller, faults=None, deserializers=None, serializers=None,
action_status=None):
"""Represents an API entity resource and the associated serialization and
deserialization logic
"""
default_deserializers = {'application/json': wsgi.JSONDeserializer()}
default_serializers = {'application/json': wsgi.JSONDictSerializer()}
format_types = {'json': 'application/json'}
action_status = dict(create=201, delete=204)
action_status = action_status or dict(create=201, delete=204)
default_deserializers.update(deserializers or {})
default_serializers.update(serializers or {})

View File

@ -160,6 +160,9 @@ class ResourceExtensionTest(base.BaseTestCase):
def custom_member_action(self, request, id):
return {'member_action': 'value'}
def custom_collection_method(self, request, **kwargs):
return {'collection': 'value'}
def custom_collection_action(self, request, **kwargs):
return {'collection': 'value'}
@ -355,6 +358,80 @@ class ResourceExtensionTest(base.BaseTestCase):
self.assertEqual(200, response.status_int)
self.assertEqual(jsonutils.loads(response.body)['collection'], "value")
def test_resource_extension_for_get_custom_collection_method(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_method': "GET"}
res_ext = extensions.ResourceExtension('tweedles', controller,
collection_methods=collections)
test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
response = test_app.get("/tweedles")
self.assertEqual(200, response.status_int)
self.assertEqual("value", jsonutils.loads(response.body)['collection'])
def test_resource_extension_for_put_custom_collection_method(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_method': "PUT"}
res_ext = extensions.ResourceExtension('tweedles', controller,
collection_methods=collections)
test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
response = test_app.put("/tweedles")
self.assertEqual(200, response.status_int)
self.assertEqual('value', jsonutils.loads(response.body)['collection'])
def test_resource_extension_for_post_custom_collection_method(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_method': "POST"}
res_ext = extensions.ResourceExtension('tweedles', controller,
collection_methods=collections)
test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
response = test_app.post("/tweedles")
self.assertEqual(200, response.status_int)
self.assertEqual('value', jsonutils.loads(response.body)['collection'])
def test_resource_extension_for_delete_custom_collection_method(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_method': "DELETE"}
res_ext = extensions.ResourceExtension('tweedles', controller,
collection_methods=collections)
test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
response = test_app.delete("/tweedles")
self.assertEqual(200, response.status_int)
self.assertEqual('value', jsonutils.loads(response.body)['collection'])
def test_resource_ext_for_formatted_req_on_custom_collection_method(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_method': "GET"}
res_ext = extensions.ResourceExtension('tweedles', controller,
collection_methods=collections)
test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
response = test_app.get("/tweedles.json")
self.assertEqual(200, response.status_int)
self.assertEqual("value", jsonutils.loads(response.body)['collection'])
def test_resource_ext_for_nested_resource_custom_collection_method(self):
controller = self.ResourceExtensionController()
collections = {'custom_collection_method': "GET"}
parent = {'collection_name': 'beetles', 'member_name': 'beetle'}
res_ext = extensions.ResourceExtension('tweedles', controller,
collection_methods=collections,
parent=parent)
test_app = _setup_extensions_test_app(SimpleExtensionManager(res_ext))
response = test_app.get("/beetles/beetle_id/tweedles")
self.assertEqual(200, response.status_int)
self.assertEqual("value", jsonutils.loads(response.body)['collection'])
def test_resource_extension_with_custom_member_action_and_attr_map(self):
controller = self.ResourceExtensionController()
member = {'custom_member_action': "GET"}

View File

@ -290,6 +290,18 @@ class ResourceTestCase(base.BaseTestCase):
res = resource.delete('', extra_environ=environ)
self.assertEqual(204, res.status_int)
def test_action_status(self):
controller = mock.MagicMock()
controller.test = lambda request: {'foo': 'bar'}
action_status = {'test_200': 200, 'test_201': 201, 'test_204': 204}
resource = webtest.TestApp(
wsgi_resource.Resource(controller,
action_status=action_status))
for action in action_status:
environ = {'wsgiorg.routing_args': (None, {'action': action})}
res = resource.get('', extra_environ=environ)
self.assertEqual(action_status[action], res.status_int)
def _test_error_log_level(self, expected_webob_exc, expect_log_info=False,
use_fault_map=True, exc_raised=None):
if not exc_raised: