Skip to content

Commit

Permalink
Merge pull request #231 from tfranzel/pr208
Browse files Browse the repository at this point in the history
PR 208 refactoring
  • Loading branch information
tfranzel authored Dec 14, 2020
2 parents 5c660d1 + e168d55 commit 047dd1f
Show file tree
Hide file tree
Showing 7 changed files with 539 additions and 24 deletions.
20 changes: 19 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Features
- ``MethodSerializerField()`` type via type hinting or ``@extend_schema_field``
- i18n support
- Tags extraction
- Request/response/parameter examples
- Description extraction from ``docstrings``
- Sane fallbacks
- Sane ``operation_id`` naming (based on path)
Expand Down Expand Up @@ -165,6 +166,15 @@ the sky is the limit.
type=OpenApiTypes.DATE,
location=OpenApiParameter.QUERY,
description='Filter by release date',
examples=[
OpenApiExample(
'Example 1',
summary='short optional summary',
description='longer description'
value='1993-08-23'
),
...
],
),
],
# override default docstring extraction
Expand All @@ -175,6 +185,15 @@ the sky is the limit.
operation_id=None,
# or even completely override what AutoSchema would generate. Provide raw Open API spec as Dict.
operation=None,
# attach request/response examples to the operation.
examples=[
OpenApiExample(
'Example 1'
description='longer description'
value=...
),
...
],
)
def list(self, request):
# your non-standard behaviour
Expand All @@ -188,7 +207,6 @@ the sky is the limit.
def set_password(self, request, pk=None):
# your action behaviour
More customization
^^^^^^^^^^^^^^^^^^

Expand Down
41 changes: 38 additions & 3 deletions docs/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,42 @@ You can apply it also to the method of a `SerializerMethodField`.
def get_field_custom(self, object):
return '2020-03-06 20:54:00.104248'
Step 4: Extensions
Step 4: :py:class:`@extend_schema_serializer <drf_spectacular.utils.extend_schema_serializer>`
-----------------------------------------------------------------------------------------------

You may also decorate your serializer with :py:func:`@extend_schema_serializer <drf_spectacular.utils.extend_schema_serializer>`.
Mainly used for excluding specific fields from the schema or attaching request/response examples.
On rare occasions (e.g. envelope serializers), overriding list detection with ``many=False`` may come in handy.

.. code:: python
@extend_schema_serializer(
exclude_fields=('single',) # schema ignore these fields
examples = [
OpenApiExample(
'Valid example 1',
summary='short summary',
description='longer description',
value={
'songs': {'top10': True}
'single': {'top10': True}
},
request_only=True, # signal that example only applies to requests
response_only=False, # signal that example only applies to responses
),
]
)
class AlbumSerializer(serializers.ModelSerializer):
songs = SongSerializer(many=True)
single = SongSerializer(read_only=True)
class Meta:
fields = '__all__'
model = Album
Step 5: Extensions
------------------
The core purpose of extensions is to make the above customization mechanisms also available for library code.
Usually, you cannot easily decorate or modify ``View``, ``Serializer`` or ``Field`` from libraries.
Expand Down Expand Up @@ -183,7 +218,7 @@ The usage of this extension is rarely necessary because most custom ``Serializer
close to the default behaviour.


Step 5: Postprocessing hooks
Step 6: Postprocessing hooks
----------------------------

The generated schema is still not to your liking? You are no easy customer, but there is one
Expand All @@ -198,7 +233,7 @@ the choice ``Enum`` are consolidated into component objects. You can register ad
return result
Step 6: Preprocessing hooks
Step 7: Preprocessing hooks
---------------------------
.. _customization_preprocessing_hooks:

Expand Down
71 changes: 55 additions & 16 deletions drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension
from drf_spectacular.plumbing import (
ComponentRegistry, ResolvedComponent, UnableToProceedError, anyisinstance, append_meta,
build_array_type, build_basic_type, build_choice_field, build_object_type, build_parameter_type,
error, follow_field_source, force_instance, get_doc, get_view_model, is_basic_type, is_field,
is_serializer, resolve_regex_path_parameter, resolve_type_hint, safe_ref, warn,
build_array_type, build_basic_type, build_choice_field, build_examples_list,
build_media_type_object, build_object_type, build_parameter_type, error, follow_field_source,
force_instance, get_doc, get_view_model, is_basic_type, is_field, is_serializer,
resolve_regex_path_parameter, resolve_type_hint, safe_ref, warn,
)
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
Expand Down Expand Up @@ -135,6 +136,7 @@ def _process_override_parameters(self):
description=parameter.description,
enum=parameter.enum,
deprecated=parameter.deprecated,
examples=build_examples_list(parameter.examples),
))
elif is_serializer(parameter):
# explode serializer into separate parameters. defaults to QUERY location
Expand All @@ -144,7 +146,7 @@ def _process_override_parameters(self):
name=property_name,
schema=property_schema,
location=OpenApiParameter.QUERY,
required=property_name in mapped.get('required', [])
required=property_name in mapped.get('required', []),
))
else:
warn(f'could not resolve parameter annotation {parameter}. skipping.')
Expand Down Expand Up @@ -785,6 +787,32 @@ def _get_serializer(self):
f'a request? Ignoring the view for now. (Exception: {exc})'
)

def get_examples(self):
return []

def _get_examples(self, serializer, direction, media_type, status_code=None):
examples = self.get_examples()

if not examples:
if isinstance(serializer, serializers.ListSerializer):
examples = get_override(serializer.child, 'examples', [])
elif is_serializer(serializer):
examples = get_override(serializer, 'examples', [])

filtered_examples = []
for example in examples:
if direction == 'request' and example.response_only:
continue
if direction == 'response' and example.request_only:
continue
if media_type and media_type != example.media_type:
continue
if status_code and status_code not in example.status_codes:
continue
filtered_examples.append(example)

return build_examples_list(filtered_examples)

def _get_request_body(self):
# only unsafe methods can have a body
if self.method not in ('PUT', 'PATCH', 'POST'):
Expand Down Expand Up @@ -828,9 +856,14 @@ def _get_request_body(self):

request_body = {
'content': {
request_media_types: {'schema': schema} for request_media_types in self.map_parsers()
media_type: build_media_type_object(
schema,
self._get_examples(serializer, 'request', media_type)
)
for media_type in self.map_parsers()
}
}

if request_body_required:
request_body['required'] = request_body_required

Expand All @@ -843,21 +876,21 @@ def _get_response_bodies(self):
if self.method == 'DELETE':
return {'204': {'description': _('No response body')}}
if self.method == 'POST' and getattr(self.view, 'action', None) == 'create':
return {'201': self._get_response_for_code(response_serializers)}
return {'200': self._get_response_for_code(response_serializers)}
return {'201': self._get_response_for_code(response_serializers, '201')}
return {'200': self._get_response_for_code(response_serializers, '200')}
elif isinstance(response_serializers, dict):
# custom handling for overriding default return codes with @extend_schema
responses = {}
for code, serializer in response_serializers.items():
if isinstance(code, tuple):
code, media_types = code[0], code[1:]
code, media_types = str(code[0]), code[1:]
else:
media_types = None
content_response = self._get_response_for_code(serializer, media_types)
if str(code) in responses:
responses[str(code)]['content'].update(content_response['content'])
code, media_types = str(code), None
content_response = self._get_response_for_code(serializer, code, media_types)
if code in responses:
responses[code]['content'].update(content_response['content'])
else:
responses[str(code)] = content_response
responses[code] = content_response
return responses
else:
warn(
Expand All @@ -867,9 +900,9 @@ def _get_response_bodies(self):
)
schema = build_basic_type(OpenApiTypes.OBJECT)
schema['description'] = _('Unspecified response body')
return {'200': self._get_response_for_code(schema)}
return {'200': self._get_response_for_code(schema, '200')}

def _get_response_for_code(self, serializer, media_types=None):
def _get_response_for_code(self, serializer, status_code, media_types=None):
serializer = force_instance(serializer)

if not serializer:
Expand Down Expand Up @@ -919,7 +952,13 @@ def _get_response_for_code(self, serializer, media_types=None):
media_types = self.map_renderers('media_type')

return {
'content': {mt: {'schema': schema} for mt in media_types},
'content': {
media_type: build_media_type_object(
schema,
self._get_examples(serializer, 'response', media_type, status_code)
)
for media_type in media_types
},
# 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
Expand Down
31 changes: 30 additions & 1 deletion drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,32 @@ def build_object_type(
return schema


def build_media_type_object(schema, examples=None):
media_type_object = {'schema': schema}
if examples:
media_type_object['examples'] = examples
return media_type_object


def build_examples_list(examples):
schema = {}
for example in examples:
normalized_name = inflection.camelize(example.name.replace(' ', '_'))
sub_schema = {}
if example.value:
sub_schema['value'] = example.value
if example.external_value:
sub_schema['externalValue'] = example.external_value
if example.summary:
sub_schema['summary'] = example.summary
elif normalized_name != example.name:
sub_schema['summary'] = example.name
if example.description:
sub_schema['description'] = example.description
schema[normalized_name] = sub_schema
return schema


def build_parameter_type(
name,
schema,
Expand All @@ -197,7 +223,8 @@ def build_parameter_type(
enum=None,
deprecated=False,
explode=None,
style=None
style=None,
examples=None,
):
irrelevant_field_meta = ['readOnly', 'writeOnly']
if location == OpenApiParameter.PATH:
Expand All @@ -219,6 +246,8 @@ def build_parameter_type(
schema['style'] = style
if enum:
schema['schema']['enum'] = sorted(enum)
if examples:
schema['examples'] = examples
return schema


Expand Down
Loading

0 comments on commit 047dd1f

Please sign in to comment.