From 86a9176529ff9b4149b7441c48be6fd74bf76a17 Mon Sep 17 00:00:00 2001 From: Dima Knivets Date: Sun, 11 Aug 2019 15:39:35 +0300 Subject: [PATCH 1/5] openapi automatic mapping of renderers/parsers media types --- rest_framework/schemas/openapi.py | 33 +++++++++++++++++++++++++++---- tests/schemas/test_openapi.py | 24 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 0af7510cde..f10ea06af4 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -1,4 +1,5 @@ import warnings +from operator import attrgetter from urllib.parse import urljoin from django.core.validators import ( @@ -8,7 +9,7 @@ from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, serializers +from rest_framework import exceptions, serializers, renderers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty @@ -78,7 +79,9 @@ def get_schema(self, request=None, public=False): class AutoSchema(ViewInspector): - content_types = ['application/json'] + request_media_types = [] + response_media_types = [] + method_mapping = { 'get': 'Retrieve', 'post': 'Create', @@ -90,6 +93,7 @@ class AutoSchema(ViewInspector): def get_operation(self, path, method): operation = {} + operation['operationId'] = self._get_operation_id(path, method) parameters = [] @@ -337,6 +341,12 @@ def _map_field(self, field): self._map_min_max(field, content) return content + if isinstance(field, serializers.FileField): + return { + 'type': 'string', + 'format': 'binary' + } + # Simplest cases, default to 'string' type: FIELD_CLASS_SCHEMA_TYPE = { serializers.BooleanField: 'boolean', @@ -423,6 +433,17 @@ def _map_field_validators(self, validators, schema): schema['maximum'] = int(digits * '9') + 1 schema['minimum'] = -schema['maximum'] + def map_parsers(self, path, method): + return list(map(attrgetter('media_type'), self.view.parser_classes)) + + def map_renderers(self, path, method): + media_types = [] + for renderer in self.view.renderer_classes: + # I assume this is not relevant to OpenAPI spec + if renderer != renderers.BrowsableAPIRenderer: + media_types.append(renderer.media_type) + return media_types + def _get_serializer(self, method, path): view = self.view @@ -442,6 +463,8 @@ def _get_request_body(self, path, method): if method not in ('PUT', 'PATCH', 'POST'): return {} + self.request_media_types = self.map_parsers(path, method) + serializer = self._get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): @@ -459,7 +482,7 @@ def _get_request_body(self, path, method): return { 'content': { ct: {'schema': content} - for ct in self.content_types + for ct in self.request_media_types } } @@ -472,6 +495,8 @@ def _get_responses(self, path, method): } } + self.response_media_types = self.map_renderers(path, method) + item_schema = {} serializer = self._get_serializer(path, method) @@ -496,7 +521,7 @@ def _get_responses(self, path, method): '200': { 'content': { ct: {'schema': response_schema} - for ct in self.content_types + 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 diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 78a5609dac..5be61448c4 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -287,6 +287,30 @@ class View(generics.DestroyAPIView): }, } + def test_multipart_request_body_generation(self): + """Test that a view's delete method generates a proper response body schema.""" + path = '/{id}/' + method = 'POST' + + class ItemSerializer(serializers.Serializer): + attachment = serializers.FileField() + + class View(generics.CreateAPIView): + serializer_class = ItemSerializer + + view = create_view( + View, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + request_body = inspector._get_request_body(path, method) + assert 'multipart/form-data' in request_body['content'] + attachment = request_body['content']['multipart/form-data']['schema']['properties']['attachment'] + assert attachment['format'] == 'binary' + def test_retrieve_response_body_generation(self): """Test that a list of properties is returned for retrieve item views.""" path = '/{id}/' From 07702abba6000b839edf88bee3ac11620e3ee52a Mon Sep 17 00:00:00 2001 From: Dima Knivets Date: Sun, 11 Aug 2019 15:43:19 +0300 Subject: [PATCH 2/5] removed extra whitespace --- rest_framework/schemas/openapi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index f10ea06af4..468c92219e 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -93,7 +93,6 @@ class AutoSchema(ViewInspector): def get_operation(self, path, method): operation = {} - operation['operationId'] = self._get_operation_id(path, method) parameters = [] From 549fec6379631daa6a16523783fc17fe399d8984 Mon Sep 17 00:00:00 2001 From: Dima Knivets Date: Mon, 12 Aug 2019 11:55:58 +0300 Subject: [PATCH 3/5] making linter happy --- rest_framework/schemas/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 468c92219e..62dafd59c0 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -9,7 +9,7 @@ from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, serializers, renderers +from rest_framework import exceptions, renderers, serializers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty From 324242bf4eac07c61123299729c5b8c2a1914d13 Mon Sep 17 00:00:00 2001 From: Dima Knivets Date: Sun, 11 Aug 2019 15:39:35 +0300 Subject: [PATCH 4/5] Schemas: Map renderers/parsers for request/response media-types. --- rest_framework/schemas/openapi.py | 34 ++++++++++++++++++++++++++----- tests/schemas/test_openapi.py | 24 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 4fee95439b..cd70b9d0c2 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -1,4 +1,5 @@ import warnings +from operator import attrgetter from urllib.parse import urljoin from django.core.validators import ( @@ -8,7 +9,7 @@ from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, serializers +from rest_framework import exceptions, renderers, serializers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty @@ -78,7 +79,9 @@ def get_schema(self, request=None, public=False): class AutoSchema(ViewInspector): - content_types = ['application/json'] + request_media_types = [] + response_media_types = [] + method_mapping = { 'get': 'Retrieve', 'post': 'Create', @@ -336,6 +339,12 @@ def _map_field(self, field): self._map_min_max(field, content) return content + if isinstance(field, serializers.FileField): + return { + 'type': 'string', + 'format': 'binary' + } + # Simplest cases, default to 'string' type: FIELD_CLASS_SCHEMA_TYPE = { serializers.BooleanField: 'boolean', @@ -430,9 +439,20 @@ def _get_pagninator(self): pagination_class = getattr(self.view, 'pagination_class', None) if pagination_class: return pagination_class() - return None + def map_parsers(self, path, method): + return list(map(attrgetter('media_type'), self.view.parser_classes)) + + def map_renderers(self, path, method): + media_types = [] + for renderer in self.view.renderer_classes: + # BrowsableAPIRenderer not relevant to OpenAPI spec + if renderer == renderers.BrowsableAPIRenderer: + continue + media_types.append(renderer.media_type) + return media_types + def _get_serializer(self, method, path): view = self.view @@ -452,6 +472,8 @@ def _get_request_body(self, path, method): if method not in ('PUT', 'PATCH', 'POST'): return {} + self.request_media_types = self.map_parsers(path, method) + serializer = self._get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): @@ -469,7 +491,7 @@ def _get_request_body(self, path, method): return { 'content': { ct: {'schema': content} - for ct in self.content_types + for ct in self.request_media_types } } @@ -482,6 +504,8 @@ def _get_responses(self, path, method): } } + self.response_media_types = self.map_renderers(path, method) + item_schema = {} serializer = self._get_serializer(path, method) @@ -509,7 +533,7 @@ def _get_responses(self, path, method): '200': { 'content': { ct: {'schema': response_schema} - for ct in self.content_types + 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 diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 508e7dba8f..6c9089e05e 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -339,6 +339,30 @@ class View(generics.DestroyAPIView): }, } + def test_multipart_request_body_generation(self): + """Test that a view's delete method generates a proper response body schema.""" + path = '/{id}/' + method = 'POST' + + class ItemSerializer(serializers.Serializer): + attachment = serializers.FileField() + + class View(generics.CreateAPIView): + serializer_class = ItemSerializer + + view = create_view( + View, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + request_body = inspector._get_request_body(path, method) + assert 'multipart/form-data' in request_body['content'] + attachment = request_body['content']['multipart/form-data']['schema']['properties']['attachment'] + assert attachment['format'] == 'binary' + def test_retrieve_response_body_generation(self): """ Test that a list of properties is returned for retrieve item views. From 140f7f104c49cdfe2962dde7210bd8231101b1b3 Mon Sep 17 00:00:00 2001 From: Dima Knivets Date: Sun, 15 Sep 2019 15:19:00 +0300 Subject: [PATCH 5/5] added tests for #6863 implementation --- tests/schemas/test_openapi.py | 57 ++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 6c9089e05e..9197b4478d 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -5,6 +5,8 @@ from rest_framework import filters, generics, pagination, routers, serializers from rest_framework.compat import uritemplate +from rest_framework.parsers import JSONParser, MultiPartParser +from rest_framework.renderers import JSONRenderer from rest_framework.request import Request from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator @@ -339,8 +341,55 @@ class View(generics.DestroyAPIView): }, } - def test_multipart_request_body_generation(self): - """Test that a view's delete method generates a proper response body schema.""" + def test_parser_mapping(self): + """Test that view's parsers are mapped to OA media types""" + path = '/{id}/' + method = 'POST' + + class View(generics.CreateAPIView): + serializer_class = views.ExampleSerializer + parser_classes = [JSONParser, MultiPartParser] + + view = create_view( + View, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + request_body = inspector._get_request_body(path, method) + + assert len(request_body['content'].keys()) == 2 + assert 'multipart/form-data' in request_body['content'] + assert 'application/json' in request_body['content'] + + def test_renderer_mapping(self): + """Test that view's renderers are mapped to OA media types""" + path = '/{id}/' + method = 'GET' + + class View(generics.CreateAPIView): + serializer_class = views.ExampleSerializer + renderer_classes = [JSONRenderer] + + view = create_view( + View, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + responses = inspector._get_responses(path, method) + # TODO this should be changed once the multiple response + # schema support is there + success_response = responses['200'] + + assert len(success_response['content'].keys()) == 1 + assert 'application/json' in success_response['content'] + + def test_serializer_filefield(self): path = '/{id}/' method = 'POST' @@ -359,8 +408,8 @@ class View(generics.CreateAPIView): inspector.view = view request_body = inspector._get_request_body(path, method) - assert 'multipart/form-data' in request_body['content'] - attachment = request_body['content']['multipart/form-data']['schema']['properties']['attachment'] + mp_media = request_body['content']['multipart/form-data'] + attachment = mp_media['schema']['properties']['attachment'] assert attachment['format'] == 'binary' def test_retrieve_response_body_generation(self): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy