JSON Schema get_schema implementation for last few fields

This implements the get_schema() method of the following FieldType:
Object, IPV6Address, NonNegativeInteger, NonNegativeFloat,
IPV4AndV6Address, Dict(fixed from last patch)

implements bp json-schema-for-versioned-object

Change-Id: I6bce3ba7bb32ed2dd8ed6e7f313411bbfef5eff0
This commit is contained in:
Julian Sy 2016-08-24 21:35:43 +00:00 committed by Julian Sy
parent b9a38b4a9a
commit 5b44fafe6a
5 changed files with 217 additions and 68 deletions

View File

@ -327,45 +327,11 @@ class VersionedObject(object):
@classmethod
def to_json_schema(cls):
obj_name = cls.obj_name()
field_schemas = {key: field.get_schema()
for key, field in cls.fields.items()}
required_fields = [name for name, schema in
sorted(field_schemas.items())]
namespace_key = cls._obj_primitive_key('namespace')
name_key = cls._obj_primitive_key('name')
version_key = cls._obj_primitive_key('version')
data_key = cls._obj_primitive_key('data')
changes_key = cls._obj_primitive_key('changes')
schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': obj_name,
'type': 'object',
'properties': {
namespace_key: {
'type': 'string'
},
name_key: {
'type': 'string'
},
version_key: {
'type': 'string'
},
changes_key: {
'type': 'array',
'items': {
'type': 'string'
}
},
data_key: {
'type': 'object',
'description': 'fields of %s' % (obj_name),
'properties': field_schemas,
'required': required_fields
},
},
'required': [namespace_key, name_key, version_key, data_key]
}
schema.update(obj_fields.Object(obj_name).get_schema())
return schema
@classmethod

View File

@ -408,6 +408,9 @@ class NonNegativeInteger(FieldType):
raise ValueError(_('Value must be >= 0 for field %s') % attr)
return v
def get_schema(self):
return {'type': ['integer'], 'minimum': 0}
class Float(FieldType):
def coerce(self, obj, attr, value):
@ -425,6 +428,9 @@ class NonNegativeFloat(FieldType):
raise ValueError(_('Value must be >= 0 for field %s') % attr)
return v
def get_schema(self):
return {'type': ['number'], 'minimum': 0}
class Boolean(FieldType):
@staticmethod
@ -535,7 +541,8 @@ class IPV4AndV6Address(IPAddress):
return result
def get_schema(self):
return {'type': ['string'], 'format': 'ipv6'}
return {'oneOf': [IPV4Address().get_schema(),
IPV6Address().get_schema()]}
class IPNetwork(IPAddress):
@ -682,7 +689,8 @@ class Dict(CompoundFieldType):
for key, val in sorted(value.items())]))
def get_schema(self):
return {'type': ['object']}
return {'type': ['object'],
'additionalProperties': self._element_type.get_schema()}
class DictProxyField(object):
@ -819,6 +827,54 @@ class Object(FieldType):
return '%s%s' % (self._obj_name, ident)
def get_schema(self):
from oslo_versionedobjects import base as obj_base
obj_classes = obj_base.VersionedObjectRegistry.obj_classes()
if self._obj_name in obj_classes:
cls = obj_classes[self._obj_name][0]
namespace_key = cls._obj_primitive_key('namespace')
name_key = cls._obj_primitive_key('name')
version_key = cls._obj_primitive_key('version')
data_key = cls._obj_primitive_key('data')
changes_key = cls._obj_primitive_key('changes')
field_schemas = {key: field.get_schema()
for key, field in cls.fields.items()}
required_fields = [key for key, field in sorted(cls.fields.items())
if not field.nullable]
schema = {
'type': 'object',
'properties': {
namespace_key: {
'type': 'string'
},
name_key: {
'type': 'string'
},
version_key: {
'type': 'string'
},
changes_key: {
'type': 'array',
'items': {
'type': 'string'
}
},
data_key: {
'type': 'object',
'description': 'fields of %s' % self._obj_name,
'properties': field_schemas,
},
},
'required': [namespace_key, name_key, version_key, data_key]
}
if required_fields:
schema['properties'][data_key]['required'] = required_fields
return schema
else:
raise exception.UnsupportedObjectError(objtype=self._obj_name)
class AutoTypedField(Field):
AUTO_TYPE = None

View File

@ -302,7 +302,6 @@ class TestUUID(TestField):
self.assertEqual(['string'], schema['type'])
self.assertEqual(False, schema['readonly'])
pattern = schema['pattern']
print(self.coerce_good_values)
for _, valid_val in self.coerce_good_values[:4]:
self.assertRegex(valid_val, pattern)
invalid_vals = [x for x in self.coerce_bad_values if type(x) == 'str']
@ -487,6 +486,10 @@ class TestNonNegativeInteger(TestField):
self.to_primitive_values = self.coerce_good_values[0:1]
self.from_primitive_values = self.coerce_good_values[0:1]
def test_get_schema(self):
self.assertEqual({'type': ['integer'], 'readonly': False,
'minimum': 0}, self.field.get_schema())
class TestFloat(TestField):
def setUp(self):
@ -514,6 +517,10 @@ class TestNonNegativeFloat(TestField):
self.to_primitive_values = self.coerce_good_values[0:1]
self.from_primitive_values = self.coerce_good_values[0:1]
def test_get_schema(self):
self.assertEqual({'type': ['number'], 'readonly': False,
'minimum': 0}, self.field.get_schema())
class TestBoolean(TestField):
def setUp(self):
@ -611,7 +618,10 @@ class TestDict(TestField):
self.assertEqual("{key=val}", self.field.stringify({'key': 'val'}))
def test_get_schema(self):
self.assertEqual({'type': ['object'], 'readonly': False},
self.assertEqual({'type': ['object'],
'additionalProperties': {'readonly': False,
'type': ['foo']},
'readonly': False},
self.field.get_schema())
@ -1010,6 +1020,31 @@ class TestObject(TestField):
self.assertEqual('An object of type TestableObject is required '
'in field attr, not a list', six.text_type(ex))
def test_get_schema(self):
self.assertEqual(
{
'properties': {
'versioned_object.changes':
{'items': {'type': 'string'}, 'type': 'array'},
'versioned_object.data': {
'description': 'fields of TestableObject',
'properties':
{'uuid': {'readonly': False, 'type': ['string']}},
'required': ['uuid'],
'type': 'object'},
'versioned_object.name': {'type': 'string'},
'versioned_object.namespace': {'type': 'string'},
'versioned_object.version': {'type': 'string'}
},
'readonly': False,
'required': ['versioned_object.namespace',
'versioned_object.name',
'versioned_object.version',
'versioned_object.data'],
'type': 'object'
},
self.field.get_schema())
class TestIPAddress(TestField):
def setUp(self):
@ -1055,8 +1090,7 @@ class TestIPAddressV6(TestField):
netaddr.IPAddress('::1'))]
self.coerce_bad_values = ['1.2', 'foo', '1.2.3.4']
self.to_primitive_values = [(netaddr.IPAddress('::1'), '::1')]
self.from_primitive_values = [('::1',
netaddr.IPAddress('::1'))]
self.from_primitive_values = [('::1', netaddr.IPAddress('::1'))]
def test_get_schema(self):
self.assertEqual({'type': ['string'], 'readonly': False,
@ -1085,6 +1119,11 @@ class TestIPV4AndV6Address(TestField):
('1.2.3.4',
netaddr.IPAddress('1.2.3.4'))]
def test_get_schema(self):
self.assertEqual({'oneOf': [{'format': 'ipv4', 'type': ['string']},
{'format': 'ipv6', 'type': ['string']}]},
self.field.get_schema())
class TestIPNetwork(TestField):
def setUp(self):

View File

@ -14,6 +14,7 @@
import copy
import datetime
import jsonschema
import logging
import pytz
import six
@ -2065,34 +2066,25 @@ class TestObjectSerializer(_BaseTestCase):
class TestSchemaGeneration(test.TestCase):
class FakeFieldType(fields.FieldType):
pass
@base.VersionedObjectRegistry.register
class FakeObject(base.VersionedObject):
fields = {
'a_boolean': fields.BooleanField(nullable=True),
}
def setUp(self):
super(TestSchemaGeneration, self).setUp()
self.nonNullableField = fields.Field(self.FakeFieldType)
self.nullableField = fields.Field(self.FakeFieldType)
class TestObject(base.VersionedObject):
fields = {'foo': self.nonNullableField,
'bar': self.nullableField}
self.test_class = TestObject
self.nonNullableField.get_schema = \
mock.Mock(return_value={'type': ['fake']})
self.nullableField.get_schema = \
mock.Mock(return_value={'type': ['fake', 'null']})
self.test_class.obj_name = mock.Mock(return_value='TestObject')
@base.VersionedObjectRegistry.register
class FakeComplexObject(base.VersionedObject):
fields = {
'a_dict': fields.DictOfListOfStringsField(),
'an_obj': fields.ObjectField('FakeObject'),
'list_of_objs': fields.ListOfObjectsField('FakeObject'),
}
def test_to_json_schema(self):
schema = self.test_class.to_json_schema()
self.nonNullableField.get_schema.assert_called_once_with()
self.nullableField.get_schema.assert_called_once_with()
schema = self.FakeObject.to_json_schema()
self.assertEqual({
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'TestObject',
'title': 'FakeObject',
'type': 'object',
'properties': {
'versioned_object.namespace': {
@ -2112,18 +2104,113 @@ class TestSchemaGeneration(test.TestCase):
},
'versioned_object.data': {
'type': 'object',
'description': 'fields of TestObject',
'description': 'fields of FakeObject',
'properties': {
'foo': {'type': ['fake']},
'bar': {'type': ['fake', 'null']}
'a_boolean': {
'readonly': False,
'type': ['boolean', 'null']},
},
'required': ['bar', 'foo'],
},
},
'required': ['versioned_object.namespace', 'versioned_object.name',
'versioned_object.version', 'versioned_object.data']
}, schema)
jsonschema.validate(self.FakeObject(a_boolean=True).obj_to_primitive(),
self.FakeObject.to_json_schema())
def test_to_json_schema_complex_object(self):
schema = self.FakeComplexObject.to_json_schema()
expected_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'properties': {
'versioned_object.changes':
{'items': {'type': 'string'}, 'type': 'array'},
'versioned_object.data': {
'description': 'fields of FakeComplexObject',
'properties': {
'a_dict': {
'readonly': False,
'type': ['object'],
'additionalProperties': {
'type': ['array'],
'readonly': False,
'items': {
'type': ['string'],
'readonly': False}}},
'an_obj': {
'properties': {
'versioned_object.changes':
{'items': {'type': 'string'},
'type': 'array'},
'versioned_object.data': {
'description': 'fields of FakeObject',
'properties':
{'a_boolean': {'readonly': False,
'type': ['boolean', 'null']}},
'type': 'object'},
'versioned_object.name': {'type': 'string'},
'versioned_object.namespace':
{'type': 'string'},
'versioned_object.version':
{'type': 'string'}},
'readonly': False,
'required': ['versioned_object.namespace',
'versioned_object.name',
'versioned_object.version',
'versioned_object.data'],
'type': 'object'},
'list_of_objs': {
'items': {
'properties': {
'versioned_object.changes':
{'items': {'type': 'string'},
'type': 'array'},
'versioned_object.data': {
'description': 'fields of FakeObject',
'properties': {
'a_boolean': {
'readonly': False,
'type': ['boolean', 'null']}},
'type': 'object'},
'versioned_object.name':
{'type': 'string'},
'versioned_object.namespace':
{'type': 'string'},
'versioned_object.version':
{'type': 'string'}},
'readonly': False,
'required': ['versioned_object.namespace',
'versioned_object.name',
'versioned_object.version',
'versioned_object.data'],
'type': 'object'},
'readonly': False,
'type': ['array']}},
'required': ['a_dict', 'an_obj', 'list_of_objs'],
'type': 'object'},
'versioned_object.name': {'type': 'string'},
'versioned_object.namespace': {'type': 'string'},
'versioned_object.version': {'type': 'string'}},
'required': ['versioned_object.namespace',
'versioned_object.name',
'versioned_object.version',
'versioned_object.data'],
'title': 'FakeComplexObject',
'type': 'object'}
self.assertEqual(expected_schema, schema)
fake_obj = self.FakeComplexObject(
a_dict={'key1': ['foo', 'bar'],
'key2': ['bar', 'baz']},
an_obj=self.FakeObject(a_boolean=True),
list_of_objs=[self.FakeObject(a_boolean=False),
self.FakeObject(a_boolean=True),
self.FakeObject(a_boolean=False)])
primitives = fake_obj.obj_to_primitive()
jsonschema.validate(primitives, schema)
class TestNamespaceCompatibility(test.TestCase):
def setUp(self):

View File

@ -9,3 +9,4 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
coverage>=3.6 # Apache-2.0
jsonschema>=2.0.0,<3.0.0,!=2.5.0 # MIT