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:
Hongbin Lu 2016-06-04 16:24:12 -05:00
parent 4df8a6af00
commit 7620cf1930
10 changed files with 366 additions and 43 deletions

View File

@ -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()

View File

@ -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):

View File

@ -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)

View 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)

View File

@ -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.")

View File

View 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))

View 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))

View 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'})