Add basic validation functionality to DesignateObjects
In this commit, we add support for basic (i.e. non-nested) validation of DesignateObjects. Future commits will enhance this to support nested validation, as well as providing schemas for each of the existing Objects Change-Id: Ib41e51d70c942332ff872f348d0d5d53fb32d10f Blueprint: validation-cleanup
This commit is contained in:
parent
282d22f350
commit
4964a91f11
@ -44,3 +44,5 @@ from designate.objects.server import Server, ServerList # noqa
|
||||
from designate.objects.tenant import Tenant, TenantList # noqa
|
||||
from designate.objects.tld import Tld, TldList # noqa
|
||||
from designate.objects.tsigkey import TsigKey, TsigKeyList # noqa
|
||||
from designate.objects.validation_error import ValidationError # noqa
|
||||
from designate.objects.validation_error import ValidationErrorList # noqa
|
||||
|
@ -16,7 +16,13 @@ import copy
|
||||
|
||||
import six
|
||||
|
||||
from designate.openstack.common import log as logging
|
||||
from designate import exceptions
|
||||
from designate.schema import validators
|
||||
from designate.schema import format
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotSpecifiedSentinel:
|
||||
@ -64,6 +70,27 @@ def make_class_properties(cls):
|
||||
setattr(cls, field, property(getter, setter))
|
||||
|
||||
|
||||
def make_class_validator(cls):
|
||||
schema = {
|
||||
'$schema': 'http://json-schema.org/draft-04/hyper-schema',
|
||||
'title': cls.obj_name(),
|
||||
'description': 'Designate %s Object' % cls.obj_name(),
|
||||
'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'required': [],
|
||||
'properties': {},
|
||||
}
|
||||
|
||||
for name, properties in cls.FIELDS.items():
|
||||
schema['properties'][name] = properties.get('schema', {})
|
||||
|
||||
if properties.get('required', False):
|
||||
schema['required'].append(name)
|
||||
|
||||
cls._obj_validator = validators.Draft4Validator(
|
||||
schema, format_checker=format.draft4_format_checker)
|
||||
|
||||
|
||||
class DesignateObjectMetaclass(type):
|
||||
def __init__(cls, names, bases, dict_):
|
||||
if not hasattr(cls, '_obj_classes'):
|
||||
@ -73,6 +100,7 @@ class DesignateObjectMetaclass(type):
|
||||
return
|
||||
|
||||
make_class_properties(cls)
|
||||
make_class_validator(cls)
|
||||
|
||||
# Add a reference to the finished class into the _obj_classes
|
||||
# dictionary, allowing us to lookup classes by their name later - this
|
||||
@ -132,6 +160,11 @@ class DesignateObject(object):
|
||||
"""
|
||||
return cls.__name__
|
||||
|
||||
@classmethod
|
||||
def obj_get_schema(cls):
|
||||
"""Returns the JSON Schema for this Object."""
|
||||
return cls._obj_validator.schema
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._obj_changes = set()
|
||||
self._obj_original_values = dict()
|
||||
@ -140,7 +173,8 @@ class DesignateObject(object):
|
||||
if name in self.FIELDS.keys():
|
||||
setattr(self, name, value)
|
||||
else:
|
||||
raise TypeError("'%s' is an invalid keyword argument" % name)
|
||||
raise TypeError("__init__() got an unexpected keyword "
|
||||
"argument '%(name)s'" % {'name': name})
|
||||
|
||||
def to_primitive(self):
|
||||
"""
|
||||
@ -184,6 +218,32 @@ class DesignateObject(object):
|
||||
for k, v in values.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Returns True if the Object is valid."""
|
||||
return self._obj_validator.is_valid(self.to_dict())
|
||||
|
||||
def validate(self):
|
||||
# NOTE(kiall): We make use of the Object registry here in order to
|
||||
# avoid an impossible circular import.
|
||||
ValidationErrorList = self.obj_cls_from_name('ValidationErrorList')
|
||||
ValidationError = self.obj_cls_from_name('ValidationError')
|
||||
|
||||
values = self.to_dict()
|
||||
errors = ValidationErrorList()
|
||||
|
||||
LOG.debug("Validating '%(name)s' object with values: %(values)r", {
|
||||
'name': self.obj_name(),
|
||||
'values': values,
|
||||
})
|
||||
|
||||
for error in self._obj_validator.iter_errors(values):
|
||||
errors.append(ValidationError.from_js_error(error))
|
||||
|
||||
if len(errors) > 0:
|
||||
raise exceptions.InvalidObject("Provided object does not match "
|
||||
"schema", errors=errors)
|
||||
|
||||
def obj_attr_is_set(self, name):
|
||||
"""
|
||||
Return True or False depending of if a particular attribute has had
|
||||
@ -425,7 +485,31 @@ class PersistentObjectMixin(object):
|
||||
|
||||
This adds the fields that we use in common for all persistent objects.
|
||||
"""
|
||||
FIELDS = {'id': {}, 'created_at': {}, 'updated_at': {}, 'version': {}}
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid',
|
||||
}
|
||||
},
|
||||
'created_at': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'date-time',
|
||||
}
|
||||
},
|
||||
'updated_at': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'format': 'date-time',
|
||||
}
|
||||
},
|
||||
'version': {
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SoftDeleteObjectMixin(object):
|
||||
@ -434,4 +518,16 @@ class SoftDeleteObjectMixin(object):
|
||||
|
||||
This adds the fields that we use in common for all soft-deleted objects.
|
||||
"""
|
||||
FIELDS = {'deleted': {}, 'deleted_at': {}}
|
||||
FIELDS = {
|
||||
'deleted': {
|
||||
'schema': {
|
||||
'type': ['string', 'integer'],
|
||||
}
|
||||
},
|
||||
'deleted_at': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'format': 'date-time',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
designate/objects/validation_error.py
Normal file
43
designate/objects/validation_error.py
Normal file
@ -0,0 +1,43 @@
|
||||
# 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 designate.objects import base
|
||||
|
||||
|
||||
class ValidationError(base.DesignateObject):
|
||||
FIELDS = {
|
||||
'relative_path': {},
|
||||
'absolute_path': {},
|
||||
'message': {},
|
||||
'validator': {},
|
||||
'validator_value': {},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_js_error(cls, js_error):
|
||||
"""Convert a JSON Schema ValidationError instance into a
|
||||
ValidationError instance.
|
||||
"""
|
||||
|
||||
e = cls()
|
||||
e.relative_path = ".".join([str(x) for x in js_error.relative_path])
|
||||
e.absolute_path = ".".join([str(x) for x in js_error.absolute_path])
|
||||
e.message = js_error.message
|
||||
e.validator = js_error.validator
|
||||
e.validator_value = js_error.validator_value
|
||||
|
||||
return e
|
||||
|
||||
|
||||
class ValidationErrorList(base.ListObjectMixin, base.DesignateObject):
|
||||
LIST_ITEM_TYPE = ValidationError
|
@ -146,6 +146,9 @@ class SQLAlchemy(object):
|
||||
return query
|
||||
|
||||
def _create(self, table, obj, exc_dup, skip_values=None):
|
||||
# Ensure the Object is valid
|
||||
obj.validate()
|
||||
|
||||
values = obj.obj_get_changes()
|
||||
|
||||
if skip_values is not None:
|
||||
@ -234,6 +237,9 @@ class SQLAlchemy(object):
|
||||
|
||||
def _update(self, context, table, obj, exc_dup, exc_notfound,
|
||||
skip_values=None):
|
||||
# Ensure the Object is valid
|
||||
obj.validate()
|
||||
|
||||
values = obj.obj_get_changes()
|
||||
|
||||
if skip_values is not None:
|
||||
|
@ -20,6 +20,7 @@ import testtools
|
||||
from designate.openstack.common import log as logging
|
||||
from designate import tests
|
||||
from designate import objects
|
||||
from designate import exceptions
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -41,6 +42,18 @@ class TestObjectList(objects.ListObjectMixin, objects.DesignateObject):
|
||||
pass
|
||||
|
||||
|
||||
class TestValidatableObject(objects.DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid',
|
||||
},
|
||||
'required': True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class DesignateObjectTest(tests.TestCase):
|
||||
def test_obj_cls_from_name(self):
|
||||
cls = objects.DesignateObject.obj_cls_from_name('TestObject')
|
||||
@ -184,6 +197,71 @@ class DesignateObjectTest(tests.TestCase):
|
||||
}
|
||||
self.assertEqual(expected, primitive)
|
||||
|
||||
def test_to_dict(self):
|
||||
obj = TestObject(id='MyID')
|
||||
|
||||
# Ensure only the id attribute is returned
|
||||
dict_ = obj.to_dict()
|
||||
expected = {
|
||||
'id': 'MyID',
|
||||
}
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
# Set the name attribute to a None value
|
||||
obj.name = None
|
||||
|
||||
# Ensure both the id and name attributes are returned
|
||||
dict_ = obj.to_dict()
|
||||
expected = {
|
||||
'id': 'MyID',
|
||||
'name': None,
|
||||
}
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
def test_to_dict_recursive(self):
|
||||
obj = TestObject(id='MyID', nested=TestObject(id='MyID-Nested'))
|
||||
|
||||
# Ensure only the id attribute is returned
|
||||
dict_ = obj.to_dict()
|
||||
expected = {
|
||||
'id': 'MyID',
|
||||
'nested': {
|
||||
'id': 'MyID-Nested',
|
||||
},
|
||||
}
|
||||
|
||||
self.assertEqual(expected, dict_)
|
||||
|
||||
def test_is_valid(self):
|
||||
obj = TestValidatableObject(id='MyID')
|
||||
|
||||
# ID should be a UUID, So - Not Valid.
|
||||
self.assertFalse(obj.is_valid)
|
||||
|
||||
# Correct the ID field
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
|
||||
# ID is now a UUID, So - Valid.
|
||||
self.assertTrue(obj.is_valid)
|
||||
|
||||
def test_validate(self):
|
||||
obj = TestValidatableObject()
|
||||
|
||||
# ID is required, so the object is not valid
|
||||
with testtools.ExpectedException(exceptions.InvalidObject):
|
||||
obj.validate()
|
||||
|
||||
# Set the ID field to an invalid value
|
||||
obj.id = 'MyID'
|
||||
|
||||
# ID is now set, but to an invalid value, still invalid
|
||||
with testtools.ExpectedException(exceptions.InvalidObject):
|
||||
obj.validate()
|
||||
|
||||
# Set the ID field to a valid value
|
||||
obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0'
|
||||
obj.validate()
|
||||
|
||||
def test_obj_attr_is_set(self):
|
||||
obj = TestObject()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user