Create DesignateAdapter and surrounding infrastructure
This implements the basic infrastructure for DesignateAdapter and creates the basis for the API v2/v1 Adapter objects These will replace views in both the v1 and v2 APIs, and allow for the validation of API requests to move into the DesignateObjects themselves. Example use is zone = central.get_domain(blah) DesignateAdapter.render('API_v1', zone) // Returns v1 dict, ready to convert to JSON These objects have a registry of sorts, so all calls will be made to DesignateAdapter, and the combination of the format param ('API_v1' in this case) will load the right Adapter and return the right JSON ready dict. Change-Id: I2b77205751675de600248180fafbfe22a2c1d8f5 Blueprint: validation-cleanup
This commit is contained in:
parent
95adcfcc4d
commit
f7252ecdb0
@ -39,6 +39,11 @@ class RelationNotLoaded(Base):
|
||||
error_type = 'relation_not_loaded'
|
||||
|
||||
|
||||
class AdapterNotFound(Base):
|
||||
error_code = 500
|
||||
error_type = 'adapter_not_found'
|
||||
|
||||
|
||||
class NSD4SlaveBackendError(Backend):
|
||||
pass
|
||||
|
||||
|
15
designate/objects/adapters/__init__.py
Normal file
15
designate/objects/adapters/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
# Base Adapter Class
|
||||
from designate.objects.adapters.base import DesignateAdapter # noqa
|
0
designate/objects/adapters/api_v1/__init__.py
Normal file
0
designate/objects/adapters/api_v1/__init__.py
Normal file
59
designate/objects/adapters/api_v1/base.py
Normal file
59
designate/objects/adapters/api_v1/base.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 oslo_log import log as logging
|
||||
|
||||
from designate.objects.adapters import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIv1Adapter(base.DesignateAdapter):
|
||||
|
||||
ADAPTER_FORMAT = 'API_v1'
|
||||
|
||||
#####################
|
||||
# Rendering methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def render(cls, object, *args, **kwargs):
|
||||
return super(APIv1Adapter, cls).render(
|
||||
cls.ADAPTER_FORMAT, object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _render_list(cls, list_object, *args, **kwargs):
|
||||
inner = cls._render_inner_list(list_object, *args, **kwargs)
|
||||
|
||||
return {cls.MODIFICATIONS['options']['collection_name']: inner}
|
||||
|
||||
@classmethod
|
||||
def _render_object(cls, object, *args, **kwargs):
|
||||
return cls._render_inner_object(object, *args, **kwargs)
|
||||
|
||||
#####################
|
||||
# Parsing methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def parse(cls, values, output_object, *args, **kwargs):
|
||||
return super(APIv1Adapter, cls).parse(
|
||||
cls.ADAPTER_FORMAT, values, output_object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _parse_list(cls, values, output_object, *args, **kwargs):
|
||||
return cls._parse_inner_list(values, output_object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _parse_object(cls, values, output_object, *args, **kwargs):
|
||||
return cls._parse_inner_object(values, output_object, *args, **kwargs)
|
0
designate/objects/adapters/api_v2/__init__.py
Normal file
0
designate/objects/adapters/api_v2/__init__.py
Normal file
141
designate/objects/adapters/api_v2/base.py
Normal file
141
designate/objects/adapters/api_v2/base.py
Normal file
@ -0,0 +1,141 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 urllib
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo.config import cfg
|
||||
|
||||
from designate.objects.adapters import base
|
||||
from designate.objects import base as obj_base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
cfg.CONF.import_opt('api_base_uri', 'designate.api', group='service:api')
|
||||
|
||||
|
||||
class APIv2Adapter(base.DesignateAdapter):
|
||||
|
||||
BASE_URI = cfg.CONF['service:api'].api_base_uri.rstrip('/')
|
||||
|
||||
ADAPTER_FORMAT = 'API_v2'
|
||||
|
||||
#####################
|
||||
# Rendering methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def render(cls, object, *args, **kwargs):
|
||||
return super(APIv2Adapter, cls).render(
|
||||
cls.ADAPTER_FORMAT, object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _render_list(cls, list_object, *args, **kwargs):
|
||||
inner = cls._render_inner_list(list_object, *args, **kwargs)
|
||||
outer = {}
|
||||
|
||||
if cls.MODIFICATIONS['options'].get('links', True):
|
||||
outer['links'] = cls._get_collection_links(
|
||||
list_object, kwargs['request'])
|
||||
# Check if we should include metadata
|
||||
if isinstance(list_object, obj_base.PagedListObjectMixin):
|
||||
metadata = {}
|
||||
metadata['total_count'] = list_object.total_count
|
||||
outer['metadata'] = metadata
|
||||
|
||||
outer[cls.MODIFICATIONS['options']['collection_name']] = inner
|
||||
|
||||
return outer
|
||||
|
||||
@classmethod
|
||||
def _render_object(cls, object, *args, **kwargs):
|
||||
inner = cls._render_inner_object(object, *args, **kwargs)
|
||||
|
||||
if cls.MODIFICATIONS['options'].get('links', True):
|
||||
inner['links'] = cls._get_resource_links(object, kwargs['request'])
|
||||
|
||||
return {cls.MODIFICATIONS['options']['resource_name']: inner}
|
||||
|
||||
#####################
|
||||
# Parsing methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def parse(cls, values, output_object, *args, **kwargs):
|
||||
return super(APIv2Adapter, cls).parse(
|
||||
cls.ADAPTER_FORMAT, values, output_object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _parse_list(cls, values, output_object, *args, **kwargs):
|
||||
|
||||
return cls._parse_inner_list(values, output_object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _parse_object(cls, values, output_object, *args, **kwargs):
|
||||
inner = values[cls.MODIFICATIONS['options']['resource_name']]
|
||||
return cls._parse_inner_object(inner, output_object, *args, **kwargs)
|
||||
|
||||
#####################
|
||||
# Link methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def _get_resource_links(cls, object, request):
|
||||
return {'self': '%s%s/%s' %
|
||||
(cls.BASE_URI, cls._get_path(request), object.id)}
|
||||
|
||||
@classmethod
|
||||
def _get_path(cls, request):
|
||||
path = request.path.lstrip('/').split('/')
|
||||
item_path = ''
|
||||
for part in path:
|
||||
if part == cls.MODIFICATIONS['options']['collection_name']:
|
||||
item_path += '/' + part
|
||||
return item_path
|
||||
else:
|
||||
item_path += '/' + part
|
||||
|
||||
@classmethod
|
||||
def _get_collection_links(cls, list, request):
|
||||
|
||||
links = {
|
||||
'self': cls._get_collection_href(request)
|
||||
}
|
||||
params = request.GET
|
||||
if 'limit' in params and int(params['limit']) == len(list):
|
||||
links['next'] = cls._get_next_href(request, list)
|
||||
|
||||
return links
|
||||
|
||||
@classmethod
|
||||
def _get_collection_href(cls, request, extra_params=None):
|
||||
params = request.GET
|
||||
|
||||
if extra_params is not None:
|
||||
params.update(extra_params)
|
||||
|
||||
href = "%s%s?%s" % (
|
||||
cls.BASE_URI,
|
||||
cls._get_path(request),
|
||||
urllib.urlencode(params))
|
||||
|
||||
return href.rstrip('?')
|
||||
|
||||
@classmethod
|
||||
def _get_next_href(cls, request, items):
|
||||
# Prepare the extra params
|
||||
extra_params = {
|
||||
'marker': items[-1]['id']
|
||||
}
|
||||
|
||||
return cls._get_collection_href(request, extra_params)
|
200
designate/objects/adapters/base.py
Normal file
200
designate/objects/adapters/base.py
Normal file
@ -0,0 +1,200 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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
|
||||
|
||||
import six
|
||||
|
||||
from designate import objects
|
||||
from designate import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DesignateObjectAdapterMetaclass(type):
|
||||
|
||||
def __init__(cls, names, bases, dict_):
|
||||
if not hasattr(cls, '_adapter_classes'):
|
||||
cls._adapter_classes = {}
|
||||
return
|
||||
|
||||
key = '%s:%s' % (cls.adapter_format(), cls.adapter_object())
|
||||
|
||||
if key not in cls._adapter_classes:
|
||||
cls._adapter_classes[key] = cls
|
||||
else:
|
||||
raise Exception(
|
||||
"Duplicate DesignateAdapterObject with"
|
||||
" format '%(format)s and object %(object)s'" %
|
||||
{'format': cls.adapter_format(),
|
||||
'object': cls.adapter_object()}
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(DesignateObjectAdapterMetaclass)
|
||||
class DesignateAdapter(object):
|
||||
"""docstring for DesignateObjectAdapter"""
|
||||
|
||||
ADAPTER_OBJECT = objects.DesignateObject
|
||||
|
||||
@classmethod
|
||||
def adapter_format(cls):
|
||||
return cls.ADAPTER_FORMAT
|
||||
|
||||
@classmethod
|
||||
def adapter_object(cls):
|
||||
return cls.ADAPTER_OBJECT.obj_name()
|
||||
|
||||
@classmethod
|
||||
def get_object_adapter(cls, format_, object):
|
||||
key = '%s:%s' % (format_, object.obj_name())
|
||||
try:
|
||||
return cls._adapter_classes[key]
|
||||
except KeyError as e:
|
||||
keys = e.message.split(':')
|
||||
msg = "Adapter for %(object)s to format %(format)s not found" % {
|
||||
"object": keys[1],
|
||||
"format": keys[0]
|
||||
}
|
||||
raise exceptions.AdapterNotFound(msg)
|
||||
|
||||
#####################
|
||||
# Rendering methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def render(cls, format_, object, *args, **kwargs):
|
||||
|
||||
if isinstance(object, objects.ListObjectMixin):
|
||||
# type_ = 'list'
|
||||
return cls.get_object_adapter(
|
||||
format_, object)._render_list(object, *args, **kwargs)
|
||||
else:
|
||||
# type_ = 'object'
|
||||
return cls.get_object_adapter(
|
||||
format_, object)._render_object(object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _render_inner_object(cls, object, *args, **kwargs):
|
||||
# The dict we will return to be rendered to JSON / output format
|
||||
r_obj = {}
|
||||
# Loop over all fields that are supposed to be output
|
||||
for key, value in cls.MODIFICATIONS['fields'].iteritems():
|
||||
# Get properties for this field
|
||||
field_props = cls.MODIFICATIONS['fields'][key]
|
||||
# Check if it has to be renamed
|
||||
if field_props.get('rename', False):
|
||||
obj = getattr(object, field_props.get('rename'))
|
||||
# if rename is specified we need to change the key
|
||||
obj_key = field_props.get('rename')
|
||||
else:
|
||||
# if not, move on
|
||||
obj = getattr(object, key, None)
|
||||
obj_key = key
|
||||
# Check if this item is a relation (another DesignateObject that
|
||||
# will need to be converted itself
|
||||
if object.FIELDS.get(obj_key, {}).get('relation'):
|
||||
# Get a adapter for the nested object
|
||||
# Get the class the object is and get its adapter, then set
|
||||
# the item in the dict to the output
|
||||
r_obj[key] = cls.get_object_adapter(
|
||||
cls.ADAPTER_FORMAT,
|
||||
object.FIELDS[obj_key].get('relation_cls')).render(
|
||||
obj, *args, **kwargs)
|
||||
else:
|
||||
# Just attach the damn item if there is no weird edge cases
|
||||
r_obj[key] = obj
|
||||
# Send it back
|
||||
return r_obj
|
||||
|
||||
@classmethod
|
||||
def _render_inner_list(cls, list_object, *args, **kwargs):
|
||||
# The list we will return to be rendered to JSON / output format
|
||||
r_list = []
|
||||
# iterate and convert each DesignateObject in the list, and append to
|
||||
# the object we are returning
|
||||
for object in list_object:
|
||||
r_list.append(cls.get_object_adapter(
|
||||
cls.ADAPTER_FORMAT,
|
||||
object.obj_name()).render(object, *args, **kwargs))
|
||||
return r_list
|
||||
|
||||
#####################
|
||||
# Parsing methods #
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def parse(cls, format_, values, output_object, *args, **kwargs):
|
||||
|
||||
if isinstance(output_object, objects.ListObjectMixin):
|
||||
# type_ = 'list'
|
||||
return cls.get_object_adapter(
|
||||
format_,
|
||||
output_object.obj_name())._parse_list(
|
||||
values, output_object, *args, **kwargs)
|
||||
else:
|
||||
# type_ = 'object'
|
||||
return cls.get_object_adapter(
|
||||
format_,
|
||||
output_object.obj_name())._parse_object(
|
||||
values, output_object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _parse_inner_object(cls, values, output_object, *args, **kwargs):
|
||||
error_keys = []
|
||||
|
||||
for key, value in values.iteritems():
|
||||
error_flag = True
|
||||
if key in cls.MODIFICATIONS['fields']:
|
||||
# No rename needed
|
||||
obj_key = key
|
||||
error_flag = False
|
||||
# This item may need to be translated
|
||||
if cls.MODIFICATIONS['fields'][key].get('rename', False):
|
||||
obj_key = cls.MODIFICATIONS['fields'][key].get('rename')
|
||||
|
||||
# Check if the key is a nested object
|
||||
if output_object.FIELDS.get(obj_key, {}).get('relation', False):
|
||||
# Get the right class name
|
||||
obj_class_name = output_object.FIELDS.get(
|
||||
obj_key, {}).get('relation_cls')
|
||||
# Get the an instance of it
|
||||
obj_class = \
|
||||
objects.DesignateObject.obj_cls_from_name(obj_class_name)
|
||||
# Get the adapted object
|
||||
obj = \
|
||||
cls.get_object_adapter(
|
||||
cls.ADAPTER_FORMAT, obj_class_name).parse(
|
||||
value, obj_class)
|
||||
# Set the object on the main object
|
||||
setattr(output_object, obj_key, obj)
|
||||
else:
|
||||
# No nested objects here, just set the value
|
||||
setattr(output_object, obj_key, value)
|
||||
if error_flag:
|
||||
# We got an extra key
|
||||
error_keys.append(key)
|
||||
|
||||
if error_keys:
|
||||
error_message = str.format(
|
||||
'Provided object does not match schema. Keys {0} are not '
|
||||
'valid in the request body',
|
||||
error_keys)
|
||||
|
||||
raise exceptions.InvalidObject(error_message)
|
||||
|
||||
return output_object
|
||||
|
||||
@classmethod
|
||||
def _parse_inner_list(cls, values, output_object, *args, **kwargs):
|
||||
raise exceptions.NotImplemented('List adaption not implemented')
|
59
designate/tests/test_objects/test_adapters.py
Normal file
59
designate/tests/test_objects/test_adapters.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@hp.com>
|
||||
#
|
||||
# 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 oslo_log import log as logging
|
||||
|
||||
from designate import tests
|
||||
from designate import objects
|
||||
from designate.objects import adapters
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DesignateTestAdapter(adapters.DesignateAdapter):
|
||||
ADAPTER_OBJECT = objects.DesignateObject
|
||||
ADAPTER_FORMAT = 'TEST_API'
|
||||
|
||||
MODIFICATIONS = {
|
||||
'fields': {},
|
||||
'options': {}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def render(cls, object, *args, **kwargs):
|
||||
return super(DesignateTestAdapter, cls).render(
|
||||
cls.ADAPTER_FORMAT, object, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _render_list(cls, list_object, *args, **kwargs):
|
||||
inner = cls._render_inner_list(list_object, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
@classmethod
|
||||
def _render_object(cls, object, *args, **kwargs):
|
||||
inner = cls._render_inner_object(object, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class DesignateAdapterTest(tests.TestCase):
|
||||
def test_get_object_adapter(self):
|
||||
adapters.DesignateAdapter.get_object_adapter(
|
||||
'TEST_API', objects.DesignateObject)
|
||||
|
||||
def test_get_object_render(self):
|
||||
adapters.DesignateAdapter.render('TEST_API', objects.DesignateObject)
|
Loading…
Reference in New Issue
Block a user