コード例 #1
0
    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
コード例 #2
0
    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
コード例 #3
0
    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)
コード例 #4
0
    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)
コード例 #5
0
    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)
コード例 #6
0
    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)
コード例 #7
0
    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)
コード例 #8
0
    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)
コード例 #9
0
ファイル: schema.py プロジェクト: 4teamwork/opengever.core
    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
コード例 #10
0
    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)
コード例 #11
0
ファイル: schema.py プロジェクト: 4teamwork/opengever.core
    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
コード例 #12
0
    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)
コード例 #13
0
    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)
コード例 #14
0
    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)
コード例 #15
0
    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)
コード例 #16
0
    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)
コード例 #17
0
    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)
コード例 #18
0
    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)
コード例 #19
0
    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)
コード例 #20
0
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)
コード例 #21
0
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'])
コード例 #22
0
ファイル: schema.py プロジェクト: 4teamwork/opengever.core
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)
コード例 #23
0
ファイル: schema.py プロジェクト: 4teamwork/opengever.core
    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
コード例 #24
0
    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
コード例 #25
0
ファイル: schema.py プロジェクト: 4teamwork/opengever.core
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'])