Repository: Kinto/kinto

Issue:
Relax record id validation
Do we really need to ensure that posted record ids match a uuid regex?

We can generate a uuid when a record without id is posted, and leave the usage of uuid in our official clients.
But is there any reason to use a different regex that collection and bucket names?

edit: The usecase is the Web sync extension chrome.storage.sync: since any key is accepted, it takes the md5 of the key to "generate" UUIDs. Instead we could let the client push any key as record id.
- Related #140 



Discussion:
I agree with your analysis, the main reason why we did that is to let people create IDs on client side without having to worry about conflicts.

Also we have a strategy to generate a UUID from another string (using the md5 approach).

But I agree with you that we should let people use something else if need be.

Ultimately, you need the client to choose the ID, but I don't think it needs to always be a UUID. I would be :+1: on relaxing the requirement.


Resolving patch:
diff --git a/kinto/views/__init__.py b/kinto/views/__init__.py
--- a/kinto/views/__init__.py
+++ b/kinto/views/__init__.py
@@ -15,6 +15,12 @@ def __call__(self):
         return ''.join(letters)
 
 
+class RelaxedUUID(generators.UUID4):
+    """A generator that generates UUIDs but accepts any string.
+    """
+    regexp = generators.Generator.regexp
+
+
 def object_exists_or_404(request, collection_id, object_id, parent_id=''):
     storage = request.registry.storage
     try:
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 @@
 from pyramid.security import Authenticated
 from pyramid.settings import asbool
 
-from kinto.views import object_exists_or_404
+from kinto.views import RelaxedUUID, object_exists_or_404
 
 
 class RecordSchema(resource.ResourceSchema):
@@ -29,6 +29,8 @@ class Record(resource.ShareableResource):
     def __init__(self, *args, **kwargs):
         super(Record, self).__init__(*args, **kwargs)
 
+        self.model.id_generator = RelaxedUUID()
+
         # Check if already fetched before (in batch).
         collections = self.request.bound_data.setdefault('collections', {})
         collection_uri = self.get_parent_id(self.request)


Test patch:
diff --git a/kinto/tests/test_views_records.py b/kinto/tests/test_views_records.py
--- a/kinto/tests/test_views_records.py
+++ b/kinto/tests/test_views_records.py
@@ -1,5 +1,6 @@
 import json
 import mock
+import re
 
 from kinto.core.utils import decode_header
 from .support import (BaseWebTest, unittest, MINIMALIST_RECORD,
@@ -154,6 +155,48 @@ def test_create_a_record_update_collection_timestamp(self):
             decode_header(json.loads(collection_resp.headers['ETag'])))
         assert old_timestamp < new_timestamp
 
+    def test_create_a_record_without_id_generates_a_uuid(self):
+        resp = self.app.post_json(self.collection_url,
+                                  MINIMALIST_RECORD,
+                                  headers=self.headers,
+                                  status=201)
+        regexp = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-'
+                            r'[0-9a-f]{4}-[0-9a-f]{12}$')
+        self.assertTrue(regexp.match(resp.json['data']['id']))
+
+    def test_create_a_record_with_an_id_uses_it(self):
+        record = {'data': dict(id='a-simple-id', **MINIMALIST_RECORD['data'])}
+        resp = self.app.post_json(self.collection_url,
+                                  record,
+                                  headers=self.headers,
+                                  status=201)
+        self.assertEqual(resp.json['data']['id'], 'a-simple-id')
+
+    def test_create_a_record_with_an_existing_id_returns_existing(self):
+        resp = self.app.post_json(self.collection_url,
+                                  MINIMALIST_RECORD,
+                                  headers=self.headers,
+                                  status=201)
+        existing_id = resp.json['data']['id']
+        record = {'data': {'id': existing_id, 'stars': 8}}
+        resp = self.app.post_json(self.collection_url,
+                                  record,
+                                  headers=self.headers,
+                                  status=200)
+        self.assertNotIn('stars', resp.json['data'])
+
+    def test_create_a_record_with_existing_from_someone_else_gives_403(self):
+        resp = self.app.post_json(self.collection_url,
+                                  MINIMALIST_RECORD,
+                                  headers=self.headers,
+                                  status=201)
+        existing_id = resp.json['data']['id']
+        record = {'data': {'id': existing_id, 'stars': 8}}
+        resp = self.app.post_json(self.collection_url,
+                                  record,
+                                  headers=get_user_headers('tartanpion'),
+                                  status=403)
+
     def test_update_a_record_update_collection_timestamp(self):
         collection_resp = self.app.get(self.collection_url,
                                        headers=self.headers)