From 11df9fcbaf10d6eb3bd12905aabc360d2a2e85d0 Mon Sep 17 00:00:00 2001 From: George K Date: Wed, 17 Jan 2018 13:10:55 +0300 Subject: [PATCH 1/9] Probably fixes #117, add support for nested seralizer fields --- drf_openapi/codec.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 8cee482..2d6b3a4 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -56,8 +56,26 @@ def parse_array_field(self): return parameter + def parse_object_field(self): + parameter = { + 'name': self.field.name, + 'required': self.field.required, + 'description': self.field_description, + 'type': self.field_type, + 'properties': { + name: { + 'description': _get_field_description(prop), + 'type': _get_field_type(prop) + } for name, prop in self.field.schema.properties.items() + } + } + + return parameter + def as_parameter(self): - if self.field_type == 'array': + if self.field_type == 'object': + param = self.parse_object_field() + elif self.field_type == 'array': param = self.parse_array_field() else: param = { @@ -82,7 +100,9 @@ def as_body_parameter(self, encoding): return param def as_schema_property(self): - if self.field_type == 'array': + if self.field_type == 'object': + return self.parse_object_field() + elif self.field_type == 'array': return self.parse_array_field() return { From c32fd3edc47f57adf5022aa35743399da68031e0 Mon Sep 17 00:00:00 2001 From: George K Date: Fri, 19 Jan 2018 15:31:35 +0300 Subject: [PATCH 2/9] Exclude object convertion for query parameter --- drf_openapi/codec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 2d6b3a4..84ab43c 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -73,7 +73,7 @@ def parse_object_field(self): return parameter def as_parameter(self): - if self.field_type == 'object': + if self.field_type == 'object' and self.location_string != 'query': param = self.parse_object_field() elif self.field_type == 'array': param = self.parse_array_field() From 8809bc625e628a4adc4f64a41ad109ed5e684a13 Mon Sep 17 00:00:00 2001 From: George K Date: Mon, 22 Jan 2018 14:46:09 +0300 Subject: [PATCH 3/9] Add support for nested serializer for more than one level --- drf_openapi/codec.py | 104 +++++++++++++++++++------------------- drf_openapi/entities.py | 6 ++- drf_openapi/inspectors.py | 75 +++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 54 deletions(-) create mode 100644 drf_openapi/inspectors.py diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 84ab43c..d7a2130 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -9,7 +9,7 @@ from coreapi import Document from coreapi.compat import urlparse, force_bytes from openapi_codec import OpenAPICodec as _OpenAPICodec -from openapi_codec.encode import _get_links, _get_field_description +from openapi_codec.encode import _get_links, _get_field_description, _get_field_required from openapi_codec.utils import get_method, get_encoding, get_location from rest_framework import status from rest_framework.renderers import JSONRenderer @@ -17,70 +17,69 @@ SwaggerUIRenderer as _SwaggerUIRenderer +def parse_nested_field(nested_field): + items_type = _get_field_type(nested_field) + + result = { + 'description': _get_field_description(nested_field), + 'type': items_type + } + + if items_type == 'array': + if hasattr(nested_field, 'schema'): + result['items'] = { + 'type': _get_field_type(nested_field.schema.items), + 'properties': { + name: parse_nested_field(prop) for name, prop in nested_field.schema.items.properties.items() + } + } + result['items']['required'] = nested_field.schema.items.required + elif hasattr(nested_field, 'items'): + result['items'] = { + 'type': _get_field_type(nested_field.items), + 'properties': { + name: parse_nested_field(prop) for name, prop in nested_field.items.properties.items() + } + } + result['items']['required'] = nested_field.items.required + elif items_type == 'object': + if hasattr(nested_field, 'schema'): + result['properties'] = { + name: parse_nested_field(prop) for name, prop in nested_field.schema.properties.items() + } + result['required'] = nested_field.schema.required + elif hasattr(nested_field, 'properties'): + result['properties'] = { + name: parse_nested_field(prop) for name, prop in nested_field.properties.items() + } + result['required'] = nested_field.required + + else: + if hasattr(nested_field, 'name'): + result['name'] = nested_field.name + return result + + class OpenApiFieldParser: def __init__(self, link, field): self.field = field self.field_description = _get_field_description(field) self.field_type = _get_field_type(field) + self.field_required = _get_field_required(field) self.location = get_location(link, field) @property def location_string(self): return 'formData' if self.location == 'form' else self.location - def parse_array_field(self): - parameter = { - 'name': self.field.name, - 'required': self.field.required, - 'description': self.field_description, - 'type': self.field_type, - } - - items_type = _get_field_type(self.field.schema.items) - if items_type == 'object': - parameter['items'] = { - 'type': items_type, - 'properties': { - name: { - 'description': _get_field_description(prop), - 'type': _get_field_type(prop) - } for name, prop in self.field.schema.items.properties.items() - } - } - else: - parameter['items'] = { - 'type': items_type, - 'description': _get_field_description(self.field.schema.items) - } - - return parameter - - def parse_object_field(self): - parameter = { - 'name': self.field.name, - 'required': self.field.required, - 'description': self.field_description, - 'type': self.field_type, - 'properties': { - name: { - 'description': _get_field_description(prop), - 'type': _get_field_type(prop) - } for name, prop in self.field.schema.properties.items() - } - } - - return parameter - def as_parameter(self): - if self.field_type == 'object' and self.location_string != 'query': - param = self.parse_object_field() - elif self.field_type == 'array': - param = self.parse_array_field() + if (self.field_type == 'object' and self.location_string != 'query') or self.field_type == 'array': + param = parse_nested_field(self.field) else: param = { 'name': self.field.name, - 'required': self.field.required, + 'required': self.field_required, 'description': self.field_description, 'type': self.field_type } @@ -100,14 +99,13 @@ def as_body_parameter(self, encoding): return param def as_schema_property(self): - if self.field_type == 'object': - return self.parse_object_field() - elif self.field_type == 'array': - return self.parse_array_field() + if self.field_type in ('object', 'array'): + return parse_nested_field(self.field) return { 'description': self.field_description, 'type': self.field_type, + 'required': self.field_required, } diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index 70bba2d..46e7bb3 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -14,7 +14,8 @@ from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination from rest_framework.schemas import SchemaGenerator from rest_framework.schemas.generators import insert_into, distribute_links, LinkNode -from rest_framework.schemas.inspectors import get_pk_description, field_to_schema +from rest_framework.schemas.inspectors import get_pk_description +from .inspectors import field_to_schema from drf_openapi.codec import _get_parameters @@ -397,6 +398,9 @@ def get_response_object(self, response_serializer_class, description): schema = res[0]['schema'] schema['properties'].update(nested_obj) + if 'required' in schema: + schema['required'] += [nested_field_name for nested_field_name in nested_obj if + getattr(serializer.fields[nested_field_name], 'required', True) is True] response_schema = { 'description': description, 'schema': schema diff --git a/drf_openapi/inspectors.py b/drf_openapi/inspectors.py new file mode 100644 index 0000000..e49c795 --- /dev/null +++ b/drf_openapi/inspectors.py @@ -0,0 +1,75 @@ +from rest_framework.compat import coreschema +from django.utils.encoding import force_text +from rest_framework import serializers +from collections import OrderedDict + + +def field_to_schema(field): + title = force_text(field.label) if field.label else '' + description = force_text(field.help_text) if field.help_text else '' + + if isinstance(field, (serializers.ListSerializer, serializers.ListField)): + child_schema = field_to_schema(field.child) + return coreschema.Array( + items=child_schema, + title=title, + description=description + ) + elif isinstance(field, serializers.Serializer): + return coreschema.Object( + properties=OrderedDict([ + (key, field_to_schema(value)) + for key, value + in field.fields.items() + ]), + required=[field_name for field_name, field_data in field.fields.items() if + getattr(field_data, 'required', True) is True], + title=title, + description=description + ) + elif isinstance(field, serializers.ManyRelatedField): + return coreschema.Array( + items=coreschema.String(), + title=title, + description=description + ) + elif isinstance(field, serializers.RelatedField): + return coreschema.String(title=title, description=description) + elif isinstance(field, serializers.MultipleChoiceField): + return coreschema.Array( + items=coreschema.Enum(enum=list(field.choices.keys())), + title=title, + description=description + ) + elif isinstance(field, serializers.ChoiceField): + return coreschema.Enum( + enum=list(field.choices.keys()), + title=title, + description=description + ) + elif isinstance(field, serializers.BooleanField): + return coreschema.Boolean(title=title, description=description) + elif isinstance(field, (serializers.DecimalField, serializers.FloatField)): + return coreschema.Number(title=title, description=description) + elif isinstance(field, serializers.IntegerField): + return coreschema.Integer(title=title, description=description) + elif isinstance(field, serializers.DateField): + return coreschema.String( + title=title, + description=description, + format='date' + ) + elif isinstance(field, serializers.DateTimeField): + return coreschema.String( + title=title, + description=description, + format='date-time' + ) + + if field.style.get('base_template') == 'textarea.html': + return coreschema.String( + title=title, + description=description, + format='textarea' + ) + return coreschema.String(title=title, description=description) From e363c2bb07705e5c80ec9372ac217c8dd7855511 Mon Sep 17 00:00:00 2001 From: George K Date: Mon, 22 Jan 2018 15:00:37 +0300 Subject: [PATCH 4/9] Fix "_get_field_required" import --- drf_openapi/codec.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index d7a2130..0a62f8c 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -9,7 +9,7 @@ from coreapi import Document from coreapi.compat import urlparse, force_bytes from openapi_codec import OpenAPICodec as _OpenAPICodec -from openapi_codec.encode import _get_links, _get_field_description, _get_field_required +from openapi_codec.encode import _get_links, _get_field_description from openapi_codec.utils import get_method, get_encoding, get_location from rest_framework import status from rest_framework.renderers import JSONRenderer @@ -17,6 +17,10 @@ SwaggerUIRenderer as _SwaggerUIRenderer +def _get_field_required(field): + return getattr(field, 'required', True) + + def parse_nested_field(nested_field): items_type = _get_field_type(nested_field) From 9dff4e7949c915ff188cabf23c31af30b0ccb490 Mon Sep 17 00:00:00 2001 From: George K Date: Mon, 22 Jan 2018 15:42:51 +0300 Subject: [PATCH 5/9] Fix for nested array handling --- drf_openapi/codec.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 0a62f8c..0f591ce 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -31,21 +31,16 @@ def parse_nested_field(nested_field): if items_type == 'array': if hasattr(nested_field, 'schema'): - result['items'] = { - 'type': _get_field_type(nested_field.schema.items), - 'properties': { - name: parse_nested_field(prop) for name, prop in nested_field.schema.items.properties.items() - } - } - result['items']['required'] = nested_field.schema.items.required - elif hasattr(nested_field, 'items'): - result['items'] = { - 'type': _get_field_type(nested_field.items), - 'properties': { - name: parse_nested_field(prop) for name, prop in nested_field.items.properties.items() - } - } - result['items']['required'] = nested_field.items.required + items = nested_field.schema.items + else: + items = nested_field.items + + result['items'] = {'type': _get_field_type(items)} + if hasattr(items, 'properties'): + result['items']['properties'] = {name: parse_nested_field(prop) for name, prop in items.properties.items()} + result['items']['required'] = items.required + # else: + # result['items']['properties'] = {nested_field.name: parse_nested_field(items)} elif items_type == 'object': if hasattr(nested_field, 'schema'): result['properties'] = { From acab0f8f1d42b715dc226c5dea53674f11b24171 Mon Sep 17 00:00:00 2001 From: George K Date: Wed, 24 Jan 2018 14:11:33 +0300 Subject: [PATCH 6/9] Code refactoring + add enum support --- drf_openapi/codec.py | 81 ++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 0f591ce..bdc693e 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -21,41 +21,61 @@ def _get_field_required(field): return getattr(field, 'required', True) -def parse_nested_field(nested_field): - items_type = _get_field_type(nested_field) +def _as_simple_field(field, add_name): + result = { + 'required': _get_field_required(field) + } + + if hasattr(field, 'name') and add_name: + result['name'] = field.name + # enum support for object and schema + if hasattr(field, 'enum'): + result['enum'] = field.enum + if hasattr(field, 'schema') and hasattr(field.schema, 'enum'): + result['enum'] = field.schema.enum + + return result + + +def _parse_field(field, flat=False, add_name=True): + field_type = _get_field_type(field) result = { - 'description': _get_field_description(nested_field), - 'type': items_type + 'description': _get_field_description(field), + 'type': field_type } - if items_type == 'array': - if hasattr(nested_field, 'schema'): - items = nested_field.schema.items + if flat: + result.update(_as_simple_field(field, add_name)) + return result + + if field_type == 'array': + if hasattr(field, 'schema'): + items = field.schema.items else: - items = nested_field.items + items = field.items result['items'] = {'type': _get_field_type(items)} if hasattr(items, 'properties'): - result['items']['properties'] = {name: parse_nested_field(prop) for name, prop in items.properties.items()} - result['items']['required'] = items.required + result['items']['properties'] = {name: _parse_field(prop) for name, prop in items.properties.items()} + result['items']['required'] = _get_field_required(items) # else: # result['items']['properties'] = {nested_field.name: parse_nested_field(items)} - elif items_type == 'object': - if hasattr(nested_field, 'schema'): + elif field_type == 'object': + if hasattr(field, 'schema'): result['properties'] = { - name: parse_nested_field(prop) for name, prop in nested_field.schema.properties.items() + name: _parse_field(prop) for name, prop in field.schema.properties.items() } - result['required'] = nested_field.schema.required - elif hasattr(nested_field, 'properties'): + result['required'] = _get_field_required(field.schema) + elif hasattr(field, 'properties'): result['properties'] = { - name: parse_nested_field(prop) for name, prop in nested_field.properties.items() + name: _parse_field(prop) for name, prop in field.properties.items() } - result['required'] = nested_field.required + result['required'] = _get_field_required(field) else: - if hasattr(nested_field, 'name'): - result['name'] = nested_field.name + result.update(_as_simple_field(field, add_name)) + return result @@ -63,9 +83,6 @@ class OpenApiFieldParser: def __init__(self, link, field): self.field = field - self.field_description = _get_field_description(field) - self.field_type = _get_field_type(field) - self.field_required = _get_field_required(field) self.location = get_location(link, field) @property @@ -73,16 +90,7 @@ def location_string(self): return 'formData' if self.location == 'form' else self.location def as_parameter(self): - if (self.field_type == 'object' and self.location_string != 'query') or self.field_type == 'array': - param = parse_nested_field(self.field) - else: - param = { - 'name': self.field.name, - 'required': self.field_required, - 'description': self.field_description, - 'type': self.field_type - } - + param = _parse_field(self.field, self.location_string == 'query') param['in'] = self.location_string return param @@ -98,14 +106,7 @@ def as_body_parameter(self, encoding): return param def as_schema_property(self): - if self.field_type in ('object', 'array'): - return parse_nested_field(self.field) - - return { - 'description': self.field_description, - 'type': self.field_type, - 'required': self.field_required, - } + return _parse_field(self.field, add_name=False) class OpenAPICodec(_OpenAPICodec): From 64ad06e63c3fefd775203583c45a363595e7d39c Mon Sep 17 00:00:00 2001 From: George K Date: Wed, 24 Jan 2018 14:42:00 +0300 Subject: [PATCH 7/9] Add enum typing support --- drf_openapi/codec.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index bdc693e..87ff8e0 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -21,6 +21,20 @@ def _get_field_required(field): return getattr(field, 'required', True) +def _get_list_type(list_obj): + types = {type(x) for x in list_obj} + + if len(types) != 1: + return "string" + else: + return { + int: "integer", + str: "string", + bool: "boolean", + float: "number" + }.get(next(iter(types)), "string") + + def _as_simple_field(field, add_name): result = { 'required': _get_field_required(field) @@ -31,8 +45,10 @@ def _as_simple_field(field, add_name): # enum support for object and schema if hasattr(field, 'enum'): result['enum'] = field.enum + result['type'] = _get_list_type(field.enum) if hasattr(field, 'schema') and hasattr(field.schema, 'enum'): result['enum'] = field.schema.enum + result['type'] = _get_list_type(field.schema.enum) return result From 0a6a7c4ab8b7cbae88356854ee8f86775f6f5669 Mon Sep 17 00:00:00 2001 From: George K Date: Wed, 24 Jan 2018 16:39:48 +0300 Subject: [PATCH 8/9] Add minLength, maxLength, format support --- drf_openapi/codec.py | 55 +++++++++++++++++++++++---------------- drf_openapi/inspectors.py | 9 ++++++- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 87ff8e0..879cb12 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -35,20 +35,21 @@ def _get_list_type(list_obj): }.get(next(iter(types)), "string") -def _as_simple_field(field, add_name): - result = { - 'required': _get_field_required(field) - } +def _parse_field_prop(field, source, dest=None, include_nulls=False): + result = {} + field_schema = getattr(field, 'schema', None) + if dest is None: + dest = source + + attr = getattr(field, source, None) + if attr is not None or (attr is None and include_nulls): + result[dest] = attr + + if attr is None and field_schema is not None: + attr = getattr(field_schema, source, None) - if hasattr(field, 'name') and add_name: - result['name'] = field.name - # enum support for object and schema - if hasattr(field, 'enum'): - result['enum'] = field.enum - result['type'] = _get_list_type(field.enum) - if hasattr(field, 'schema') and hasattr(field.schema, 'enum'): - result['enum'] = field.schema.enum - result['type'] = _get_list_type(field.schema.enum) + if attr is not None or (attr is None and include_nulls): + result[dest] = attr return result @@ -58,14 +59,27 @@ def _parse_field(field, flat=False, add_name=True): result = { 'description': _get_field_description(field), - 'type': field_type + 'type': field_type, + 'required': _get_field_required(field) } - if flat: - result.update(_as_simple_field(field, add_name)) - return result + # name for all types + if add_name: + result.update(_parse_field_prop(field, 'name')) + + # enum + result.update(_parse_field_prop(field, 'enum')) + if 'enum' in result: + result['type'] = _get_list_type(result['enum']) + + # format + result.update(_parse_field_prop(field, 'format')) - if field_type == 'array': + # string + if field_type == "string": + result.update(_parse_field_prop(field, 'min_length', 'minLength')) + result.update(_parse_field_prop(field, 'max_length', 'maxLength')) + elif field_type == 'array': if hasattr(field, 'schema'): items = field.schema.items else: @@ -75,8 +89,6 @@ def _parse_field(field, flat=False, add_name=True): if hasattr(items, 'properties'): result['items']['properties'] = {name: _parse_field(prop) for name, prop in items.properties.items()} result['items']['required'] = _get_field_required(items) - # else: - # result['items']['properties'] = {nested_field.name: parse_nested_field(items)} elif field_type == 'object': if hasattr(field, 'schema'): result['properties'] = { @@ -89,9 +101,6 @@ def _parse_field(field, flat=False, add_name=True): } result['required'] = _get_field_required(field) - else: - result.update(_as_simple_field(field, add_name)) - return result diff --git a/drf_openapi/inspectors.py b/drf_openapi/inspectors.py index e49c795..9a751f7 100644 --- a/drf_openapi/inspectors.py +++ b/drf_openapi/inspectors.py @@ -8,7 +8,14 @@ def field_to_schema(field): title = force_text(field.label) if field.label else '' description = force_text(field.help_text) if field.help_text else '' - if isinstance(field, (serializers.ListSerializer, serializers.ListField)): + if isinstance(field, serializers.CharField): + return coreschema.String( + title=title, + description=description, + max_length=getattr(field, 'max_length', None), + min_length=getattr(field, 'min_length', None) + ) + elif isinstance(field, (serializers.ListSerializer, serializers.ListField)): child_schema = field_to_schema(field.child) return coreschema.Array( items=child_schema, From dd3f94cfe23ac2b7e003499d264da9bb167f5e2c Mon Sep 17 00:00:00 2001 From: George K Date: Fri, 26 Jan 2018 10:57:16 +0300 Subject: [PATCH 9/9] `flat` parameter removed --- drf_openapi/codec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drf_openapi/codec.py b/drf_openapi/codec.py index 879cb12..9873988 100644 --- a/drf_openapi/codec.py +++ b/drf_openapi/codec.py @@ -54,7 +54,7 @@ def _parse_field_prop(field, source, dest=None, include_nulls=False): return result -def _parse_field(field, flat=False, add_name=True): +def _parse_field(field, add_name=True): field_type = _get_field_type(field) result = { @@ -115,7 +115,7 @@ def location_string(self): return 'formData' if self.location == 'form' else self.location def as_parameter(self): - param = _parse_field(self.field, self.location_string == 'query') + param = _parse_field(self.field, add_name=self.location_string == 'query') param['in'] = self.location_string return param