Repository: encode/django-rest-framework

Issue:
Schema autogeneration
# Schema Autogeneration

This pull request addresses Schema autogeneration from the API views.
1. it resolves a bug where the higher namespace urls (/account/{account_id}/books/{pk}/) are hidden by lower namespace (/account/{pk}/).
2. It uses the docstring of the method that will handle the request (either get, post, put, etc for the APIView or list, retrieve, update for the viewset) as the description of the coreapi.Link.
3. It uses the help_text of the serializers fields as the description of the coreapi.Field.
4. It capitalize the names of the Link.



Discussion:
Great stuff in here, thanks!
Will review shortly.

Okay, so there's a number of different aspects here, some we want, some we probably don't, some may need further thought.
1. Yes, we should resolve that, although not 100% clear to me exactly what we should use for the key generation. Needs further thought.
2. Useful, although note that this would _also_ pull in unhelpful docstrings from the default mixin implementations, so not sure how we should best handle that.
3. Yes.
4. Not totally convinced if we should be using `Humanized` or `programatic` formatting for the keys.

The `path_descriptions` API you've added is also perfectly reasonable, tho again, I'm not 100% sure if it's exactly what we want or not.

1. Sounds like an issue, but I can't think of a clean way to resolve them properly. Someone else who has more experience reverse-engineering url patterns can probably chip in here.
2. I'm a fan of this, because I personally have a tendency to write method-specific documentation when I'm overriding them. With that said, I do see the issue that Tom mentioned with the default mixins having developer-oriented documentation instead of user-oriented documentation. I'd personally prefer developer-oriented documentation there, so I don't have a solution.
3. Handled in https://github.com/tomchristie/django-rest-framework/pull/4387.
4. Imo, this is better handled client-side. The keys appear to match the url path for links right now, and this change can be done on the client if needed.

On second thoughts I noted that the `.get` etc... methods that we provide in REST framework _don't_ have default docstrings, so we _could_ use them. Although some users will be using base classes and/or mixins where the docstrings may not be correct/relevant, so point still applies, but less so than it would otherwise.

- Some remaining work on (1) prior to the 3.4.4 release - still not handling that quite right.
- For (2) I'm happy to reassess that as part of 3.5.0 - happy to see an individual issue opened for it. We're adding in the form field descriptions now. We could also look at addressing path field and query field descriptions. 
- 3. Now resolved.
- 4. Going to stay with lowercase for now. We'll firm up the generator API methods and document them as public API, so that folks can introduce third party packages with overrides if wanted.


Resolving patch:
diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py
--- a/rest_framework/schemas.py
+++ b/rest_framework/schemas.py
@@ -39,6 +39,10 @@ class SchemaGenerator(object):
         'patch': 'partial_update',
         'delete': 'destroy',
     }
+    known_actions = (
+        'create', 'read', 'retrieve', 'list',
+        'update', 'partial_update', 'destroy'
+    )
 
     def __init__(self, title=None, url=None, patterns=None, urlconf=None):
         assert coreapi, '`coreapi` must be installed for schema support.'
@@ -118,7 +122,8 @@ def get_api_endpoints(self, patterns, prefix=''):
                 if self.should_include_endpoint(path, callback):
                     for method in self.get_allowed_methods(callback):
                         action = self.get_action(path, method, callback)
-                        endpoint = (path, method, action, callback)
+                        category = self.get_category(path, method, callback, action)
+                        endpoint = (path, method, category, action, callback)
                         api_endpoints.append(endpoint)
 
             elif isinstance(pattern, RegexURLResolver):
@@ -128,21 +133,7 @@ def get_api_endpoints(self, patterns, prefix=''):
                 )
                 api_endpoints.extend(nested_endpoints)
 
-        return self.add_categories(api_endpoints)
-
-    def add_categories(self, api_endpoints):
-        """
-        (path, method, action, callback) -> (path, method, category, action, callback)
-        """
-        # Determine the top level categories for the schema content,
-        # based on the URLs of the endpoints. Eg `set(['users', 'organisations'])`
-        paths = [endpoint[0] for endpoint in api_endpoints]
-        categories = self.get_categories(paths)
-
-        return [
-            (path, method, self.get_category(categories, path), action, callback)
-            for (path, method, action, callback) in api_endpoints
-        ]
+        return api_endpoints
 
     def get_path(self, path_regex):
         """
@@ -181,36 +172,41 @@ def get_allowed_methods(self, callback):
 
     def get_action(self, path, method, callback):
         """
-        Return a description action string for the endpoint, eg. 'list'.
+        Return a descriptive action string for the endpoint, eg. 'list'.
         """
         actions = getattr(callback, 'actions', self.default_mapping)
         return actions[method.lower()]
 
-    def get_categories(self, paths):
-        categories = set()
-        split_paths = set([
-            tuple(path.split("{")[0].strip('/').split('/'))
-            for path in paths
-        ])
-
-        while split_paths:
-            for split_path in list(split_paths):
-                if len(split_path) == 0:
-                    split_paths.remove(split_path)
-                elif len(split_path) == 1:
-                    categories.add(split_path[0])
-                    split_paths.remove(split_path)
-                elif split_path[0] in categories:
-                    split_paths.remove(split_path)
-
-        return categories
-
-    def get_category(self, categories, path):
-        path_components = path.split("{")[0].strip('/').split('/')
-        for path_component in path_components:
-            if path_component in categories:
-                return path_component
-        return None
+    def get_category(self, path, method, callback, action):
+        """
+        Return a descriptive category string for the endpoint, eg. 'users'.
+
+        Examples of category/action pairs that should be generated for various
+        endpoints:
+
+        /users/                     [users][list], [users][create]
+        /users/{pk}/                [users][read], [users][update], [users][destroy]
+        /users/enabled/             [users][enabled]  (custom action)
+        /users/{pk}/star/           [users][star]     (custom action)
+        /users/{pk}/groups/         [groups][list], [groups][create]
+        /users/{pk}/groups/{pk}/    [groups][read], [groups][update], [groups][destroy]
+        """
+        path_components = path.strip('/').split('/')
+        path_components = [
+            component for component in path_components
+            if '{' not in component
+        ]
+        if action in self.known_actions:
+            # Default action, eg "/users/", "/users/{pk}/"
+            idx = -1
+        else:
+            # Custom action, eg "/users/{pk}/activate/", "/users/active/"
+            idx = -2
+
+        try:
+            return path_components[idx]
+        except IndexError:
+            return None
 
     # Methods for generating each individual `Link` instance...
 


Test patch: