def build_schema(self): # The OGGBundle JSON schemas all are flat arrays of items, where the # actual items' schemas are (more or less) the GEVER content types' # schemas, stored in #/definitions/<short_name> self.schema = JSONSchema(type_='array') self.schema._schema['items'] = { "$ref": "#/definitions/%s" % self.short_name } # Build the standard content type schema self.ct_schema = JSONSchemaBuilder(self.portal_type).build_schema() # Tweak the content type schema for use in OGGBundles self._add_review_state_property() self._add_guid_properties() self._add_permissions_property() self._add_file_properties() self._add_sequence_number_property() self._add_mail_properties() self._filter_fields() self.ct_schema.make_optional_properties_nullable() # Finally add the CT schema under #/definitions/<short_name> self.schema.add_definition(self.short_name, self.ct_schema) return self.schema
def build_schema(self): type_dump = self.type_dumper.dump(self.portal_type) self.schema = JSONSchema( title=type_dump['title'], additional_properties=False, ) field_order = [] # Collect field info from all schemas (base schema + behaviors) for _schema in type_dump['schemas']: # Note: This is not the final / "correct" field order as displayed # in the user interface (which should eventually honor fieldsets # and plone.autoform directives). # The intent here is rather to keep a *consistent* order for now. field_order.extend([field['name'] for field in _schema['fields']]) translated_title_fields = [] for field in _schema['fields']: prop_def = self._property_definition_from_field(field) self.schema.add_property(field['name'], prop_def) if field['name'] in TRANSLATED_TITLE_NAMES: translated_title_fields.append(field['name']) if translated_title_fields: self.schema.require_any_of(translated_title_fields) self.schema.set_field_order(field_order) return self.schema
def test_constructs_minimal_schema(self): schema = JSONSchema() expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', } serialized = schema.serialize() self.assertEqual(expected, serialized) self.assertIsInstance(serialized, OrderedDict)
def test_constructor_accepts_type(self): schema = JSONSchema(type_='array') expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'array', } serialized = schema.serialize() self.assertEqual(expected, serialized) self.assertIsInstance(serialized, OrderedDict)
def test_drop_property_with_missing_property(self): schema = JSONSchema() schema.drop_property('doesnt-exist') expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', } serialized = schema.serialize() self.assertEqual(expected, serialized)
def test_constructor_accepts_title(self): schema = JSONSchema(title=u'Foobar') expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'title': 'Foobar', } serialized = schema.serialize() self.assertEqual(expected, serialized) self.assertIsInstance(serialized, OrderedDict)
def test_constructor_accepts_additional_properties_flag(self): schema = JSONSchema(additional_properties=False) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'additionalProperties': False, } serialized = schema.serialize() self.assertEqual(expected, serialized) self.assertIsInstance(serialized, OrderedDict)
def test_add_property_supports_required_kwarg(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}, required=True) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, }, 'required': ['foo'], } serialized = schema.serialize() self.assertEqual(expected, serialized)
def build_schema(self): type_dump = self.type_dumper.dump(self.portal_type) self.schema = JSONSchema( title=type_dump['title'], additional_properties=False, ) field_order = [] # Collect field info from all schemas (base schema + behaviors) for _schema in type_dump['schemas']: # Note: This is not the final / "correct" field order as displayed # in the user interface (which should eventually honor fieldsets # and plone.autoform directives). # The intent here is rather to keep a *consistent* order for now. field_order.extend([field['name'] for field in _schema['fields']]) translated_title_fields = [] for field in _schema['fields']: prop_def = self._property_definition_from_field(field) self.schema.add_property(field['name'], prop_def) if field['name'] in TRANSLATED_TITLE_NAMES: translated_title_fields.append(field['name']) if translated_title_fields: self.schema.require_any_of(translated_title_fields) self.schema.set_field_order(field_order) return self.schema
def test_add_property(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, }, } serialized = schema.serialize() self.assertEqual(expected, serialized) self.assertIsInstance(serialized, OrderedDict) self.assertIsInstance(serialized['properties'], OrderedDict) self.assertIsInstance(serialized['properties']['foo'], OrderedDict)
def build_schema(self): # The OGGBundle JSON schemas all are flat arrays of items, where the # actual items' schemas are (more or less) the GEVER content types' # schemas, stored in #/definitions/<short_name> self.schema = JSONSchema(type_='array') self.schema._schema['items'] = { "$ref": "#/definitions/%s" % self.short_name} # Build the standard content type schema self.ct_schema = JSONSchemaBuilder(self.portal_type).build_schema() # Tweak the content type schema for use in OGGBundles self._add_review_state_property() self._add_guid_properties() self._add_permissions_property() self._add_file_properties() self._add_sequence_number_property() self._add_mail_properties() self._filter_fields() self.ct_schema.make_optional_properties_nullable() # Finally add the CT schema under #/definitions/<short_name> self.schema.add_definition(self.short_name, self.ct_schema) return self.schema
def test_require_any_of(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}) schema.add_property('bar', {'type': 'string'}) schema.require_any_of(['foo', 'bar']) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, 'bar': {'type': 'string'}, }, 'allOf': [ { 'anyOf': [ {'required': ['foo']}, {'required': ['bar']} ], }, ], } serialized = schema.serialize() self.assertEqual(expected, serialized)
def test_dumping_schema(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}, required=True) expected = dedent(""" { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "foo": { "type": "string" } }, "required": [ "foo" ] } """).strip() dump_path = join(self.tempdir, 'test.schema.json') schema.dump(dump_path) with open(dump_path) as f: actual = f.read() self.assertEqual(expected, actual)
def test_make_optional_properties_nullable(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}, required=True) schema.add_property('bar', {'type': 'string'}) schema.make_optional_properties_nullable() expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, 'bar': {'type': ['null', 'string']}, }, 'required': ['foo'], } serialized = schema.serialize() self.assertEqual(expected, serialized)
def test_dropping_last_property_removes_properties_key(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}) schema.drop_property('foo') expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', } serialized = schema.serialize() self.assertEqual(expected, serialized)
def test_add_definition(self): schema = JSONSchema() subschema = JSONSchema() subschema.add_property('subfoo', {'type': 'string'}) schema.add_definition('sub', subschema) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'definitions': { 'sub': { 'type': 'object', 'properties': { 'subfoo': {'type': 'string'}, }, } } } serialized = schema.serialize() self.assertDictEqual(expected, serialized)
def test_set_not_required_removes_empty_requires_list(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}, required=True) schema.set_not_required('foo') expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, }, } serialized = schema.serialize() self.assertEqual(expected, serialized)
def test_set_required_doesnt_add_property_twice_to_requireds(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}) schema.set_required('foo') schema.set_required('foo') expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, }, 'required': ['foo'], } serialized = schema.serialize() self.assertEqual(expected, serialized)
def test_set_field_order(self): schema = JSONSchema() schema.add_property('foo', {'type': 'string'}) schema.add_property('bar', {'type': 'string'}) schema.set_field_order(['foo', 'bar']) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { 'foo': {'type': 'string'}, 'bar': {'type': 'string'}, }, 'field_order': ['foo', 'bar'], } serialized = schema.serialize() self.assertEqual(expected, serialized)
class OGGBundleJSONSchemaBuilder(object): """Builds a JSON Schema representation of a single OGGBundle type. """ def __init__(self, portal_type): self.portal_type = portal_type self.short_name = GEVER_TYPES_TO_OGGBUNDLE_TYPES[portal_type] # Bundle schema (array of items) self.schema = None # Content type schema (item, i.e. GEVER type) self.ct_schema = None def build_schema(self): # The OGGBundle JSON schemas all are flat arrays of items, where the # actual items' schemas are (more or less) the GEVER content types' # schemas, stored in #/definitions/<short_name> self.schema = JSONSchema(type_='array') self.schema._schema['items'] = { "$ref": "#/definitions/%s" % self.short_name } # Build the standard content type schema self.ct_schema = JSONSchemaBuilder(self.portal_type).build_schema() # Tweak the content type schema for use in OGGBundles self._add_review_state_property() self._add_guid_properties() self._add_permissions_property() self._add_file_properties() self._add_sequence_number_property() self._add_mail_properties() self._filter_fields() self.ct_schema.make_optional_properties_nullable() # Finally add the CT schema under #/definitions/<short_name> self.schema.add_definition(self.short_name, self.ct_schema) return self.schema def _add_review_state_property(self): # XXX: Eventually, these states should be dynamically generated by # interrogating the FTI / workflow tool. Hardcoded for now. self.ct_schema.add_property( 'review_state', { 'type': 'string', 'enum': ALLOWED_REVIEW_STATES[self.portal_type] }, required=True, ) def _add_guid_properties(self): self.ct_schema.add_property('guid', {'type': 'string'}, required=True) if self.portal_type != 'opengever.repository.repositoryroot': # Everything except repository roots needs a parent GUID self.ct_schema.add_property('parent_guid', {'type': 'string'}) array_of_ints = { "type": "array", "items": { "type": "integer" }, } self.ct_schema.add_property('parent_reference', { 'type': 'array', 'items': array_of_ints }) self.ct_schema.require_any_of(['parent_guid', 'parent_reference']) def _add_permissions_property(self): if not self.portal_type == 'opengever.document.document': permissions_schema = self._build_permission_subschema() self.ct_schema._schema['properties']['_permissions'] = { "$ref": "#/definitions/permission" } # XXX: This is just to preserve order in definitions for now self.schema._schema['definitions'] = OrderedDict() self.schema._schema['definitions'][self.short_name] = None self.schema.add_definition('permission', permissions_schema) def _build_permission_subschema(self): subschema = JSONSchema(additional_properties=False) string_array = { "type": "array", "items": { "type": "string" }, } subschema.add_property('block_inheritance', {"type": "boolean"}) subschema.add_property('read', string_array) subschema.add_property('add', string_array) subschema.add_property('edit', string_array) subschema.add_property('close', string_array) subschema.add_property('reactivate', string_array) if self.portal_type in [ 'opengever.repository.repositoryroot', 'opengever.repository.repositoryfolder' ]: subschema.add_property('manage_dossiers', string_array) return subschema def _add_file_properties(self): if self.portal_type == 'opengever.document.document': self.ct_schema.add_property('filepath', {'type': 'string'}) # In OGGBundles we always require a title for documents self.ct_schema.set_required('title') # XXX: Documents without files? For now we always require filepath self.ct_schema.set_required('filepath') def _add_sequence_number_property(self): if self.portal_type not in SEQUENCE_NUMBER_LABELS: return desc = SEQUENCE_NUMBER_LABELS[self.portal_type] self.ct_schema.add_property('sequence_number', { 'type': 'integer', 'title': u'Laufnummer', 'description': desc, }) def _add_mail_properties(self): # Mails in OGGBundles are expecter in documents.json, and for large # parts treated like documents. Only later (bundle loader) will their # *actual* portal_type (ftw.mail.mail) be determined and set. if self.portal_type == 'opengever.document.document': self.ct_schema.add_property('original_message_path', {'type': 'string'}) def _filter_fields(self): filtered_fields = IGNORED_OGGBUNDLE_FIELDS.get(self.short_name, []) for field_name in filtered_fields: self.ct_schema.drop_property(field_name)
class JSONSchemaBuilder(object): """Builds a JSON Schema representation of a single GEVER type. """ def __init__(self, portal_type): self.portal_type = portal_type self.schema = None if portal_type in GEVER_TYPES: self.type_dumper = TypeDumper() elif portal_type in GEVER_SQL_TYPES: self.type_dumper = SQLTypeDumper() else: raise Exception("Unmapped type: %r" % portal_type) def build_schema(self): type_dump = self.type_dumper.dump(self.portal_type) self.schema = JSONSchema( title=type_dump['title'], additional_properties=False, ) field_order = [] # Collect field info from all schemas (base schema + behaviors) for _schema in type_dump['schemas']: # Note: This is not the final / "correct" field order as displayed # in the user interface (which should eventually honor fieldsets # and plone.autoform directives). # The intent here is rather to keep a *consistent* order for now. field_order.extend([field['name'] for field in _schema['fields']]) translated_title_fields = [] for field in _schema['fields']: prop_def = self._property_definition_from_field(field) self.schema.add_property(field['name'], prop_def) if field['name'] in TRANSLATED_TITLE_NAMES: translated_title_fields.append(field['name']) if translated_title_fields: self.schema.require_any_of(translated_title_fields) self.schema.set_field_order(field_order) return self.schema def _property_definition_from_field(self, field): """Create a JSON Schema property definition from a field info dump. """ prop_def = self._js_type_from_zope_type(field['type']) prop_def['title'] = field['title'] prop_def['description'] = field['description'] prop_def['_zope_schema_type'] = field['type'] self._process_choice(prop_def, field) self._process_max_length(prop_def, field) self._process_default(prop_def, field) self._process_vocabulary(prop_def, field) self._process_required(prop_def, field) return prop_def def _js_type_from_zope_type(self, zope_type): """Map a zope.schema type to a JSON schema (JavaScript) type. Returns a minimal property definition dict including at least a 'type', and possibly a 'format' as well. """ if zope_type not in JSON_SCHEMA_FIELD_TYPES: raise Exception( "Don't know what JS type to map %r to. Please map it in " "JSON_SCHEMA_FIELD_TYPES first." % zope_type) type_spec = JSON_SCHEMA_FIELD_TYPES[zope_type].copy() return type_spec def _process_choice(self, prop_def, field): if field['type'] == 'Choice': prop_def['type'] = field['value_type'] def _process_max_length(self, prop_def, field): if field.get('max_length') is not None: prop_def['maxLength'] = field['max_length'] def _process_default(self, prop_def, field): if 'default' in field: prop_def['default'] = field['default'] def _process_vocabulary(self, prop_def, field): if field.get('vocabulary'): vocab = field['vocabulary'] if isinstance(vocab, basestring) and vocab.startswith('<'): # Placeholder vocabulary (like <Any valid user> ) prop_def['_vocabulary'] = vocab else: prop_def['enum'] = vocab def _process_required(self, prop_def, field): if field.get('required', False): if field['name'] not in TRANSLATED_TITLE_NAMES: self.schema.set_required(field['name'])
class OGGBundleJSONSchemaBuilder(object): """Builds a JSON Schema representation of a single OGGBundle type. """ def __init__(self, portal_type): self.portal_type = portal_type self.short_name = GEVER_TYPES_TO_OGGBUNDLE_TYPES[portal_type] # Bundle schema (array of items) self.schema = None # Content type schema (item, i.e. GEVER type) self.ct_schema = None def build_schema(self): # The OGGBundle JSON schemas all are flat arrays of items, where the # actual items' schemas are (more or less) the GEVER content types' # schemas, stored in #/definitions/<short_name> self.schema = JSONSchema(type_='array') self.schema._schema['items'] = { "$ref": "#/definitions/%s" % self.short_name} # Build the standard content type schema self.ct_schema = JSONSchemaBuilder(self.portal_type).build_schema() # Tweak the content type schema for use in OGGBundles self._add_review_state_property() self._add_guid_properties() self._add_permissions_property() self._add_file_properties() self._add_sequence_number_property() self._add_mail_properties() self._filter_fields() self.ct_schema.make_optional_properties_nullable() # Finally add the CT schema under #/definitions/<short_name> self.schema.add_definition(self.short_name, self.ct_schema) return self.schema def _add_review_state_property(self): # XXX: Eventually, these states should be dynamically generated by # interrogating the FTI / workflow tool. Hardcoded for now. self.ct_schema.add_property( 'review_state', {'type': 'string', 'enum': ALLOWED_REVIEW_STATES[self.portal_type]}, required=True, ) def _add_guid_properties(self): self.ct_schema.add_property('guid', {'type': 'string'}, required=True) if self.portal_type != 'opengever.repository.repositoryroot': # Everything except repository roots needs a parent GUID self.ct_schema.add_property('parent_guid', {'type': 'string'}) array_of_ints = { "type": "array", "items": {"type": "integer"}, } self.ct_schema.add_property( 'parent_reference', {'type': 'array', 'items': array_of_ints}) self.ct_schema.require_any_of(['parent_guid', 'parent_reference']) def _add_permissions_property(self): if not self.portal_type == 'opengever.document.document': permissions_schema = self._build_permission_subschema() self.ct_schema._schema['properties']['_permissions'] = { "$ref": "#/definitions/permission"} # XXX: This is just to preserve order in definitions for now self.schema._schema['definitions'] = OrderedDict() self.schema._schema['definitions'][self.short_name] = None self.schema.add_definition('permission', permissions_schema) def _build_permission_subschema(self): subschema = JSONSchema(additional_properties=False) string_array = { "type": "array", "items": {"type": "string"}, } subschema.add_property('block_inheritance', {"type": "boolean"}) subschema.add_property('read', string_array) subschema.add_property('add', string_array) subschema.add_property('edit', string_array) subschema.add_property('close', string_array) subschema.add_property('reactivate', string_array) if self.portal_type in ['opengever.repository.repositoryroot', 'opengever.repository.repositoryfolder']: subschema.add_property('manage_dossiers', string_array) return subschema def _add_file_properties(self): if self.portal_type == 'opengever.document.document': self.ct_schema.add_property('filepath', {'type': 'string'}) # In OGGBundles we always require a title for documents self.ct_schema.set_required('title') # XXX: Documents without files? For now we always require filepath self.ct_schema.set_required('filepath') def _add_sequence_number_property(self): if self.portal_type not in SEQUENCE_NUMBER_LABELS: return desc = SEQUENCE_NUMBER_LABELS[self.portal_type] self.ct_schema.add_property('sequence_number', { 'type': 'integer', 'title': u'Laufnummer', 'description': desc, }) def _add_mail_properties(self): # Mails in OGGBundles are expecter in documents.json, and for large # parts treated like documents. Only later (bundle loader) will their # *actual* portal_type (ftw.mail.mail) be determined and set. if self.portal_type == 'opengever.document.document': self.ct_schema.add_property('original_message_path', {'type': 'string'}) def _filter_fields(self): filtered_fields = IGNORED_OGGBUNDLE_FIELDS.get(self.short_name, []) for field_name in filtered_fields: self.ct_schema.drop_property(field_name)
def _build_permission_subschema(self): subschema = JSONSchema(additional_properties=False) string_array = { "type": "array", "items": {"type": "string"}, } subschema.add_property('block_inheritance', {"type": "boolean"}) subschema.add_property('read', string_array) subschema.add_property('add', string_array) subschema.add_property('edit', string_array) subschema.add_property('close', string_array) subschema.add_property('reactivate', string_array) if self.portal_type in ['opengever.repository.repositoryroot', 'opengever.repository.repositoryfolder']: subschema.add_property('manage_dossiers', string_array) return subschema
def _build_permission_subschema(self): subschema = JSONSchema(additional_properties=False) string_array = { "type": "array", "items": { "type": "string" }, } subschema.add_property('block_inheritance', {"type": "boolean"}) subschema.add_property('read', string_array) subschema.add_property('add', string_array) subschema.add_property('edit', string_array) subschema.add_property('close', string_array) subschema.add_property('reactivate', string_array) if self.portal_type in [ 'opengever.repository.repositoryroot', 'opengever.repository.repositoryfolder' ]: subschema.add_property('manage_dossiers', string_array) return subschema
class JSONSchemaBuilder(object): """Builds a JSON Schema representation of a single GEVER type. """ def __init__(self, portal_type): self.portal_type = portal_type self.schema = None if portal_type in GEVER_TYPES: self.type_dumper = TypeDumper() elif portal_type in GEVER_SQL_TYPES: self.type_dumper = SQLTypeDumper() else: raise Exception("Unmapped type: %r" % portal_type) def build_schema(self): type_dump = self.type_dumper.dump(self.portal_type) self.schema = JSONSchema( title=type_dump['title'], additional_properties=False, ) field_order = [] # Collect field info from all schemas (base schema + behaviors) for _schema in type_dump['schemas']: # Note: This is not the final / "correct" field order as displayed # in the user interface (which should eventually honor fieldsets # and plone.autoform directives). # The intent here is rather to keep a *consistent* order for now. field_order.extend([field['name'] for field in _schema['fields']]) translated_title_fields = [] for field in _schema['fields']: prop_def = self._property_definition_from_field(field) self.schema.add_property(field['name'], prop_def) if field['name'] in TRANSLATED_TITLE_NAMES: translated_title_fields.append(field['name']) if translated_title_fields: self.schema.require_any_of(translated_title_fields) self.schema.set_field_order(field_order) return self.schema def _property_definition_from_field(self, field): """Create a JSON Schema property definition from a field info dump. """ prop_def = self._js_type_from_zope_type(field['type']) prop_def['title'] = field['title'] prop_def['description'] = field['description'] prop_def['_zope_schema_type'] = field['type'] self._process_choice(prop_def, field) self._process_max_length(prop_def, field) self._process_default(prop_def, field) self._process_vocabulary(prop_def, field) self._process_required(prop_def, field) return prop_def def _js_type_from_zope_type(self, zope_type): """Map a zope.schema type to a JSON schema (JavaScript) type. Returns a minimal property definition dict including at least a 'type', and possibly a 'format' as well. """ if zope_type not in JSON_SCHEMA_FIELD_TYPES: raise Exception( "Don't know what JS type to map %r to. Please map it in " "JSON_SCHEMA_FIELD_TYPES first." % zope_type) type_spec = JSON_SCHEMA_FIELD_TYPES[zope_type].copy() return type_spec def _process_choice(self, prop_def, field): if field['type'] == 'Choice': prop_def['type'] = field['value_type'] def _process_max_length(self, prop_def, field): if field.get('max_length') is not None: prop_def['maxLength'] = field['max_length'] def _process_default(self, prop_def, field): if 'default' in field: prop_def['default'] = field['default'] def _process_vocabulary(self, prop_def, field): if field.get('vocabulary'): vocab = field['vocabulary'] if isinstance(vocab, basestring) and vocab.startswith('<'): # Placeholder vocabulary (like <Any valid user> ) prop_def['_vocabulary'] = vocab else: prop_def['enum'] = vocab def _process_required(self, prop_def, field): if field.get('required', False): if field['name'] not in TRANSLATED_TITLE_NAMES: self.schema.set_required(field['name'])