Beispiel #1
0
    def get_response_schema(self,
                            path: str,
                            method: str,
                            api_format: str = None) -> OpenAPISchema:
        response_schema: OpenAPISchema = {'type': 'object', 'properties': {}}

        item_schema = self.get_item_schema(path, method, api_format)

        if is_list_view(path, method, self.view):
            response_schema['properties']['data'] = {
                'type': 'array',
                'items': item_schema
            }
            paginator = self.get_paginator()
            if paginator:
                response_schema = paginator.get_paginated_response_schema(
                    response_schema)
            meta_schema = self.get_meta_schema(path, method, api_format)
            if response_schema['properties'].get('meta'):
                response_schema = self.update_meta_schema(
                    response_schema, meta_schema)
            else:
                response_schema['properties']['meta'] = meta_schema
        else:
            response_schema['properties']['data'] = item_schema

        return response_schema
Beispiel #2
0
    def _get_operation_id(self, path, method):
        # rest framework implementation will try to deduce the  name
        # from the model, serializer before using the view name. That
        # leads to duplicate `operationId`.
        method_name = getattr(self.view, 'action', method.lower())
        if is_list_view(path, method, self.view):
            action = 'List'
        elif method_name not in self.method_mapping:
            action = method_name
        else:
            action = self.method_mapping[method.lower()]

        name = self.view.__class__.__name__
        if name.endswith('APIView'):
            name = name[:-7]
        elif name.endswith('View'):
            name = name[:-4]
        if name.endswith(action):  # ListView, UpdateAPIView, ThingDelete ...
            name = name[:-len(action)]

        if action == 'List' and not name.endswith(
                's'):  # ListThings instead of ListThing
            name += 's'

        return action + name
Beispiel #3
0
    def _get_operation_id(self, path, method):
        """
        Compute an operation ID from the model, serializer or view name.
        This subclass removes the check on serializers.
        """
        method_name = getattr(self.view, 'action', method.lower())
        if is_list_view(path, method, self.view):
            action = 'List'
        elif method_name not in self.method_mapping:
            action = method_name
        else:
            action = self.method_mapping[method.lower()]

        # Try to deduce the ID from the view's model
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)
        if model is not None:
            name = model.__name__

        # Fallback to the view name
        else:
            name = self.view.__class__.__name__
            if name.endswith('APIView'):
                name = name[:-7]
            elif name.endswith('View'):
                name = name[:-4]
            if name.endswith(
                    action):  # ListView, UpdateAPIView, ThingDelete ...
                name = name[:-len(action)]

        if action == 'List' and not name.endswith(
                's'):  # ListThings instead of ListThing
            name += 's'

        return action + name
Beispiel #4
0
 def get_action_name(self, path: str, method: str) -> str:
     action = get_action(path, method, self.view)
     if is_list_view(path, method, self.view):
         return 'list'
     elif action not in self.method_mapping:
         return action.lower()
     else:
         return self.method_mapping[method.lower()].lower()
def test_is_list_view_recognises_retrieve_view_subclasses():
    class TestView(generics.RetrieveAPIView):
        pass

    path = '/looks/like/a/list/view/'
    method = 'get'
    view = TestView()

    is_list = is_list_view(path, method, view)
    assert not is_list, "RetrieveAPIView subclasses should not be classified as list views."
Beispiel #6
0
 def get_operation_id(self, path, method):
     method_name = getattr(self.view, 'action', method.lower())
     if is_list_view(path, method, self.view):
         action = 'list'
     elif method_name not in self.method_mapping:
         action = self._to_camel_case(method_name)
     else:
         action = self.method_mapping[method.lower()]
     name = self.get_operation_id_base(path, method, action)
     return name + action.capitalize()
def test_is_list_view_recognises_retrieve_view_subclasses():
    class TestView(generics.RetrieveAPIView):
        pass

    path = '/looks/like/a/list/view/'
    method = 'get'
    view = TestView()

    is_list = is_list_view(path, method, view)
    assert not is_list, "RetrieveAPIView subclasses should not be classified as list views."
Beispiel #8
0
    def get_es_pagination_fields(self, path, method):
        view = self.view
        if not is_list_view(path, method, view):
            return []

        pagination = getattr(view, 'es_pagination_class', None)
        if not pagination:
            return []

        return pagination().get_schema_fields(view)
Beispiel #9
0
 def get_manual_fields(self, path, method):
     if is_list_view(path, method, self.view) and method.lower() == "get":
         return [
             coreapi.Field(name="q",
                           required=False,
                           location="query",
                           schema=coreschema.String(
                               title='Query',
                               description='A city name or zip code.'))
         ]
     return []
Beispiel #10
0
    def _get_pagination_parameters(self, path, method):
        view = self.view

        if not is_list_view(path, method, view):
            return []

        paginator = self._get_paginator()
        if not paginator:
            return []

        return paginator.get_schema_operation_parameters(view)
Beispiel #11
0
    def get_operation_id(self, path, method):
        """ override this for custom behaviour """
        tokenized_path = self._tokenize_path(path)
        # replace dashes as they can be problematic later in code generation
        tokenized_path = [t.replace('-', '_') for t in tokenized_path]

        if is_list_view(path, method, self.view):
            action = 'list'
        else:
            action = self.method_mapping[method.lower()]

        return '_'.join(tokenized_path + [action])
Beispiel #12
0
    def get_pagination_parameters(self, path: str, method: str) -> typing.List[OpenAPISchema]:
        if not is_list_view(path, method, self.view):
            return []

        paginator = self.get_paginator()
        if not paginator:
            return []

        serializer_class = getattr(paginator, 'serializer_class', None)
        if serializer_class:
            return self.map_query_serializer(serializer_class())
        return paginator.get_schema_operation_parameters(self.view)
Beispiel #13
0
    def get_operation_id(self, path, method):
        method_name = getattr(self.view, "action", method.lower())
        if is_list_view(path, method, self.view):
            action = "list"
        elif method_name not in self.method_mapping:
            action = self._to_camel_case(
                f"{self.method_mapping[method.lower()]}_{method_name}")
        else:
            action = self.method_mapping[method.lower()]

        name = self.get_operation_id_base(path, method, action)

        return action + name
Beispiel #14
0
    def get_path_parameters(self, path, method):
        if not is_list_view(path, method, self.view):
            return super(RecipeSchema, self).get_path_parameters(path, method)

        parameters = super().get_path_parameters(path, method)
        parameters.append({
            "name": 'query', "in": "query", "required": False,
            "description": 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'keywords', "in": "query", "required": False,
            "description": 'Id of keyword a recipe should have. For multiple repeat parameter.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'foods', "in": "query", "required": False,
            "description": 'Id of food a recipe should have. For multiple repeat parameter.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'books', "in": "query", "required": False,
            "description": 'Id of book a recipe should have. For multiple repeat parameter.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'keywords_or', "in": "query", "required": False,
            "description": 'If recipe should have all (AND) or any (OR) of the provided keywords.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'foods_or', "in": "query", "required": False,
            "description": 'If recipe should have all (AND) or any (OR) any of the provided foods.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'books_or', "in": "query", "required": False,
            "description": 'If recipe should be in all (AND) or any (OR) any of the provided books.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'internal', "in": "query", "required": False,
            "description": 'true or false. If only internal recipes should be returned or not.',
            'schema': {'type': 'string', },
        })
        parameters.append({
            "name": 'random', "in": "query", "required": False,
            "description": 'true or false. returns the results in randomized order.',
            'schema': {'type': 'string', },
        })
        return parameters
Beispiel #15
0
 def get_operation_id(self, path, method):
     """
     The upstream DRF version creates non-unique operationIDs, because the same view is
     used for the main path as well as such as related and relationships.
     This concatenates the (mapped) method name and path as the spec allows most any
     """
     method_name = getattr(self.view, "action", method.lower())
     if is_list_view(path, method, self.view):
         action = "List"
     elif method_name not in self.method_mapping:
         action = method_name
     else:
         action = self.method_mapping[method.lower()]
     return action + path
    def get_operation_keys(self, subpath, method, view) -> List[str]:
        if method != "HEAD":
            return super().get_operation_keys(subpath, method, view)

        assert not is_list_view(
            subpath, method,
            view), "HEAD requests are only supported on detail endpoints"

        # taken from DRF schema generation
        named_path_components = [
            component for component in subpath.strip("/").split("/")
            if "{" not in component
        ]

        return named_path_components + ["headers"]
Beispiel #17
0
    def get_path_parameters(self, path, method):
        if not is_list_view(path, method, self.view):
            return super(TreeSchema, self).get_path_parameters(path, method)

        api_name = path.split('/')[2]
        parameters = super().get_path_parameters(path, method)
        parameters.append({
            "name":
            'query',
            "in":
            "query",
            "required":
            False,
            "description":
            'Query string matched against {} name.'.format(api_name),
            'schema': {
                'type': 'string',
            },
        })
        parameters.append({
            "name":
            'root',
            "in":
            "query",
            "required":
            False,
            "description":
            'Return first level children of {obj} with ID [int].  Integer 0 will return root {obj}s.'
            .format(obj=api_name),
            'schema': {
                'type': 'int',
            },
        })
        parameters.append({
            "name":
            'tree',
            "in":
            "query",
            "required":
            False,
            "description":
            'Return all self and children of {} with ID [int].'.format(
                api_name),
            'schema': {
                'type': 'int',
            },
        })
        return parameters
Beispiel #18
0
    def get_path_parameters(self, path, method):
        if not is_list_view(path, method, self.view):
            return super(QueryOnlySchema,
                         self).get_path_parameters(path, method)

        parameters = super().get_path_parameters(path, method)
        parameters.append({
            "name": 'query',
            "in": "query",
            "required": False,
            "description": 'Query string matched (fuzzy) against object name.',
            'schema': {
                'type': 'string',
            },
        })
        return parameters
Beispiel #19
0
    def get_pagination_parameters(self, path: str,
                                  method: str) -> typing.List[OpenAPISchema]:
        if not is_list_view(path, method, self.view):
            return []

        paginator: BasePagination = self.get_paginator()
        if not paginator:
            return []

        if isinstance(paginator, SerializerClassPaginationMixin):
            serializer_class = paginator.get_request_serializer_class()
        else:
            serializer_class = getattr(paginator, 'serializer_class', None)
        if serializer_class:
            return self.map_query_serializer(serializer_class())
        return paginator.get_schema_operation_parameters(self.view)
Beispiel #20
0
    def get_path_parameters(self, path, method):
        if not is_list_view(path, method, self.view):
            return super().get_path_parameters(path, method)
        parameters = super().get_path_parameters(path, method)
        for q in self.view.query_params:
            parameters.append({
                "name": q.name,
                "in": "query",
                "required": q.required,
                "description": q.description,
                'schema': {
                    'type': q.qtype,
                },
            })

        return parameters
    def _get_responses(self, path, method):
        # TODO: Handle multiple codes and pagination classes.
        if method == "DELETE":
            return {"204": {"description": ""}}

        self.response_media_types = self.map_renderers(path, method)

        item_schema = {}
        serializer = self._get_serializer(path, method)

        if isinstance(serializer, serializers.Serializer):
            item_schema = self._map_serializer(serializer)
            # No write_only fields for response.
            for name, schema in item_schema["properties"].copy().items():
                if "writeOnly" in schema:
                    del item_schema["properties"][name]
                    if "required" in item_schema:
                        item_schema["required"] = [
                            f for f in item_schema["required"] if f != name
                        ]

        if is_list_view(path, method, self.view):
            response_schema = {
                "type": "array",
                "items": item_schema,
            }
            paginator = self._get_paginator()
            if paginator:
                response_schema = paginator.get_paginated_response_schema(
                    response_schema)
        else:
            response_schema = item_schema

        return {
            "200": {
                "content": {
                    ct: {
                        "schema": response_schema
                    }
                    for ct in self.response_media_types
                },
                # description is a mandatory property,
                # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
                # TODO: put something meaningful into it
                "description": "",
            }
        }
Beispiel #22
0
    def get_request_serializer_filter_parameters(self, path: str, method: str) -> typing.List[OpenAPISchema]:
        if not is_list_view(path, method, self.view):
            return []

        parameters = []
        request_serializer_class = self.view.get_request_serializer_class(use_default=False)
        request_serializer = request_serializer_class()
        if not isinstance(request_serializer, EmptySerializer):
            for field in request_serializer.fields.values():
                field_schema = self.get_field_schema(field)
                parameters.append({
                    'name': field.field_name,
                    'required': field.required,
                    'in': 'query',
                    'schema': field_schema,
                })
        return parameters
Beispiel #23
0
    def _get_response_for_code(self, path, method, serializer):
        serializer = force_instance(serializer)

        if not serializer:
            return {'description': 'No response body'}
        elif isinstance(serializer, serializers.ListSerializer):
            schema = self.resolve_serializer(method, serializer.child).ref
        elif is_serializer(serializer):
            component = self.resolve_serializer(method, serializer)
            if not component:
                return {'description': 'No response body'}
            schema = component.ref
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
        elif isinstance(serializer, dict):
            # bypass processing and use given schema directly
            schema = serializer
        else:
            warn(
                f'could not resolve "{serializer}" for {method} {path}. Expected either '
                f'a serializer or some supported override mechanism. defaulting to '
                f'generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = 'Unspecified response body'

        if isinstance(serializer, serializers.ListSerializer) or is_list_view(
                path, method, self.view):
            # TODO i fear is_list_view is not covering all the cases
            schema = build_array_type(schema)
            paginator = self._get_paginator()
            if paginator:
                schema = paginator.get_paginated_response_schema(schema)

        return {
            'content': {
                mt: {
                    'schema': schema
                }
                for mt in self.map_renderers(path, method)
            },
            # Description is required by spec, but descriptions for each response code don't really
            # fit into our model. Description is therefore put into the higher level slots.
            # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject
            'description': ''
        }
    def _get_operation_id(self, path, method):
        """
        Compute an operation ID from the model, serializer or view name.
        """
        method_name = getattr(self.view, "action", method.lower())
        if is_list_view(path, method, self.view):
            action = "list"
        elif method_name not in self.method_mapping:
            action = method_name
        else:
            action = self.method_mapping[method.lower()]

        # Try to deduce the ID from the view's model
        model = getattr(getattr(self.view, "queryset", None), "model", None)
        if model is not None:
            name = model.__name__

        # Try with the serializer class name
        elif hasattr(self.view, "get_serializer_class"):
            name = self.view.get_serializer_class().__name__
            if name.endswith("Serializer"):
                name = name[:-10]

        # Fallback to the view name
        else:
            name = self.view.__class__.__name__
            if name.endswith("APIView"):
                name = name[:-7]
            elif name.endswith("View"):
                name = name[:-4]

            # Due to camel-casing of classes and `action` being lowercase, apply title in order to find if action truly
            # comes at the end of the name
            if name.endswith(action.title()
                             ):  # ListView, UpdateAPIView, ThingDelete ...
                name = name[:-len(action)]

        if action == "list" and not name.endswith(
                "s"):  # listThings instead of listThing
            name += "s"

        return action + name
Beispiel #25
0
    def get_path_parameters(self, path, method):
        if not is_list_view(path, method, self.view):
            return super(FilterSchema, self).get_path_parameters(path, method)

        api_name = path.split('/')[2]
        parameters = super().get_path_parameters(path, method)
        parameters.append({
            "name":
            'query',
            "in":
            "query",
            "required":
            False,
            "description":
            'Query string matched against {} name.'.format(api_name),
            'schema': {
                'type': 'string',
            },
        })
        return parameters
Beispiel #26
0
    def get_responses(self, path, method):
        if method == 'DELETE':
            return {
                '204': {
                    'description': ''
                }
            }

        self.response_media_types = self.map_renderers(path, method)

        serializer = self.get_response_serializer(path, method)

        if not isinstance(serializer, serializers.Serializer):
            item_schema = {}
        else:
            item_schema = self._get_reference(serializer)

        if is_list_view(path, method, self.view):
            response_schema = {
                'type': 'array',
                'items': item_schema,
            }
            paginator = self.get_paginator()
            if paginator:
                response_schema = paginator.get_paginated_response_schema(response_schema)
        else:
            response_schema = item_schema
        status_code = '201' if method == 'POST' else '200'
        return {
            status_code: {
                'content': {
                    ct: {'schema': response_schema}
                    for ct in self.response_media_types
                },
                # description is a mandatory property,
                # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
                # TODO: put something meaningful into it
                'description': ""
            }
        }
Beispiel #27
0
    def get_operation(self, path, method):
        """
        JSON:API adds some standard fields to the API response that are not in upstream DRF:
        - some that only apply to GET/HEAD methods.
        - collections
        - special handling for POST, PATCH, DELETE
        """
        operation = {}
        operation["operationId"] = self.get_operation_id(path, method)
        operation["description"] = self.get_description(path, method)

        parameters = []
        parameters += self.get_path_parameters(path, method)
        # pagination, filters only apply to GET/HEAD of collections and items
        if method in ["GET", "HEAD"]:
            parameters += self._get_include_parameters(path, method)
            parameters += self._get_fields_parameters(path, method)
            parameters += self._get_sort_parameters(path, method)
            parameters += self.get_pagination_parameters(path, method)
            parameters += self.get_filter_parameters(path, method)
        operation["parameters"] = parameters
        operation["tags"] = self.get_tags(path, method)

        # get request and response code schemas
        if method == "GET":
            if is_list_view(path, method, self.view):
                self._add_get_collection_response(operation)
            else:
                self._add_get_item_response(operation)
        elif method == "POST":
            self._add_post_item_response(operation, path)
        elif method == "PATCH":
            self._add_patch_item_response(operation, path)
        elif method == "DELETE":
            # should only allow deleting a resource, not a collection
            # TODO: implement delete of a relationship in future release.
            self._add_delete_item_response(operation, path)
        return operation
    def get_operationId(self, subpath, method, view):
        """
        Return a list of keys that should be used to layout a link within
        the schema document.

        /users/                   ("users", "list"), ("users", "create")
        /users/{pk}/              ("users", "read"), ("users", "update"), ("users", "delete")
        /users/enabled/           ("users", "enabled")  # custom viewset list action
        /users/{pk}/star/         ("users", "star")     # custom viewset detail action
        /users/{pk}/groups/       ("users", "groups", "list"), ("users", "groups", "create")
        /users/{pk}/groups/{pk}/  ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete")
        """
        if hasattr(view, 'action'):
            # Viewsets have explicitly named actions.
            action = view.action
        else:
            # Views have no associated action, so we determine one from the method.
            if is_list_view(subpath, method, view):
                action = 'list'
            else:
                action = method_mapping[method.lower()]
        subprefix_path = subpath.replace(settings.REST_API_PREFIX, '', 1)
        named_path_components = [
            component for component
            in subprefix_path.strip('/').split('/')
            if '{' not in component
        ]

        if is_custom_action(action):
            # Custom action, eg "/users/{pk}/activate/", "/users/active/"
            if view.action_map and len(view.action_map) > 1:
                action = view.action_map[method.lower()]
                return '_'.join(named_path_components + [action]), action
            else:
                return '_'.join(named_path_components[:-1] + [action]), action

        return '_'.join(named_path_components + [action]), action
Beispiel #29
0
 def allows_filters(self, path: str, method: str) -> bool:
     if settings.API_IGNORE_FILTER_PARAMS_FOR_DETAIL and not is_list_view(
             path, method, self.view):
         return False
     return super().allows_filters(path, method)