JSON schema generation for versioned objects

This adds a method that generates a versioned object's fields
in json schema. In addition, each field would also generate
its respective json schema property.

Implements blueprint json-schema-for-versioned-object

Change-Id: I5914606e4db79a15b573cfc4a6b9b8807199e402
This commit is contained in:
Julian Sy 2016-05-19 21:04:48 +00:00 committed by Julian
parent e7b1be0e26
commit 92e7ac7108
4 changed files with 138 additions and 0 deletions

View File

@ -324,6 +324,50 @@ class VersionedObject(object):
except AttributeError:
return False
@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]
}
return schema
@classmethod
def obj_name(cls):
"""Return the object's name

View File

@ -127,6 +127,9 @@ class FieldType(AbstractFieldType):
def stringify(self, value):
return str(value)
def get_schema(self):
raise NotImplementedError()
class UnspecifiedDefault(object):
pass
@ -237,6 +240,16 @@ class Field(object):
else:
return self._type.stringify(value)
def get_schema(self):
schema = self._type.get_schema()
schema.update({'readonly': self.read_only})
if self.nullable:
schema['type'].append('null')
default = self.default
if not isinstance(default, UnspecifiedDefault):
schema.update({'default': default})
return schema
class String(FieldType):
@staticmethod

View File

@ -37,6 +37,9 @@ class FakeFieldType(fields.FieldType):
def from_primitive(self, obj, attr, value):
return value[1:-1]
def get_schema(self):
return {'type': ['foo']}
class FakeEnum(fields.Enum):
FROG = "frog"
@ -96,6 +99,11 @@ class FakeEnumAltField(fields.BaseEnumField):
AUTO_TYPE = FakeEnumAlt()
class TestFieldType(test.TestCase):
def test_get_schema(self):
self.assertRaises(NotImplementedError, fields.FieldType().get_schema)
class TestField(test.TestCase):
def setUp(self):
super(TestField, self).setUp()
@ -131,6 +139,18 @@ class TestField(test.TestCase):
self.assertEqual('123', self.field.stringify(123))
class TestSchema(test.TestCase):
def setUp(self):
super(TestSchema, self).setUp()
self.field = fields.Field(FakeFieldType(), nullable=True,
default='', read_only=False)
def test_get_schema(self):
self.assertEqual({'type': ['foo', 'null'], 'default': '',
'readonly': False},
self.field.get_schema())
class TestString(TestField):
def setUp(self):
super(TestString, self).setUp()

View File

@ -2041,6 +2041,67 @@ class TestObjectSerializer(_BaseTestCase):
mock.sentinel.context, prim, '1.0')
class TestSchemaGeneration(test.TestCase):
class FakeFieldType(fields.FieldType):
pass
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')
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()
self.assertEqual({
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'TestObject',
'type': 'object',
'properties': {
'versioned_object.namespace': {
'type': 'string'
},
'versioned_object.name': {
'type': 'string'
},
'versioned_object.version': {
'type': 'string'
},
'versioned_object.changes': {
'type': 'array',
'items': {
'type': 'string'
}
},
'versioned_object.data': {
'type': 'object',
'description': 'fields of TestObject',
'properties': {
'foo': {'type': ['fake']},
'bar': {'type': ['fake', 'null']}
},
},
'required': ['bar', 'foo']
},
'required': ['versioned_object.namespace', 'versioned_object.name',
'versioned_object.version', 'versioned_object.data']
}, schema)
class TestNamespaceCompatibility(test.TestCase):
def setUp(self):
super(TestNamespaceCompatibility, self).setUp()