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)
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 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)