Repository: Kinto/kinto

Issue:
Expose a parse_resource utility function for plugins.
In [kinto-signer](https://github.com/Kinto/kinto-signer/blob/25a3979/kinto_signer/utils.py#L14-L24) and [kinto-amo](https://github.com/mozilla-services/kinto-amo/blob/a1801b2efee0d95b41f9b8ffe285148077fc3864/kinto_amo/utils.py#L5-L14) we are using `parse_resource` to extract from a configuration the bucket and collection of a given resource.

Since two plugins start to using the same thing, it is probably time to make it available as a plugin utility in Kinto itself.



Discussion:

Resolving patch:
diff --git a/kinto/core/utils.py b/kinto/core/utils.py
--- a/kinto/core/utils.py
+++ b/kinto/core/utils.py
@@ -423,3 +423,33 @@ def instance_uri(request, resource_name, **params):
     """Return the URI for the given resource."""
     return strip_uri_prefix(request.route_path('%s-record' % resource_name,
                                                **params))
+
+
+def parse_resource(resource):
+    """Extract the bucket_id and collection_id of the given resource (URI)
+
+    :param str resource: a uri formatted /buckets/<bid>/collections/<cid> or <bid>/<cid>.
+    :returns: a dictionary with the bucket_id and collection_id of the resource
+    """
+
+    error_msg = "Resources should be defined as "
+    "'/buckets/<bid>/collections/<cid>' or '<bid>/<cid>'. "
+    "with valid collection and bucket ids."
+
+    from kinto.views import NameGenerator
+    id_generator = NameGenerator()
+    parts = resource.split('/')
+    if len(parts) == 2:
+        bucket, collection = parts
+    elif len(parts) == 5:
+        _, _, bucket, _, collection = parts
+    else:
+        raise ValueError(error_msg)
+    if bucket == '' or collection == '':
+        raise ValueError(error_msg)
+    if not id_generator.match(bucket) or not id_generator.match(collection):
+        raise ValueError(error_msg)
+    return {
+        'bucket': bucket,
+        'collection': collection
+    }


Test patch:
diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py
--- a/tests/core/test_utils.py
+++ b/tests/core/test_utils.py
@@ -2,6 +2,7 @@
 from __future__ import unicode_literals
 import unittest
 import os
+import pytest
 
 import colander
 import mock
@@ -15,7 +16,7 @@
 from kinto.core.utils import (
     native_value, strip_whitespace, random_bytes_hex, read_env, hmac_digest,
     current_service, encode_header, decode_header, follow_subrequest,
-    build_request, dict_subset
+    build_request, dict_subset, parse_resource
 )
 from kinto.core.testing import DummyRequest
 
@@ -246,3 +247,40 @@ def test_ignores_if_subobject_is_not_dict(self):
         obtained = dict_subset(input, ["a", "b.c.d", "b.d"])
         expected = dict(a=1, b=dict(c=2, d=3))
         self.assertEqual(obtained, expected)
+
+
+class ParseResourceTest(unittest.TestCase):
+
+    expected = {
+        'bucket': 'bid',
+        'collection': 'cid'
+    }
+    error_msg = "Resources should be defined as "
+    "'/buckets/<bid>/collections/<cid>' or '<bid>/<cid>'. "
+    "with valid collection and bucket ids."
+
+    def _assert_success(self, input):
+        parts = parse_resource(input)
+        self.assertEqual(self.expected, parts)
+
+    def _assert_error(self, input_arr):
+        for input in input_arr:
+            with pytest.raises(ValueError) as excinfo:
+                parse_resource(input)
+            self.assertEquals(str(excinfo.value), self.error_msg)
+
+    def test_malformed_url_raises_an_exception(self):
+        input_arr = ['foo', '/', '/bar', 'baz/', '/fo/o', '/buckets/sbid/scid']
+        self._assert_error(input_arr)
+
+    def test_returned_resources_match_the_expected_format(self):
+        input = "/buckets/bid/collections/cid"
+        self._assert_success(input)
+
+    def test_returned_resources_match_the_legacy_format(self):
+        input = "bid/cid"
+        self._assert_success(input)
+
+    def test_resources_must_be_valid_names(self):
+        input_arr = ['/buckets/bi+d1/collections/cid', '/buckets/bid1/collections/dci,d']
+        self._assert_error(input_arr)