Skip to content

Commit

Permalink
overhaul documentation #52
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed May 23, 2020
1 parent 7c849da commit c3f5a7e
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 40 deletions.
46 changes: 8 additions & 38 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ Features
- ``MethodSerializerField()`` type via type hinting or ``@extend_schema_field``
- Tags extraction
- Description extraction from ``docstrings``
- Sane fallbacks where no Serializer is available (free-form objects)
- Sane fallbacks
- Sane ``operation_id`` naming (based on path)
- Easy to use hooks for extending the spectacular ``AutoSchema``
- Schema serving with ``SpectacularAPIView`` (Redoc and Swagger-UI views are also available)
- Optional input/output serializer component split
- Included support for:
- `django-polymorphic <https://github.com/django-polymorphic/django-polymorphic>`_ / `django-rest-polymorphic <https://github.com/apirobot/django-rest-polymorphic>`_
- `SimpleJWT <https://github.com/SimpleJWT/django-rest-framework-simplejwt>`_
- `DjangoOAuthToolkit <https://github.com/jazzband/django-oauth-toolkit>`_
- `djangorestframework-jwt <https://github.com/jpadilla/django-rest-framework-jwt>`_ (tested fork `drf-jwt <https://github.com/Styria-Digital/django-rest-framework-jwt>`_)

Incomplete features (in progress):
- optional input/output serializer component split

For more information visit the `documentation <https://drf-spectacular.readthedocs.io>`_.

License
-------
Expand Down Expand Up @@ -169,41 +169,11 @@ the sky is the limit.
# your action behaviour
More customization
^^^^^^^^^^^^^^^^^^

Customization by overriding ``AutoSchema``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Still not satisifed? You want more! We still got you covered. We prepared some convenient hooks for things that
are probably up to taste. If you are careful, you can change pretty much anything.

Don't forget to register your custom AutoSchema in the ``DEFAULT_SCHEMA_CLASS``.

.. code:: python
from drf_spectacular.openapi import AutoSchema
class CustomAutoSchema(AutoSchema):
def get_operation_id(self, path, method):
return 'YOUR-ID'.replace('-', '_')
Extras
^^^^^^

got endpoints that yield list of differing objects? Enter ``PolymorphicProxySerializer``

.. code:: python
@extend_schema(
responses=PolymorphicProxySerializer(
component_name='MetaPerson',
serializers=[SerializerA, SerializerB],
resource_type_field_name='type',
)
)
@api_view()
def poly_list(request):
return Response(list_of_multiple_object_types)
Still not satisifed? You want more! We still got you covered.
Visit `customization <https://drf-spectacular.readthedocs.io/en/latest/customization.html>`_ for more information.


Testing
Expand Down
34 changes: 34 additions & 0 deletions docs/blueprints.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.. _blueprints:

Extension Blueprints
====================

Blueprints are a collection of schema fixes for Django and REST Framework apps.
Some libraries/apps do not play well with `drf-spectacular`'s automatic introspection.
With extensions you can manually provide the necessary information to generate a better schema.

There is no blueprint for the app you are looking for? No problem, you can easily write extensions
yourself. Take the blueprints here as examples and have a look at :ref:`customization`.
Feel free to contribute new ones or fixes with a `PR <https://github.com/tfranzel/drf-spectacular/pulls>`_.
Blueprint files can be found `here <https://github.com/tfranzel/drf-spectacular/tree/master/docs/blueprints>`_.

.. note:: Simply copy&paste the snippets into your codebase. The extensions register
themselves automatically. Just be sure that the python interpreter sees them at least once.
To that end, we recommend creating a ``YOURPROJECT/schema.py`` file and importing it in your
``settings.py`` with ``import * from YOURPROJECT.schema``. Now you are all set.


dj-stripe
---------

Stripe Models for Django: `dj-stripe <https://github.com/dj-stripe/dj-stripe>`_

.. literalinclude:: blueprints/djstripe.py


django-oscar-api
----------------

RESTful API for django-oscar: `django-oscar-api <https://github.com/django-oscar/django-oscar-api>`_

.. literalinclude:: blueprints/oscarapi.py
21 changes: 21 additions & 0 deletions docs/blueprints/djstripe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from djstripe.contrib.rest_framework.serializers import SubscriptionSerializer, CreateSubscriptionSerializer

from drf_spectacular.extensions import OpenApiViewExtension
from drf_spectacular.utils import extend_schema


class FixDjstripeSubscriptionRestView(OpenApiViewExtension):
target_class = 'djstripe.contrib.rest_framework.views.SubscriptionRestView'

def view_replacement(self):
class Fixed(self.target_class):
serializer_class = SubscriptionSerializer

@extend_schema(
request=CreateSubscriptionSerializer,
responses=CreateSubscriptionSerializer
)
def post(self, request, *args, **kwargs):
pass

return Fixed
120 changes: 120 additions & 0 deletions docs/blueprints/oscarapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

from rest_framework import serializers

from drf_spectacular.extensions import (
OpenApiSerializerFieldExtension, OpenApiSerializerExtension, OpenApiViewExtension
)
from drf_spectacular.plumbing import build_basic_type
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_field, OpenApiParameter


class Fix1(OpenApiViewExtension):
target_class = 'oscarapi.views.root.api_root'

def view_replacement(self):
return extend_schema(responses=OpenApiTypes.OBJECT)(self.target_class)


class Fix2(OpenApiViewExtension):
target_class = 'oscarapi.views.product.ProductAvailability'

def view_replacement(self):
from oscarapi.serializers.product import AvailabilitySerializer

class Fixed(self.target_class):
serializer_class = AvailabilitySerializer
return Fixed


class Fix3(OpenApiViewExtension):
target_class = 'oscarapi.views.product.ProductPrice'

def view_replacement(self):
from oscarapi.serializers.checkout import PriceSerializer

class Fixed(self.target_class):
serializer_class = PriceSerializer
return Fixed


class Fix4(OpenApiViewExtension):
target_class = 'oscarapi.views.checkout.UserAddressDetail'

def view_replacement(self):
from oscar.apps.address.models import UserAddress

class Fixed(self.target_class):
queryset = UserAddress.objects.none()
return Fixed


class Fix5(OpenApiViewExtension):
target_class = 'oscarapi.views.product.CategoryList'

def view_replacement(self):
class Fixed(self.target_class):
@extend_schema(parameters=[
OpenApiParameter(name='breadcrumbs', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)
])
def get(self, request, *args, **kwargs):
pass

return Fixed


class Fix6(OpenApiSerializerExtension):
target_class = 'oscarapi.serializers.checkout.OrderSerializer'

def map_serializer(self, auto_schema, direction):
from oscarapi.serializers.checkout import OrderOfferDiscountSerializer, OrderVoucherOfferSerializer

class Fixed(self.target_class):
@extend_schema_field(OrderOfferDiscountSerializer(many=True))
def get_offer_discounts(self):
pass

@extend_schema_field(OpenApiTypes.URI)
def get_payment_url(self):
pass

@extend_schema_field(OrderVoucherOfferSerializer(many=True))
def get_voucher_discounts(self):
pass

return auto_schema._map_serializer(Fixed, direction)


class Fix7(OpenApiSerializerFieldExtension):
target_class = 'oscarapi.serializers.fields.CategoryField'

def map_serializer_field(self, auto_schema, direction):
return build_basic_type(OpenApiTypes.STR)


class Fix8(OpenApiSerializerFieldExtension):
target_class = 'oscarapi.serializers.fields.AttributeValueField'

def map_serializer_field(self, auto_schema, direction):
return {
'oneOf': [
build_basic_type(OpenApiTypes.STR),
]
}


class Fix9(OpenApiSerializerExtension):
target_class = 'oscarapi.serializers.basket.BasketSerializer'

def map_serializer(self, auto_schema, direction):
class Fixed(self.target_class):
is_tax_known = serializers.SerializerMethodField()

def get_is_tax_known(self) -> bool:
pass

return auto_schema._map_serializer(Fixed, direction)


class Fix10(Fix9):
target_class = 'oscarapi.serializers.basket.BasketLineSerializer'
134 changes: 134 additions & 0 deletions docs/customization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
.. _customization:

Workflow & schema customization
===============================

You are not satisfied with your generated schema? Follow these steps in order to get your
schema closer to your API.

.. note:: The warnings emitted by ``./manage.py spectacular --file schema.yaml --validate``
are intended as an indicator to where `drf-spectacular` discovered issues.
Sane fallbacks are used wherever possible and some warnings might not even be relevant to you.
The remaining issues can be solved with the following steps.


Step 1: ``queryset`` and ``serializer_class``
---------------------------------------------
Introspection heavily relies on those two attributes. `get_serializer_class()`
and `get_serializer()` are also used if available. You can also set those
on `APIView`. Even though this is not supported by DRF, `drf-spectacular` will pick
them up and use them.


Step 2: :py:class:`@extend_schema <drf_spectacular.utils.extend_schema>`
------------------------------------------------------------------------
Decorate your view functions with the :py:func:`@extend_schema <drf_spectacular.utils.extend_schema>` decorator.
There is a multitude of override options, but you only need to override what was not properly
discovered in the introspection.

.. code-block:: python
class PersonView(viewsets.GenericViewSet):
@extend_schema(
request=YourRequestSerializer,
responses=YourResponseSerializer,
# more customizations
)
def retrieve(self, request, *args, **kwargs)
# your code
Step 3: :py:class:`@extend_schema_field <drf_spectacular.utils.extend_schema_field>` and type hints
---------------------------------------------------------------------------------------------------
Custom `SerializerField`s might not get picked up properly. You can inform `drf-spectacular`
on what is to be expected with the :py:func:`@extend_schema_field <drf_spectacular.utils.extend_schema_field>`
decorator.

.. code-block:: python
@extend_schema_field(OpenApiTypes.BYTE) # also takes basic python types
class CustomField(serializers.Field):
def to_representation(self, value):
return urlsafe_base64_encode(b'\xf0\xf1\xf2')
Step 4: 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.
Extensions provide a way to hook into the introspection without actually touching the library.

All extensions work on the same principle. You provide a ``target_class`` (import path
string or actual class) and then state what `drf-spectcular` should use instead of what
it would normally discover.


Replace views with :py:class:`OpenApiViewExtension <drf_spectacular.extensions.OpenApiViewExtension>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Many libraries use ``@api_view`` or ``APIView`` instead of `ViewSet` or `GenericAPIView`.
In those cases, introspection has very little to work with. The purpose of this extension
is to augment or switch out the encountered view (only for schema generation). Simply extending
the discovered class (``class Fixed(self.target_class)``) with a ``queryset`` or
``serializer_class`` attribute will often solve most issues.

.. code-block:: python
class Fix4(OpenApiViewExtension):
target_class = 'oscarapi.views.checkout.UserAddressDetail'
def view_replacement(self):
from oscar.apps.address.models import UserAddress
class Fixed(self.target_class):
queryset = UserAddress.objects.none()
return Fixed
Specify authentication with :py:class:`OpenApiAuthenticationExtension <drf_spectacular.extensions.OpenApiAuthenticationExtension>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Authentication classes that do not have 3rd party support will emit warnings and be ignored.
Luckily authentication extensions are very easy to implement. Have a look at the
`default authentication method extensions <https://github.com/tfranzel/drf-spectacular/blob/master/drf_spectacular/authentication.py>`_.

Declare field output with :py:class:`OpenApiSerializerFieldExtension <drf_spectacular.extensions.OpenApiSerializerFieldExtension>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is mainly targeted to custom `SerializerField`'s that are within library code. This extension
is functionally equivalent to :py:func:`@extend_schema_field <drf_spectacular.utils.extend_schema_field>`

.. code-block:: python
class CategoryFieldFix(OpenApiSerializerFieldExtension):
target_class = 'oscarapi.serializers.fields.CategoryField'
def map_serializer_field(self, auto_schema, direction):
# equivalent to return {'type': 'string'}
return build_basic_type(OpenApiTypes.STR)
Declare serializer magic with :py:class:`OpenApiSerializerExtension <drf_spectacular.extensions.OpenApiSerializerExtension>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is one of the more involved extension mechanisms. `drf-spectacular` uses those to implement
`polymorphic serializers <https://github.com/tfranzel/drf-spectacular/blob/master/drf_spectacular/serializers.py>`_.
The usage of this extension is rarely necessary because most custom ``Serializer`` classes stay very
close to the default behaviour.


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

The generated schema is still not to your liking? You are no easy customer, but there is one
more thing you can do. Postprocessing hooks run at the very end of schema generation. This is how
the choice ``Enum`` are consolidated into component objects. You can register additional hooks with the
``POSTPROCESSING_HOOKS`` setting.

.. code-block:: python
def custom_hook(result, generator, request, public):
# your modifications to the schema in parameter result
return result
Congratulations
---------------

You should now have no more warnings and a spectacular schema that satisfies all your requirements.
If that is not the case, feel free to open an `issue <https://github.com/tfranzel/drf-spectacular/issues>`_
and make a suggestion for improvement.
7 changes: 7 additions & 0 deletions docs/drf_spectacular.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ drf\_spectacular\.views
:undoc-members:
:show-inheritance:

drf\_spectacular\.extensions
----------------------------

.. automodule:: drf_spectacular.extensions
:members:
:undoc-members:
:show-inheritance:

drf\_spectacular\.openapi
-------------------------
Expand Down
Loading

0 comments on commit c3f5a7e

Please sign in to comment.