-
Notifications
You must be signed in to change notification settings - Fork 270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: AutoSchema: Inherit DRF AutoSchema #46
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ | |
from rest_framework import permissions, renderers, serializers | ||
from rest_framework.fields import _UnvalidatedField, empty | ||
from rest_framework.generics import GenericAPIView | ||
from rest_framework.schemas.inspectors import ViewInspector | ||
from rest_framework.schemas.openapi import AutoSchema as DRFAutoSchema | ||
from rest_framework.schemas.utils import get_pk_description | ||
from rest_framework.settings import api_settings | ||
from rest_framework.views import APIView | ||
|
@@ -34,7 +34,7 @@ | |
from drf_spectacular.authentication import OpenApiAuthenticationExtension | ||
|
||
|
||
class AutoSchema(ViewInspector): | ||
class AutoSchema(DRFAutoSchema): | ||
method_mapping = { | ||
'get': 'retrieve', | ||
'post': 'create', | ||
|
@@ -502,8 +502,9 @@ def _map_serializer_field(self, field): | |
return content | ||
|
||
if isinstance(field, serializers.FileField): | ||
# TODO returns filename. but does it accept binary data on upload? | ||
return build_basic_type(OpenApiTypes.STR) | ||
rv = build_basic_type(OpenApiTypes.STR) | ||
rv['format'] = 'binary' | ||
return rv | ||
|
||
if isinstance(field, serializers.SerializerMethodField): | ||
method = getattr(field.parent, field.method_name) | ||
|
@@ -541,58 +542,44 @@ def _map_min_max(self, field, content): | |
if field.min_value: | ||
content['minimum'] = field.min_value | ||
|
||
def _map_serializer(self, serializer, direction): | ||
def _map_serializer(self, serializer, direction=None): | ||
serializer = force_instance(serializer) | ||
serializer_extension = OpenApiSerializerExtension.get_match(serializer) | ||
|
||
if serializer_extension: | ||
if serializer_extension and direction: | ||
return serializer_extension.map_serializer(self, direction) | ||
else: | ||
return self._map_basic_serializer(serializer, direction) | ||
|
||
def _map_basic_serializer(self, serializer, direction): | ||
required = [] | ||
properties = {} | ||
|
||
for field in serializer.fields.values(): | ||
if isinstance(field, serializers.HiddenField): | ||
continue | ||
|
||
if field.required: | ||
required.append(field.field_name) | ||
|
||
schema = self._map_serializer_field(field) | ||
|
||
if field.read_only: | ||
schema['readOnly'] = True | ||
if field.write_only: | ||
schema['writeOnly'] = True | ||
if field.allow_null: | ||
schema['nullable'] = True | ||
if field.default is not None and field.default != empty and not callable(field.default): | ||
schema['default'] = field.to_representation(field.default) | ||
if field.help_text: | ||
schema['description'] = str(field.help_text) | ||
self._map_field_validators(field, schema) | ||
|
||
# sibling entries to $ref will be ignored as it replaces itself and its context with | ||
# the referenced object. Wrap it in a separate context. | ||
if '$ref' in schema and len(schema) > 1: | ||
schema = {'allOf': [{'$ref': schema.pop('$ref')}], **schema} | ||
|
||
properties[field.field_name] = schema | ||
|
||
result = { | ||
'type': 'object', | ||
'properties': properties | ||
} | ||
if required and (self.method != 'PATCH' or direction == 'response'): | ||
result['required'] = required | ||
|
||
return result | ||
if hasattr(DRFAutoSchema, 'map_serializer'): | ||
result = super().map_serializer(serializer) | ||
else: | ||
result = super()._map_serializer(serializer) | ||
|
||
if result.get('properties'): | ||
# Move 'type' to top | ||
new = {'type': 'object'} | ||
new.update(result) | ||
result = new | ||
|
||
required = result.get('required') | ||
if required and self.method == 'PATCH' and direction == 'request': | ||
del result['required'] | ||
elif required: | ||
# Move required to the end for DRF 3.10 | ||
del result['required'] | ||
result['required'] = required | ||
elif required is not None: | ||
del result['required'] | ||
|
||
return result | ||
|
||
def _map_field_validators(self, field, schema): | ||
for v in field.validators: | ||
# DRF 3.10 'field' is the list of validators | ||
if hasattr(field, 'validators'): | ||
field_validators = field.validators | ||
else: | ||
field_validators = field | ||
|
||
for v in field_validators: | ||
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#data-types | ||
if isinstance(v, validators.EmailValidator): | ||
schema['format'] = 'email' | ||
|
@@ -624,6 +611,69 @@ def _map_field_validators(self, field, schema): | |
schema['maximum'] = int(digits * '9') + 1 | ||
schema['minimum'] = -schema['maximum'] | ||
|
||
if schema.get('type') == 'array': | ||
maxItems = schema.get('maxLength') | ||
if maxItems: | ||
del schema['maxLength'] | ||
schema['maxItems'] = maxItems | ||
minItems = schema.get('minLength') | ||
if minItems: | ||
del schema['minLength'] | ||
schema['minItems'] = minItems | ||
|
||
def _field_has_extra_metadata(self, field): | ||
"""Determine whether other metadata will be added after _map_field.""" | ||
if field.read_only: | ||
return True | ||
if field.write_only: | ||
return True | ||
if field.allow_null: | ||
return True | ||
if field.default and field.default != empty: # why don't they use None?! | ||
return True | ||
if field.help_text: | ||
return True | ||
|
||
def _map_field(self, field): | ||
schema = self._map_serializer_field(field) | ||
|
||
if '$ref' in schema: | ||
schema.pop('type', None) | ||
if len(schema) > 1 or self._field_has_extra_metadata(field): | ||
return {'allOf': [{'$ref': schema.pop('$ref')}], **schema} | ||
return schema | ||
|
||
try: | ||
result = super()._map_field(field) | ||
except RecursionError: | ||
return schema | ||
|
||
result.update(schema) | ||
if result.get('properties'): | ||
result['type'] = 'object' | ||
|
||
# sibling entries to $ref will be ignored as it replaces itself and its context with | ||
# the referenced object. Wrap it in a separate context. | ||
if '$ref' in result: | ||
result.pop('type', None) | ||
if len(result) > 1 or self._field_has_extra_metadata(field): | ||
return {'allOf': [{'$ref': result.pop('$ref')}], **result} | ||
return result | ||
|
||
new = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is only needed to minimise changes to the .yml file. |
||
if result.get('enum'): | ||
new['enum'] = result['enum'] | ||
if result.get('type'): | ||
new['type'] = result['type'] | ||
if result.get('format'): | ||
new['format'] = result['format'] | ||
|
||
new.update(result) | ||
|
||
return new | ||
|
||
map_field = _map_field | ||
|
||
def _map_type_hint(self, method): | ||
hint = getattr(method, '_spectacular_annotation', None) or typing.get_type_hints(method).get('return') | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,8 +161,10 @@ components: | |
minimum: -1000 | ||
field_file: | ||
type: string | ||
format: binary | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a byproduct of using DRF's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Diagnosed and raised as #69 |
||
field_img: | ||
type: string | ||
format: binary | ||
field_date: | ||
type: string | ||
format: date | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
and direction
is optional, and probably needs to be removed.