From 1362c57821d5f23f9669717f1a3cb3e225816584 Mon Sep 17 00:00:00 2001 From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com> Date: Wed, 8 Apr 2020 20:25:08 +0530 Subject: [PATCH 1/4] add handling for COERCE_DECIMAL_TO_STRING & Decimal --- rest_framework/schemas/openapi.py | 36 +++++++++++++++---------------- tests/schemas/test_openapi.py | 13 +++++++++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 1d0ec35d54..5eaba6bfec 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -15,6 +15,7 @@ from rest_framework import exceptions, renderers, serializers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty +from rest_framework.settings import api_settings from .generators import BaseSchemaGenerator from .inspectors import ViewInspector @@ -325,30 +326,29 @@ def _get_pagination_parameters(self, path, method): def _map_choicefield(self, field): choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates - if all(isinstance(choice, bool) for choice in choices): - type = 'boolean' - elif all(isinstance(choice, int) for choice in choices): - type = 'integer' - elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` - # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 - type = 'number' - elif all(isinstance(choice, str) for choice in choices): - type = 'string' - else: - type = None - mapping = { # The value of `enum` keyword MUST be an array and SHOULD be unique. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20 'enum': choices } - # If We figured out `type` then and only then we should set it. It must be a string. - # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type - # It is optional but it can not be null. - # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 - if type: - mapping['type'] = type + if all(isinstance(choice, bool) for choice in choices): + mapping['type'] = 'boolean' + elif all(isinstance(choice, int) for choice in choices): + mapping['type'] = 'integer' + elif all(isinstance(choice, Decimal) for choice in choices): + if api_settings.COERCE_DECIMAL_TO_STRING: + mapping['enum'] = [str(choice) for choice in mapping['enum']] + mapping['type'] = 'string' + mapping['format'] = 'decimal' + else: + mapping['type'] = 'number' + elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` + # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 + mapping['type'] = 'number' + elif all(isinstance(choice, str) for choice in choices): + mapping['type'] = 'string' + return mapping def _map_field(self, field): diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index c9f6d967ec..98cd725c7b 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1,5 +1,6 @@ import uuid import warnings +from decimal import Decimal import pytest from django.conf.urls import url @@ -78,6 +79,9 @@ def test_list_field_mapping(self): (1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'), ])), {'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}), + (serializers.ListField(child= + serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])), + {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}), ] @@ -85,6 +89,15 @@ def test_list_field_mapping(self): with self.subTest(field=field): assert inspector._map_field(field) == mapping + @override_settings(REST_FRAMEWORK={'COERCE_DECIMAL_TO_STRING': False}) + def test_decimal_schema_for_choice_field(self): + inspector = AutoSchema() + field = serializers.ListField( + child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])) + mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'} + assert inspector._map_field(field) == mapping + + def test_lazy_string_field(self): class ItemSerializer(serializers.Serializer): text = serializers.CharField(help_text=_('lazy string')) From cfd876ad1b2b496f066cfc55d6fe466fc771ea38 Mon Sep 17 00:00:00 2001 From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com> Date: Wed, 8 Apr 2020 20:31:42 +0530 Subject: [PATCH 2/4] add decimal format for number datatype --- 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 5eaba6bfec..31627f58e5 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -337,10 +337,10 @@ def _map_choicefield(self, field): elif all(isinstance(choice, int) for choice in choices): mapping['type'] = 'integer' elif all(isinstance(choice, Decimal) for choice in choices): + mapping['format'] = 'decimal' if api_settings.COERCE_DECIMAL_TO_STRING: mapping['enum'] = [str(choice) for choice in mapping['enum']] mapping['type'] = 'string' - mapping['format'] = 'decimal' else: mapping['type'] = 'number' elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` From 40de7e2fa4e7827cf2ac61fd886c35c9b8bd6727 Mon Sep 17 00:00:00 2001 From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com> Date: Fri, 10 Apr 2020 03:29:22 +0530 Subject: [PATCH 3/4] add format for decimal --- tests/schemas/test_openapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 98cd725c7b..7a6314a27e 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -94,10 +94,10 @@ def test_decimal_schema_for_choice_field(self): inspector = AutoSchema() field = serializers.ListField( child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])) - mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'} + mapping = {'type': 'array', 'items': { + 'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number', 'format': 'decimal'}} assert inspector._map_field(field) == mapping - def test_lazy_string_field(self): class ItemSerializer(serializers.Serializer): text = serializers.CharField(help_text=_('lazy string')) From a3305e7818a7c2fd6582fad4772dffe3017de209 Mon Sep 17 00:00:00 2001 From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com> Date: Fri, 10 Apr 2020 03:35:22 +0530 Subject: [PATCH 4/4] fix lint test failed --- tests/schemas/test_openapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 7a6314a27e..411bac6dd4 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -79,8 +79,8 @@ def test_list_field_mapping(self): (1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'), ])), {'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}), - (serializers.ListField(child= - serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])), + (serializers.ListField(child=serializers.ChoiceField( + choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])), {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}),