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:
parent
b9a38b4a9a
commit
5b44fafe6a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue