From e549217a407474918046a4d66624cfa20375bffb Mon Sep 17 00:00:00 2001 From: Kiall Mac Innes Date: Tue, 21 Oct 2014 14:01:54 +0100 Subject: [PATCH] Support Nested/Recursive Object Validations This allows for Objects to reference other objects's schemas for use by JSON-Schema validations. Lookups are performed via the Object Registry using a pseudo URI scheme of "obj://" e.g. '$ref': 'obj://TestObject#/' Is a JSON-Schema reference, pointing towards the root of the Schema generated for the "TestObject" object. Change-Id: Id35c38536eb0d21bb66d3ca720d18e7f6ee733b5 Blueprint: validation-cleanup --- designate/objects/base.py | 19 +++++++- designate/tests/test_objects/test_base.py | 55 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/designate/objects/base.py b/designate/objects/base.py index 29d02444..d5681612 100644 --- a/designate/objects/base.py +++ b/designate/objects/base.py @@ -13,8 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. import copy +import urlparse import six +import jsonschema from designate.openstack.common import log as logging from designate import exceptions @@ -70,6 +72,18 @@ def make_class_properties(cls): setattr(cls, field, property(getter, setter)) +def _schema_ref_resolver(uri): + """ + Fetches an DesignateObject's schema from a JSON Schema Reference URI + + Sample URI: obj://ObjectName#/subpathA/subpathB + """ + obj_name = urlparse.urlsplit(uri).netloc + obj = DesignateObject.obj_cls_from_name(obj_name) + + return obj.obj_get_schema() + + def make_class_validator(cls): schema = { '$schema': 'http://json-schema.org/draft-04/hyper-schema', @@ -87,8 +101,11 @@ def make_class_validator(cls): if properties.get('required', False): schema['required'].append(name) + resolver = jsonschema.RefResolver.from_schema( + schema, handlers={'obj': _schema_ref_resolver}) + cls._obj_validator = validators.Draft4Validator( - schema, format_checker=format.draft4_format_checker) + schema, resolver=resolver, format_checker=format.draft4_format_checker) class DesignateObjectMetaclass(type): diff --git a/designate/tests/test_objects/test_base.py b/designate/tests/test_objects/test_base.py index 55906d22..b8c3aca0 100644 --- a/designate/tests/test_objects/test_base.py +++ b/designate/tests/test_objects/test_base.py @@ -51,6 +51,11 @@ class TestValidatableObject(objects.DesignateObject): }, 'required': True, }, + 'nested': { + 'schema': { + '$ref': 'obj://TestValidatableObject#/' + } + } } @@ -244,6 +249,26 @@ class DesignateObjectTest(tests.TestCase): # ID is now a UUID, So - Valid. self.assertTrue(obj.is_valid) + def test_is_valid_recursive(self): + obj = TestValidatableObject( + id='MyID', + nested=TestValidatableObject(id='MyID')) + + # ID should be a UUID, So - Not Valid. + self.assertFalse(obj.is_valid) + + # Correct the outer objects ID field + obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0' + + # Outer ID is now a UUID, Nested ID is Not. So - Invalid. + self.assertFalse(obj.is_valid) + + # Correct the nested objects ID field + obj.nested.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0' + + # Outer and Nested IDs are now UUIDs. So - Valid. + self.assertTrue(obj.is_valid) + def test_validate(self): obj = TestValidatableObject() @@ -262,6 +287,36 @@ class DesignateObjectTest(tests.TestCase): obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0' obj.validate() + def test_validate_recursive(self): + obj = TestValidatableObject( + id='MyID', + nested=TestValidatableObject(id='MyID')) + + # ID should be a UUID, So - Invalid. + with testtools.ExpectedException(exceptions.InvalidObject): + obj.validate() + + # Correct the outer objects ID field + obj.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0' + + # Outer ID is now set, Inner ID is not, still invalid. + e = self.assertRaises(exceptions.InvalidObject, obj.validate) + + # Ensure we have exactly one error and fetch it + self.assertEqual(1, len(e.errors)) + error = e.errors.pop(0) + + # Ensure the format validator has triggered the failure. + self.assertEqual('format', error.validator) + + # Ensure the nested ID field has triggered the failure. + self.assertEqual('nested.id', error.absolute_path) + self.assertEqual('nested.id', error.relative_path) + + # Set the Nested ID field to a valid value + obj.nested.id = 'ffded5c4-e4f6-4e02-a175-48e13c5c12a0' + obj.validate() + def test_obj_attr_is_set(self): obj = TestObject()