Implement root controller
This patch remove the usage of WSME because it is deprecated: http://lists.openstack.org/pipermail/openstack-dev/2016-March/ 088658.html . We will manually validate the inputs instead. Change-Id: I976d4c9e67581a12e42b6f46e45b32a84b95d32f
This commit is contained in:
parent
4df8a6af00
commit
7620cf1930
@ -12,23 +12,26 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
class APIBase(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for field in self.fields:
|
||||
if field in kwargs:
|
||||
value = kwargs[field]
|
||||
setattr(self, field, value)
|
||||
|
||||
class APIBase(wtypes.Base):
|
||||
|
||||
created_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is created"""
|
||||
|
||||
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is updated"""
|
||||
def __setattr__(self, field, value):
|
||||
if field in self.fields:
|
||||
validator = self.fields[field]['validate']
|
||||
value = validator(value)
|
||||
super(APIBase, self).__setattr__(field, value)
|
||||
|
||||
def as_dict(self):
|
||||
"""Render this object as a dict of its fields."""
|
||||
return {k: getattr(self, k)
|
||||
for k in self.fields
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset}
|
||||
return {f: getattr(self, f)
|
||||
for f in self.fields
|
||||
if hasattr(self, f)}
|
||||
|
||||
def __json__(self):
|
||||
return self.as_dict()
|
||||
|
@ -14,9 +14,9 @@
|
||||
# under the License.
|
||||
|
||||
import pecan
|
||||
from wsme import types as wtypes
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import types
|
||||
|
||||
|
||||
def build_url(resource, resource_args, bookmark=False, base_url=None):
|
||||
@ -34,21 +34,27 @@ def build_url(resource, resource_args, bookmark=False, base_url=None):
|
||||
class Link(base.APIBase):
|
||||
"""A link representation."""
|
||||
|
||||
href = wtypes.text
|
||||
"""The url of a link."""
|
||||
|
||||
rel = wtypes.text
|
||||
"""The name of a link."""
|
||||
|
||||
type = wtypes.text
|
||||
"""Indicates the type of document/link."""
|
||||
fields = {
|
||||
'href': {
|
||||
'validate': types.Text.validate
|
||||
},
|
||||
'rel': {
|
||||
'validate': types.Text.validate
|
||||
},
|
||||
'type': {
|
||||
'validate': types.Text.validate
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def make_link(rel_name, url, resource, resource_args,
|
||||
bookmark=False, type=wtypes.Unset):
|
||||
bookmark=False, type=None):
|
||||
href = build_url(resource, resource_args,
|
||||
bookmark=bookmark, base_url=url)
|
||||
return Link(href=href, rel=rel_name, type=type)
|
||||
if type is None:
|
||||
return Link(href=href, rel=rel_name)
|
||||
else:
|
||||
return Link(href=href, rel=rel_name, type=type)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
|
@ -10,26 +10,88 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pecan import expose
|
||||
from pecan import redirect
|
||||
from webob.exc import status_map
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers import types
|
||||
|
||||
|
||||
class RootController(object):
|
||||
class Version(base.APIBase):
|
||||
"""An API version representation."""
|
||||
|
||||
@expose(generic=True, template='index.html')
|
||||
def index(self):
|
||||
return dict()
|
||||
fields = {
|
||||
'id': {
|
||||
'validate': types.Text.validate
|
||||
},
|
||||
'links': {
|
||||
'validate': types.List(types.Custom(link.Link)).validate
|
||||
},
|
||||
}
|
||||
|
||||
@index.when(method='POST')
|
||||
def index_post(self, q):
|
||||
redirect('http://pecan.readthedocs.org/en/latest/search.html?q=%s' % q)
|
||||
@staticmethod
|
||||
def convert(id):
|
||||
version = Version()
|
||||
version.id = id
|
||||
version.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||
id, '', bookmark=True)]
|
||||
return version
|
||||
|
||||
@expose('error.html')
|
||||
def error(self, status):
|
||||
try:
|
||||
status = int(status)
|
||||
except ValueError: # pragma: no cover
|
||||
status = 500
|
||||
message = getattr(status_map.get(status), 'explanation', '')
|
||||
return dict(status=status, message=message)
|
||||
|
||||
class Root(base.APIBase):
|
||||
|
||||
fields = {
|
||||
'id': {
|
||||
'validate': types.Text.validate
|
||||
},
|
||||
'description': {
|
||||
'validate': types.Text.validate
|
||||
},
|
||||
'versions': {
|
||||
'validate': types.List(types.Custom(Version)).validate
|
||||
},
|
||||
'default_version': {
|
||||
'validate': types.Custom(Version).validate
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def convert():
|
||||
root = Root()
|
||||
root.name = "OpenStack Zun API"
|
||||
root.description = ("Zun is an OpenStack project which aims to "
|
||||
"provide container management.")
|
||||
root.versions = [Version.convert('v1')]
|
||||
root.default_version = Version.convert('v1')
|
||||
return root
|
||||
|
||||
|
||||
class RootController(rest.RestController):
|
||||
|
||||
_versions = ['v1']
|
||||
"""All supported API versions"""
|
||||
|
||||
_default_version = 'v1'
|
||||
"""The default API version"""
|
||||
|
||||
# v1 = v1.Controller()
|
||||
|
||||
@pecan.expose('json')
|
||||
def get(self):
|
||||
# NOTE: The reason why convert() it's being called for every
|
||||
# request is because we need to get the host url from
|
||||
# the request object to make the links.
|
||||
return Root.convert()
|
||||
|
||||
@pecan.expose()
|
||||
def _route(self, args):
|
||||
"""Overrides the default routing behavior.
|
||||
|
||||
It redirects the request to the default version of the zun API
|
||||
if the version number is not specified in the url.
|
||||
"""
|
||||
|
||||
if args[0] and args[0] not in self._versions:
|
||||
args = [self._default_version] + args
|
||||
return super(RootController, self)._route(args)
|
||||
|
61
zun/api/controllers/types.py
Normal file
61
zun/api/controllers/types.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from zun.common import exception
|
||||
from zun.common.i18n import _LE
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Text(object):
|
||||
type_name = 'Text'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value):
|
||||
return value
|
||||
|
||||
|
||||
class Custom(object):
|
||||
def __init__(self, user_class):
|
||||
super(Custom, self).__init__()
|
||||
self.user_class = user_class
|
||||
self.type_name = self.user_class.__name__
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, self.user_class):
|
||||
try:
|
||||
value = self.user_class(**value)
|
||||
except Exception:
|
||||
LOG.exception(_LE('Failed to validate received value'))
|
||||
raise exception.InvalidValue(value=value, type=self.type_name)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class List(object):
|
||||
def __init__(self, type):
|
||||
super(List, self).__init__()
|
||||
self.type = type
|
||||
self.type_name = 'List(%s)' % self.type.type_name
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise exception.InvalidValue(value=value, type=self.type_name)
|
||||
|
||||
try:
|
||||
return [self.type.validate(v) for v in value]
|
||||
except Exception:
|
||||
LOG.exception(_LE('Failed to validate received value'))
|
||||
raise exception.InvalidValue(value=value, type=self.type_name)
|
@ -268,6 +268,10 @@ class Invalid(ZunException):
|
||||
code = 400
|
||||
|
||||
|
||||
class InvalidValue(Invalid):
|
||||
message = _("Received value '%(value)s' is invalid for type %(type)s.")
|
||||
|
||||
|
||||
class InvalidUUID(Invalid):
|
||||
message = _("Expected a uuid but received %(uuid)s.")
|
||||
|
||||
|
0
zun/tests/unit/api/__init__.py
Normal file
0
zun/tests/unit/api/__init__.py
Normal file
0
zun/tests/unit/api/controllers/__init__.py
Normal file
0
zun/tests/unit/api/controllers/__init__.py
Normal file
85
zun/tests/unit/api/controllers/test_base.py
Normal file
85
zun/tests/unit/api/controllers/test_base.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import collections
|
||||
import six
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.tests import base as test_base
|
||||
|
||||
|
||||
class TestAPIBase(test_base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAPIBase, self).setUp()
|
||||
|
||||
class TestAPI(base.APIBase):
|
||||
fields = {
|
||||
'test': {
|
||||
'validate': lambda v: v
|
||||
},
|
||||
}
|
||||
|
||||
self.test_api_cls = TestAPI
|
||||
|
||||
def test_assign_field(self):
|
||||
test_api = self.test_api_cls()
|
||||
test_api.test = 'test_value'
|
||||
|
||||
expected_value = {
|
||||
'test': 'test_value',
|
||||
}
|
||||
self.assertEqual(test_api.__json__(), expected_value)
|
||||
|
||||
def test_no_field_assigned(self):
|
||||
test_api = self.test_api_cls()
|
||||
expected_value = {}
|
||||
self.assertEqual(test_api.__json__(), expected_value)
|
||||
|
||||
def test_assign_field_in_constructor(self):
|
||||
test_api = self.test_api_cls(test='test_value')
|
||||
expected_value = {
|
||||
'test': 'test_value',
|
||||
}
|
||||
self.assertEqual(test_api.__json__(), expected_value)
|
||||
|
||||
def test_assign_nonexist_field(self):
|
||||
test_api = self.test_api_cls()
|
||||
test_api.nonexist = 'test_value'
|
||||
|
||||
expected_value = {}
|
||||
self.assertEqual(test_api.__json__(), expected_value)
|
||||
|
||||
def test_assign_multiple_fields(self):
|
||||
class TestAPI(base.APIBase):
|
||||
fields = {
|
||||
'test': {
|
||||
'validate': lambda v: v
|
||||
},
|
||||
'test2': {
|
||||
'validate': lambda v: v
|
||||
},
|
||||
}
|
||||
|
||||
test_api = TestAPI()
|
||||
test_api.test = 'test_value'
|
||||
test_api.test2 = 'test_value2'
|
||||
test_api.test3 = 'test_value3'
|
||||
|
||||
expected_value = collections.OrderedDict([
|
||||
('test', 'test_value'),
|
||||
('test2', 'test_value2'),
|
||||
])
|
||||
actual_value = collections.OrderedDict(
|
||||
sorted(test_api.as_dict().items()))
|
||||
self.assertEqual(six.text_type(actual_value),
|
||||
six.text_type(expected_value))
|
33
zun/tests/unit/api/controllers/test_link.py
Normal file
33
zun/tests/unit/api/controllers/test_link.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import six
|
||||
|
||||
from zun.api.controllers import link as link_module
|
||||
from zun.tests import base as test_base
|
||||
|
||||
|
||||
class TestLink(test_base.BaseTestCase):
|
||||
|
||||
def test_make_link(self):
|
||||
link = link_module.Link.make_link(
|
||||
'self', 'http://localhost:8080', 'v1', '',
|
||||
bookmark=True)
|
||||
|
||||
ordered_link = collections.OrderedDict(sorted(link.as_dict().items()))
|
||||
expected_value = collections.OrderedDict([
|
||||
('href', 'http://localhost:8080/v1/'),
|
||||
('rel', 'self')
|
||||
])
|
||||
self.assertEqual(six.text_type(ordered_link),
|
||||
six.text_type(expected_value))
|
69
zun/tests/unit/api/controllers/test_types.py
Normal file
69
zun/tests/unit/api/controllers/test_types.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import types
|
||||
from zun.common import exception
|
||||
from zun.tests import base as test_base
|
||||
|
||||
|
||||
class TestTypes(test_base.BaseTestCase):
|
||||
|
||||
def test_text(self):
|
||||
self.assertEqual('test_value', types.Text.validate('test_value'))
|
||||
|
||||
def test_custom(self):
|
||||
class TestAPI(base.APIBase):
|
||||
fields = {
|
||||
'test': {
|
||||
'validate': lambda v: v
|
||||
},
|
||||
}
|
||||
|
||||
test_type = types.Custom(TestAPI)
|
||||
value = TestAPI(test='test_value')
|
||||
value = test_type.validate(value)
|
||||
self.assertIsInstance(value, TestAPI)
|
||||
self.assertEqual(value.as_dict(), {'test': 'test_value'})
|
||||
|
||||
test_type = types.Custom(TestAPI)
|
||||
value = test_type.validate({'test': 'test_value'})
|
||||
self.assertIsInstance(value, TestAPI)
|
||||
self.assertEqual(value.as_dict(), {'test': 'test_value'})
|
||||
|
||||
self.assertRaises(
|
||||
exception.InvalidValue,
|
||||
test_type.validate, 'invalid_value')
|
||||
|
||||
def test_list_with_text_type(self):
|
||||
list_type = types.List(types.Text)
|
||||
value = list_type.validate(['test1', 'test2'])
|
||||
self.assertEqual(value, ['test1', 'test2'])
|
||||
|
||||
self.assertRaises(
|
||||
exception.InvalidValue,
|
||||
list_type.validate, 'invalid_value')
|
||||
|
||||
def test_list_with_custom_type(self):
|
||||
class TestAPI(base.APIBase):
|
||||
fields = {
|
||||
'test': {
|
||||
'validate': lambda v: v
|
||||
},
|
||||
}
|
||||
|
||||
list_type = types.List(types.Custom(TestAPI))
|
||||
value = [{'test': 'test_value'}]
|
||||
value = list_type.validate(value)
|
||||
self.assertIsInstance(value, list)
|
||||
self.assertIsInstance(value[0], TestAPI)
|
||||
self.assertEqual(value[0].as_dict(), {'test': 'test_value'})
|
Loading…
Reference in New Issue
Block a user