diff --git a/designate/exceptions.py b/designate/exceptions.py index 45b31fa1..a1c99c37 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -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 diff --git a/designate/objects/adapters/__init__.py b/designate/objects/adapters/__init__.py new file mode 100644 index 00000000..ff4bd784 --- /dev/null +++ b/designate/objects/adapters/__init__.py @@ -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 diff --git a/designate/objects/adapters/api_v1/__init__.py b/designate/objects/adapters/api_v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/designate/objects/adapters/api_v1/base.py b/designate/objects/adapters/api_v1/base.py new file mode 100644 index 00000000..09c3b96f --- /dev/null +++ b/designate/objects/adapters/api_v1/base.py @@ -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) diff --git a/designate/objects/adapters/api_v2/__init__.py b/designate/objects/adapters/api_v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/designate/objects/adapters/api_v2/base.py b/designate/objects/adapters/api_v2/base.py new file mode 100644 index 00000000..988171a2 --- /dev/null +++ b/designate/objects/adapters/api_v2/base.py @@ -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) diff --git a/designate/objects/adapters/base.py b/designate/objects/adapters/base.py new file mode 100644 index 00000000..e13d893b --- /dev/null +++ b/designate/objects/adapters/base.py @@ -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') diff --git a/designate/tests/test_objects/test_adapters.py b/designate/tests/test_objects/test_adapters.py new file mode 100644 index 00000000..8b906010 --- /dev/null +++ b/designate/tests/test_objects/test_adapters.py @@ -0,0 +1,59 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Author: Kiall Mac Innes +# +# 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)