We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add custom (meta) data on buckets and collections 
For some use-cases, it might become useful to be able to store some custom attributes in buckets or collections (e.g. metadata like application version, contact email or whatever).

Currently both Collection and Bucket resources do not define extra fields in their schema, and Cliquet drops unknown fields if not explicitly allowed.

We can either:
- Allow unknown fields in collection and buckets schemas
- Add a specific root level field (along `data` and `permissions`)
- Add a specific field (called `meta` for example) in the schema that could receive anything.

The advantage of the latter is that custom fields do not interfere with anything in the protocol, and are trivial to implement. The inconvenient is having to put `{data: {metadata: {email: "toto@yo.com"}}` in the payload.

Thoughts ?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kinto/views/collections.py`
Content:
```
1 import colander
2 import jsonschema
3 from cliquet import resource
4 from jsonschema import exceptions as jsonschema_exceptions
5 
6 from kinto.views import NameGenerator, object_exists_or_404
7 
8 
9 class JSONSchemaMapping(colander.SchemaNode):
10     def schema_type(self, **kw):
11         return colander.Mapping(unknown='preserve')
12 
13     def deserialize(self, cstruct=colander.null):
14         # Start by deserializing a simple mapping.
15         validated = super(JSONSchemaMapping, self).deserialize(cstruct)
16 
17         # In case it is optional in parent schema.
18         if not validated or validated in (colander.null, colander.drop):
19             return validated
20 
21         try:
22             jsonschema.Draft4Validator.check_schema(validated)
23         except jsonschema_exceptions.SchemaError as e:
24             self.raise_invalid(e.path.pop() + e.message)
25         return validated
26 
27 
28 class CollectionSchema(resource.ResourceSchema):
29     schema = JSONSchemaMapping(missing=colander.drop)
30 
31 
32 @resource.register(name='collection',
33                    collection_methods=('GET',),
34                    collection_path='/buckets/{{bucket_id}}/collections',
35                    record_path='/buckets/{{bucket_id}}/collections/{{id}}')
36 class Collection(resource.ProtectedResource):
37     mapping = CollectionSchema()
38     permissions = ('read', 'write', 'record:create')
39 
40     def __init__(self, *args, **kwargs):
41         super(Collection, self).__init__(*args, **kwargs)
42 
43         bucket_id = self.request.matchdict['bucket_id']
44         object_exists_or_404(self.request,
45                              collection_id='bucket',
46                              object_id=bucket_id)
47 
48         self.collection.id_generator = NameGenerator()
49 
50     def get_parent_id(self, request):
51         bucket_id = request.matchdict['bucket_id']
52         parent_id = '/buckets/%s' % bucket_id
53         return parent_id
54 
55     def delete(self):
56         result = super(Collection, self).delete()
57 
58         # Delete records.
59         storage = self.collection.storage
60         parent_id = '%s/collections/%s' % (self.collection.parent_id,
61                                            self.record_id)
62         storage.delete_all(collection_id='record',
63                            parent_id=parent_id,
64                            with_deleted=False)
65         storage.purge_deleted(collection_id='record', parent_id=parent_id)
66 
67         return result
68
```

Path: `kinto/views/records.py`
Content:
```
1 import jsonschema
2 from cliquet import resource, schema
3 from cliquet.errors import raise_invalid
4 from jsonschema import exceptions as jsonschema_exceptions
5 
6 from kinto.views import object_exists_or_404
7 
8 
9 class RecordSchema(schema.ResourceSchema):
10     class Options():
11         preserve_unknown = True
12 
13 
14 _parent_path = '/buckets/{{bucket_id}}/collections/{{collection_id}}'
15 
16 
17 @resource.register(name='record',
18                    collection_path=_parent_path + '/records',
19                    record_path=_parent_path + '/records/{{id}}')
20 class Record(resource.ProtectedResource):
21 
22     mapping = RecordSchema()
23     schema_field = 'schema'
24 
25     def __init__(self, *args, **kwargs):
26         super(Record, self).__init__(*args, **kwargs)
27 
28         bucket_id = self.request.matchdict['bucket_id']
29         object_exists_or_404(self.request,
30                              collection_id='bucket',
31                              object_id=bucket_id)
32 
33         collection_id = self.request.matchdict['collection_id']
34         parent_id = '/buckets/%s' % bucket_id
35         self._collection = object_exists_or_404(self.request,
36                                                 collection_id='collection',
37                                                 parent_id=parent_id,
38                                                 object_id=collection_id)
39 
40     def get_parent_id(self, request):
41         bucket_id = request.matchdict['bucket_id']
42         collection_id = request.matchdict['collection_id']
43         return '/buckets/%s/collections/%s' % (bucket_id, collection_id)
44 
45     def is_known_field(self, field_name):
46         """Without schema, any field is considered as known."""
47         return True
48 
49     def process_record(self, new, old=None):
50         """Validate records against collection schema, if any."""
51         schema = self._collection.get('schema')
52         if not schema:
53             return new
54 
55         collection_timestamp = self._collection[self.collection.modified_field]
56 
57         try:
58             jsonschema.validate(new, schema)
59             new[self.schema_field] = collection_timestamp
60         except jsonschema_exceptions.ValidationError as e:
61             field = e.path.pop() if e.path else e.validator_value.pop()
62             raise_invalid(self.request, name=field, description=e.message)
63 
64         return new
65
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kinto/views/collections.py b/kinto/views/collections.py
--- a/kinto/views/collections.py
+++ b/kinto/views/collections.py
@@ -28,6 +28,9 @@
 class CollectionSchema(resource.ResourceSchema):
     schema = JSONSchemaMapping(missing=colander.drop)
 
+    class Options:
+        preserve_unknown = True
+
 
 @resource.register(name='collection',
                    collection_methods=('GET',),
diff --git a/kinto/views/records.py b/kinto/views/records.py
--- a/kinto/views/records.py
+++ b/kinto/views/records.py
@@ -7,7 +7,7 @@
 
 
 class RecordSchema(schema.ResourceSchema):
-    class Options():
+    class Options:
         preserve_unknown = True

{"golden_diff": "diff --git a/kinto/views/collections.py b/kinto/views/collections.py\n--- a/kinto/views/collections.py\n+++ b/kinto/views/collections.py\n@@ -28,6 +28,9 @@\n class CollectionSchema(resource.ResourceSchema):\n     schema = JSONSchemaMapping(missing=colander.drop)\n \n+    class Options:\n+        preserve_unknown = True\n+\n \n @resource.register(name='collection',\n                    collection_methods=('GET',),\ndiff --git a/kinto/views/records.py b/kinto/views/records.py\n--- a/kinto/views/records.py\n+++ b/kinto/views/records.py\n@@ -7,7 +7,7 @@\n \n \n class RecordSchema(schema.ResourceSchema):\n-    class Options():\n+    class Options:\n         preserve_unknown = True\n", "issue": "Add custom (meta) data on buckets and collections \nFor some use-cases, it might become useful to be able to store some custom attributes in buckets or collections (e.g. metadata like application version, contact email or whatever).\n\nCurrently both Collection and Bucket resources do not define extra fields in their schema, and Cliquet drops unknown fields if not explicitly allowed.\n\nWe can either:\n- Allow unknown fields in collection and buckets schemas\n- Add a specific root level field (along `data` and `permissions`)\n- Add a specific field (called `meta` for example) in the schema that could receive anything.\n\nThe advantage of the latter is that custom fields do not interfere with anything in the protocol, and are trivial to implement. The inconvenient is having to put `{data: {metadata: {email: \"toto@yo.com\"}}` in the payload.\n\nThoughts ?\n\n", "before_files": [{"content": "import colander\nimport jsonschema\nfrom cliquet import resource\nfrom jsonschema import exceptions as jsonschema_exceptions\n\nfrom kinto.views import NameGenerator, object_exists_or_404\n\n\nclass JSONSchemaMapping(colander.SchemaNode):\n    def schema_type(self, **kw):\n        return colander.Mapping(unknown='preserve')\n\n    def deserialize(self, cstruct=colander.null):\n        # Start by deserializing a simple mapping.\n        validated = super(JSONSchemaMapping, self).deserialize(cstruct)\n\n        # In case it is optional in parent schema.\n        if not validated or validated in (colander.null, colander.drop):\n            return validated\n\n        try:\n            jsonschema.Draft4Validator.check_schema(validated)\n        except jsonschema_exceptions.SchemaError as e:\n            self.raise_invalid(e.path.pop() + e.message)\n        return validated\n\n\nclass CollectionSchema(resource.ResourceSchema):\n    schema = JSONSchemaMapping(missing=colander.drop)\n\n\n@resource.register(name='collection',\n                   collection_methods=('GET',),\n                   collection_path='/buckets/{{bucket_id}}/collections',\n                   record_path='/buckets/{{bucket_id}}/collections/{{id}}')\nclass Collection(resource.ProtectedResource):\n    mapping = CollectionSchema()\n    permissions = ('read', 'write', 'record:create')\n\n    def __init__(self, *args, **kwargs):\n        super(Collection, self).__init__(*args, **kwargs)\n\n        bucket_id = self.request.matchdict['bucket_id']\n        object_exists_or_404(self.request,\n                             collection_id='bucket',\n                             object_id=bucket_id)\n\n        self.collection.id_generator = NameGenerator()\n\n    def get_parent_id(self, request):\n        bucket_id = request.matchdict['bucket_id']\n        parent_id = '/buckets/%s' % bucket_id\n        return parent_id\n\n    def delete(self):\n        result = super(Collection, self).delete()\n\n        # Delete records.\n        storage = self.collection.storage\n        parent_id = '%s/collections/%s' % (self.collection.parent_id,\n                                           self.record_id)\n        storage.delete_all(collection_id='record',\n                           parent_id=parent_id,\n                           with_deleted=False)\n        storage.purge_deleted(collection_id='record', parent_id=parent_id)\n\n        return result\n", "path": "kinto/views/collections.py"}, {"content": "import jsonschema\nfrom cliquet import resource, schema\nfrom cliquet.errors import raise_invalid\nfrom jsonschema import exceptions as jsonschema_exceptions\n\nfrom kinto.views import object_exists_or_404\n\n\nclass RecordSchema(schema.ResourceSchema):\n    class Options():\n        preserve_unknown = True\n\n\n_parent_path = '/buckets/{{bucket_id}}/collections/{{collection_id}}'\n\n\n@resource.register(name='record',\n                   collection_path=_parent_path + '/records',\n                   record_path=_parent_path + '/records/{{id}}')\nclass Record(resource.ProtectedResource):\n\n    mapping = RecordSchema()\n    schema_field = 'schema'\n\n    def __init__(self, *args, **kwargs):\n        super(Record, self).__init__(*args, **kwargs)\n\n        bucket_id = self.request.matchdict['bucket_id']\n        object_exists_or_404(self.request,\n                             collection_id='bucket',\n                             object_id=bucket_id)\n\n        collection_id = self.request.matchdict['collection_id']\n        parent_id = '/buckets/%s' % bucket_id\n        self._collection = object_exists_or_404(self.request,\n                                                collection_id='collection',\n                                                parent_id=parent_id,\n                                                object_id=collection_id)\n\n    def get_parent_id(self, request):\n        bucket_id = request.matchdict['bucket_id']\n        collection_id = request.matchdict['collection_id']\n        return '/buckets/%s/collections/%s' % (bucket_id, collection_id)\n\n    def is_known_field(self, field_name):\n        \"\"\"Without schema, any field is considered as known.\"\"\"\n        return True\n\n    def process_record(self, new, old=None):\n        \"\"\"Validate records against collection schema, if any.\"\"\"\n        schema = self._collection.get('schema')\n        if not schema:\n            return new\n\n        collection_timestamp = self._collection[self.collection.modified_field]\n\n        try:\n            jsonschema.validate(new, schema)\n            new[self.schema_field] = collection_timestamp\n        except jsonschema_exceptions.ValidationError as e:\n            field = e.path.pop() if e.path else e.validator_value.pop()\n            raise_invalid(self.request, name=field, description=e.message)\n\n        return new\n", "path": "kinto/views/records.py"}], "after_files": [{"content": "import colander\nimport jsonschema\nfrom cliquet import resource\nfrom jsonschema import exceptions as jsonschema_exceptions\n\nfrom kinto.views import NameGenerator, object_exists_or_404\n\n\nclass JSONSchemaMapping(colander.SchemaNode):\n    def schema_type(self, **kw):\n        return colander.Mapping(unknown='preserve')\n\n    def deserialize(self, cstruct=colander.null):\n        # Start by deserializing a simple mapping.\n        validated = super(JSONSchemaMapping, self).deserialize(cstruct)\n\n        # In case it is optional in parent schema.\n        if not validated or validated in (colander.null, colander.drop):\n            return validated\n\n        try:\n            jsonschema.Draft4Validator.check_schema(validated)\n        except jsonschema_exceptions.SchemaError as e:\n            self.raise_invalid(e.path.pop() + e.message)\n        return validated\n\n\nclass CollectionSchema(resource.ResourceSchema):\n    schema = JSONSchemaMapping(missing=colander.drop)\n\n    class Options:\n        preserve_unknown = True\n\n\n@resource.register(name='collection',\n                   collection_methods=('GET',),\n                   collection_path='/buckets/{{bucket_id}}/collections',\n                   record_path='/buckets/{{bucket_id}}/collections/{{id}}')\nclass Collection(resource.ProtectedResource):\n    mapping = CollectionSchema()\n    permissions = ('read', 'write', 'record:create')\n\n    def __init__(self, *args, **kwargs):\n        super(Collection, self).__init__(*args, **kwargs)\n\n        bucket_id = self.request.matchdict['bucket_id']\n        object_exists_or_404(self.request,\n                             collection_id='bucket',\n                             object_id=bucket_id)\n\n        self.collection.id_generator = NameGenerator()\n\n    def get_parent_id(self, request):\n        bucket_id = request.matchdict['bucket_id']\n        parent_id = '/buckets/%s' % bucket_id\n        return parent_id\n\n    def delete(self):\n        result = super(Collection, self).delete()\n\n        # Delete records.\n        storage = self.collection.storage\n        parent_id = '%s/collections/%s' % (self.collection.parent_id,\n                                           self.record_id)\n        storage.delete_all(collection_id='record',\n                           parent_id=parent_id,\n                           with_deleted=False)\n        storage.purge_deleted(collection_id='record', parent_id=parent_id)\n\n        return result\n", "path": "kinto/views/collections.py"}, {"content": "import jsonschema\nfrom cliquet import resource, schema\nfrom cliquet.errors import raise_invalid\nfrom jsonschema import exceptions as jsonschema_exceptions\n\nfrom kinto.views import object_exists_or_404\n\n\nclass RecordSchema(schema.ResourceSchema):\n    class Options:\n        preserve_unknown = True\n\n\n_parent_path = '/buckets/{{bucket_id}}/collections/{{collection_id}}'\n\n\n@resource.register(name='record',\n                   collection_path=_parent_path + '/records',\n                   record_path=_parent_path + '/records/{{id}}')\nclass Record(resource.ProtectedResource):\n\n    mapping = RecordSchema()\n    schema_field = 'schema'\n\n    def __init__(self, *args, **kwargs):\n        super(Record, self).__init__(*args, **kwargs)\n\n        bucket_id = self.request.matchdict['bucket_id']\n        object_exists_or_404(self.request,\n                             collection_id='bucket',\n                             object_id=bucket_id)\n\n        collection_id = self.request.matchdict['collection_id']\n        parent_id = '/buckets/%s' % bucket_id\n        self._collection = object_exists_or_404(self.request,\n                                                collection_id='collection',\n                                                parent_id=parent_id,\n                                                object_id=collection_id)\n\n    def get_parent_id(self, request):\n        bucket_id = request.matchdict['bucket_id']\n        collection_id = request.matchdict['collection_id']\n        return '/buckets/%s/collections/%s' % (bucket_id, collection_id)\n\n    def is_known_field(self, field_name):\n        \"\"\"Without schema, any field is considered as known.\"\"\"\n        return True\n\n    def process_record(self, new, old=None):\n        \"\"\"Validate records against collection schema, if any.\"\"\"\n        schema = self._collection.get('schema')\n        if not schema:\n            return new\n\n        collection_timestamp = self._collection[self.collection.modified_field]\n\n        try:\n            jsonschema.validate(new, schema)\n            new[self.schema_field] = collection_timestamp\n        except jsonschema_exceptions.ValidationError as e:\n            field = e.path.pop() if e.path else e.validator_value.pop()\n            raise_invalid(self.request, name=field, description=e.message)\n\n        return new\n", "path": "kinto/views/records.py"}]}