# Copyright (c) 2014 Spotify AB
#
# Licensed under the Apache License, Version 2.0 (the 'License'); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
'''
Extension for generating JSON schema schemas from PySchema classes
JSON schema: http://json-schema.org/
When dumping to JSON schema, all fields in a record are mandatory, although
a list or map can be empty (but must be present). These records are still
dump-able, but they will not validate against the schema.
Usage:
>>> class MyRecord(pyschema.Record):
>>> foo = Text()
>>> bar = Integer()
>>>
>>> [pyschema_extensions.jsonschema.]get_root_schema_string(MyRecord)
'{"additionalProperties": false, "required": ["bar", "foo"], "type": "object", "id": "MyRecord", "properties": {"foo": {"t
ype": "string"}, "bar": {"type": "integer"}}} '
'''
from pyschema import core
from pyschema.types import Field, Boolean, Integer, Float
from pyschema.types import Text, Enum, List, Map, SubRecord
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
try:
import simplejson as json
except ImportError:
import json
# Bytes are not supported
Boolean.jsonschema_type_name = 'boolean'
Integer.jsonschema_type_name = 'integer'
Float.jsonschema_type_name = 'number'
Text.jsonschema_type_name = 'string'
Enum.jsonschema_type_name = 'string'
List.jsonschema_type_name = 'array'
Map.avro_type_name = 'object'
@Field.mixin
[docs]class FieldMixin:
[docs] def jsonschema_type_schema(self, state):
return {
'type': self.jsonschema_type_name,
}
@Enum.mixin
[docs]class EnumMixin:
[docs] def jsonschema_type_schema(self, state):
return {
'type': self.jsonschema_type_name,
'enum': sorted(list(self.values)),
}
@List.mixin
[docs]class ListMixin:
[docs] def jsonschema_type_schema(self, state):
return {
'type': self.jsonschema_type_name,
'items': self.field_type.jsonschema_type_schema(state),
}
@Map.mixin
[docs]class MapMixin:
[docs] def jsonschema_type_schema(self, state):
return {
'type': 'object',
'additionalProperties': True,
'patternProperties': {
'^.*$': self.value_type.jsonschema_type_schema(state),
},
}
@SubRecord.mixin
[docs]class SubRecordMixin:
[docs] def jsonschema_type_schema(self, state):
cls = self._schema
state.record_schemas[cls._schema_name] = get_schema_dict(cls, state)
return {
'$ref': self.jsonschema_type_name,
}
@property
[docs] def jsonschema_type_name(self):
return '#/definitions/{0}'.format(self._schema._schema_name)
# Schema generation
[docs]class SchemaGeneratorState(object):
def __init__(self):
self.record_schemas = dict()
[docs]def get_schema_dict(record, state=None):
"""Return a python dict representing the jsonschema of a record
Any references to sub-schemas will be URI fragments that won't be
resolvable without a root schema, available from get_root_schema_dict.
"""
state = state or SchemaGeneratorState()
schema = OrderedDict([
('type', 'object'),
('id', record._schema_name),
])
fields = dict()
for field_name, field_type in record._fields.iteritems():
fields[field_name] = field_type.jsonschema_type_schema(state)
required = set(fields.keys())
schema['properties'] = fields
schema['required'] = sorted(list(required))
schema['additionalProperties'] = False
state.record_schemas[record._schema_name] = schema
return schema
[docs]def get_root_schema_dict(record):
"""Return a root jsonschema for a given record
A root schema includes the $schema attribute and all sub-record
schemas and definitions.
"""
state = SchemaGeneratorState()
schema = get_schema_dict(record, state)
del state.record_schemas[record._schema_name]
if state.record_schemas:
schema['definitions'] = dict()
for name, sub_schema in state.record_schemas.iteritems():
schema['definitions'][name] = sub_schema
return schema
[docs]def get_root_schema_string(record):
return json.dumps(get_root_schema_dict(record))
[docs]def dumps(record):
return json.dumps(core.to_json_compatible(record))
[docs]def loads(s, record_store=None, schema=None):
return core.loads(s, record_store, schema, core.from_json_compatible)