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:
parent
e7b1be0e26
commit
92e7ac7108
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue