Часть 16. Перестаньте вручную управлять документацией по интерфейсу

Django
Часть 16. Перестаньте вручную управлять документацией по интерфейсу

автор:HelloGitHub-стремящийся к мечте

В большинстве случаев разрабатываемый интерфейс не используется тем, кто разрабатывал интерфейс, поэтому если нет документации по интерфейсу, у других не может быть, какие интерфейсы можно вызывать.Даже если известен URL интерфейса, это трудно узнать какие параметры нужны интерфейсу.Даже если вы знаете эти параметры, вы можете не понимать смысл этих параметров. Поэтому интерфейсный документ должен быть важной конфигурацией проекта.

Есть много способов написать интерфейсный документ. Самый простой и прямой способ — открыть блокнот или текстовый документ, записать подробную информацию и использование интерфейса, а другие могут обратиться к этому документу, чтобы вызвать интерфейс. Хоть это и просто, но имеет очевидные недостатки: во-первых, нужно писать много текста описания, что очень скучно, но по факту эта информация уже отражена в коде, что немного похоже на написание кода заново в естественный язык; при обновлении интерфейса необходимо вручную обновлять интерфейсный документ. Разработчики легко забывают об этом, в результате чего возникает несоответствие между содержанием интерфейсного документа и реальной функцией интерфейса.

Поскольку многие сведения об интерфейсе на самом деле отражены в коде, люди, естественно, задумываются о том, следует ли автоматически извлекать соответствующую информацию из написанного кода для создания документа, чтобы документ интерфейса автоматически обновлялся при изменении кода.Любая проблема может быть решена. .

Конечно, написание интерфейсных документов - это не литературное творение. Чтобы автоматически извлекать информацию из написанного кода для генерации документов, должен быть стандартный формат документа. В противном случае инструмент не может знать, какую информацию извлекать из кода. извлекается, и не знаю, как организовать эту информацию.

После всеобщих усилий в настоящее время существует множество зрелых стандартов документов интерфейса и инструментов генерации, среди которыхOpenAPI SpecificationЭто широко принятый и используемый стандарт.Инструмент автоматизации документации, используемый интерфейсом нашего блога, также будет извлекать информацию о документации из кода на основе стандарта OpenAPI, а затем организовывать ее в стандартный формат OpenAPI.

Советы:

Более знакомый термин, связанный с OpenAPI, — чванство.SwaggerПредоставить ряд бесплатных инструментов с открытым исходным кодом, связанных с OpenAPI, компания, стоящая за нимиSMARTBEAR, известный как лидер индустрии разработки инструментов качества кода.

Введение в OpenAPI

Интерфейсный документ не является литературным произведением, и необходимое ему содержание в основном фиксировано. Например, для интерфейса RESTful достаточно знать следующую ключевую информацию, чтобы завершить вызов к нему. Эта информация, в свою очередь, может определять полный интерфейс RESTful:

  • Метод HTTP и URL-адрес запроса.
  • Полученные параметры (включая параметры пути в URL-адресе, параметры запроса, параметры заголовка HTTP-запроса, тело HTTP-запроса и другие параметры).
  • Контент, возвращаемый интерфейсом.

OpenAPI стандартизирует приведенную выше информацию, тем самым предлагаяOpenAPI specification, если содержимое документа соответствует этому стандарту, инструмент OpenAPI может его обработать, например, визуальный инструмент документа может прочитать содержимое документа и создать документ в формате HTML.

Уведомление:

Последняя версия спецификации OpenAPI в настоящее время — 3, но большинство инструментов в настоящее время лучше всего поддерживают 2, а библиотека, используемая в руководстве, поддерживает только 2.

drf-yasg

drf-yasgЭто стороннее приложение django, которое может автоматически извлекать информацию об интерфейсе из кода, написанного в среде django-rest-framework, для создания документов, соответствующих стандарту OpenAPI. Мы будем использовать его для создания документации интерфейса для приложения блога.

Первым шагом, конечно же, является установка drf-yasg, вход в корневой каталог проекта и выполнение команды:

Command Tab

Linux/macOS
$ pipenv install drf-yasg
Windows
...\> pipenv install drf-yasg

Затем добавьте drf-yasg вINSTALLED_APPSВ пункте конфигурации:

# filename="blogproject/settings/common.py"
INSTALLED_APPS = [
    # 其它已添加的应用...
		"pure_pagination",  # 分页
    "haystack",  # 搜索
    "drf_yasg", # 文档
]

Затем используйте функцию, предоставляемую drf_yasg, для создания представления django, которое будет возвращать содержимое документа в формате HTML, чтобы мы могли просматривать документ интерфейса блога прямо в браузере:

# filename="blogproject/urls.py"
from django.urls import include, path, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions, routers


schema_view = get_schema_view(
    openapi.Info(
        title="HelloDjango REST framework tutorial API",
        default_version="v1",
        description="HelloDjango REST framework tutorial AP",
        terms_of_service="",
        contact=openapi.Contact(email="zmrenwu@163.com"),
        license=openapi.License(name="GPLv3 License"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)

urlpatterns = [
  	# 其它已注册的 URL 模式...
  
    # 文档
    re_path(
        r"swagger(?P<format>\.json|\.yaml)",
        schema_view.without_ui(cache_timeout=0),
        name="schema-json",
    ),
    path(
        "swagger/",
        schema_view.with_ui("swagger", cache_timeout=0),
        name="schema-swagger-ui",
    ),
    path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
]

просто используйтеget_schema_viewМы можем сгенерировать представление документа, а затем сопоставить эту функцию представления с 4 URL-адресами.

Теперь перейдите в корневой каталог проекта и запустите сервер разработки:

Command Tab

Linux/macOS
$ pipenv run python manage.py runserver
Windows
...\> pipenv run python manage.py runserver

тогда посетитеhttp://127.0.0.1:8000/swagger/илиhttp://127.0.0.1:8000/redoc/, вы можете увидетьdrf-yasg автоматически генерирует интерфейсную документацию в формате HTML. если посетитьhttp://127.0.0.1:8000/swagger.jsonилиhttp://127.0.0.1:8000/swagger.yamlВы можете увидеть исходный стандартный документ OpenAPI, swagger и redoc основаны на этом стандартном документе для создания визуального интерфейса пользовательского интерфейса.

Полная документация

Ведь drf-yasg разрабатывается не с использованием искусственного интеллекта, даже если это искусственный интеллект, сложно быть на 100% правильным, ведь код, написанный людьми, может постоянно меняться, и инструмент не может предсказать все возможные До такой степени, что с этим невозможно справиться, автоматически сгенерированная документация может быть неправильной, или сгенерированный контент может не соответствовать нашим ожиданиям.

мы могли бы также посетитьhttp://127.0.0.1:8000/swagger/Давайте посмотрим на эффект, созданный до того, как будет выполнена какая-либо настройка. Видно, что содержимое в целом правильное, и интерфейсы в основном перечислены, но если вы внимательно проверите содержимое каждого интерфейса, то обнаружите некоторые проблемы:

  1. GET /api-version/test/ Этот интерфейс предназначен для тестирования, и мы не хотим, чтобы он отображался в документации.
  2. Практически нет описательной информации о том, что делает этот интерфейс.
  3. Некоторые параметры интерфейса также не имеют описательной информации, что может привести к тому, что пользователь интерфейса не сможет узнать их точное значение.
  4. GET /posts/archive/dates/ Параметры, отображаемые этим интерфейсом, неверны, он не должен принимать какие-либо параметры запроса, и параметры ответа интерфейса также неверны.
  5. GET /posts/{id}/comments/ Этот интерфейс также должен поддерживать параметры запроса пейджинга, но он не указан в сгенерированном документе, и параметры ответа интерфейса также неверны. Правильным должен быть список комментариев с разбивкой на страницы, но в документе есть один объект комментария.
  6. GET /search/ не выводит текст параметра поиска.
  7. Существует дополнительный интерфейс GET /search/{id}/, который нам не нужен, поэтому его не нужно указывать в документации.

Далее мы решим вышеуказанные проблемы одну за другой, и нам нужно лишь немного изменить поведение drf-yasg по умолчанию, чтобы сгенерировать ожидаемое содержимое документа.

Скрыть нежелательные интерфейсы

Сначала скройте нежелательные интерфейсы, упомянутые в пунктах 1 и 7, из автоматически сгенерированной документации.

Для интерфейса GET /api-version/test/ соответствующий ему набор представлений:ApiVersionTestViewSet, добавитьswagger_schemaсвойство класса, установите значениеNone, чтобы drf-yasg знал, что интерфейс, соответствующий этому набору представлений, следует игнорировать.

# filename="blog/views.py"
class ApiVersionTestViewSet(viewsets.ViewSet):  # pragma: no cover
    swagger_schema = None

Способ скрытия интерфейса GET /search/{id}/ немного отличается, поскольку соответствующий набор представленийPostSearchViewНе только этот один интерфейс, описанный выше метод обработки будет скрывать интерфейс всего набора представлений, нам нужно найти способ скрыть интерфейс, соответствующий указанному действию.

drf-yasg предоставляетswagger_auto_schemaДекоратор для украшения вида, просто установите его для декоратораauto_shema=NoneВы можете заставить drf-yasg игнорировать оформленный вид, конкретное использование выглядит следующим образом:

# filename="blog/views.py"
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema


@method_decorator(
    name="retrieve",
    decorator=swagger_auto_schema(
        auto_schema=None,
    ),
)
class PostSearchView(HaystackViewSet):
    index_models = [Post]
    serializer_class = PostHaystackSerializer
    throttle_classes = [PostSearchAnonRateThrottle]

Интерфейс, который необходимо скрыть, соответствует действию получения, поэтому мы украшаем этот метод. так какPostSearchViewунаследовано отHaystackViewSet, который явно не определен в кодеretrieveЭтот метод унаследован от родительского класса, поэтому мы используем вспомогательные функции, предоставляемые django.method_decoratorНеинвазивно добавьте декоратор к методу класса.

Теперь посетите адрес документа интерфейса, вы увидите, что нежелательный интерфейс был скрыт из документа.

Добавить описание функций интерфейса

Далее решаем вторую задачу и добавляем в интерфейс необходимое функциональное описание. drf-yasg поддерживает синтаксический анализ информации описания, соответствующей интерфейсу, из строки документации представления, если она соответствует указанному формату.

Начнем с простого примера, добавим информацию описания для интерфейса GET /categories/, найдемCategoryViewSetViewset, добавьте отформатированную строку документации:

# filename="blog/views.py"
class CategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    """
    博客文章分类视图集

    list:
    返回博客文章分类列表
    """

CategoryViewSetНабор представлений — это просто интерфейс, и соответствующее действиеlist, поэтому строка документации форматируется, как указано выше, и эффект в документе выглядит следующим образом:

Вы можете видеть, что описание, которое мы написали, находится ниже URL-адреса запроса интерфейса. Другие простые интерфейсы могут использовать этот метод для добавления сведений о функциональном описании, оставляя вам содержимое в качестве упражнения.

Содержимое, описанное в совете, также поддерживает формат Markdown, так что мы можем писать богато отформатированное содержимое по мере необходимости.

Для немного более сложного представления, например.PostViewSet, этот набор представлений содержит несколько действий, соответствующих нескольким интерфейсам. Формат информации описания функции почти такой же. Ключевым моментом является указание содержимого, соответствующего каждому действию:

# filename="blog/views.py"
class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    """
    博客文章视图集

    list:
    返回博客文章列表

    retrieve:
    返回博客文章详情

    list_comments:
    返回博客文章下的评论列表

    list_archive_dates:
    返回博客文章归档日期列表
    """

Добавить описание параметра

Далее улучшим документ описания параметров интерфейса. При просмотре параметров каждого интерфейса в автоматически сгенерированном документе обнаруживается, что в основном существуют следующие проблемы:

  • Некоторые параметры не указаны, и невозможно точно узнать, что они означают.
  • Параметры, которые должны быть у некоторых интерфейсов, в документации не указаны.
  • Некоторые параметры, которых не должно быть в интерфейсе, перечислены в документации.

Например, мы можем видеть параметры ответа интерфейса GET /posts/{id}/, большинство из которых имеют описания китайской информации, мы можем сделать вывод, что эти описания автоматически определены в drf-yasgPostполя моделиverbose_nameЗначение параметра извлекается. вtocи body_html, потому что нетPostполя, определенные в , поэтому drf-yasg не может узнать описание этих двух полей.

Как drf-yasg узнает, какие параметры ответа вернет этот интерфейс? Принцип заключается в том, что drf-yasg попытается разобрать сериализатор (Serializer), соответствующий интерфейсу, и извлечь соответствующие поля запроса и ответа из сериализатора (если сериализатор не будет найден, он будет дополнительно моделировать ассоциации десериализатора), поэтому мы может добавлять информацию описания к полям, определенным в сериализаторе. Например, давайтеtocи body_html добавитьlabelпараметр:

# filename="blog/views.py"
class PostRetrieveSerializer(serializers.ModelSerializer):
  	toc = serializers.CharField(label="文章目录")
    body_html = serializers.CharField(label="文章内容")

Получите доступ к адресу документа интерфейса, найдите соответствующий интерфейс, вы увидите, что соответствующая информация описания была добавлена ​​к этим двум полям в документе, и вы также можете использоватьhelp_text(Поля в модели также поддерживают этот параметр), чтобы добавить более подробное описание, например:

# filename="blog/serializers.py"
class PostRetrieveSerializer(serializers.ModelSerializer):
  	toc = serializers.CharField(label="文章目录", help_text="HTML 格式,每个目录条目均由 li 标签包裹。")
    body_html = serializers.CharField(
        label="文章内容", help_text="HTML 格式,从 `body` 字段解析而来。"
    )

Таким образом, значение двух полей очень ясно, и эффект следующий:

Таким же образом можно добавить и другие поля без описательной информации, достаточно найти в коде исходное поле, соответствующее параметрам в документе. Помимо добавления в сериализатор (Serializer), модель (Model). Таким же образом можно задать и параметры фильтра запроса, для примера рассмотрим параметры GET /posts/:

Видно, что параметры, используемые для фильтрации списка статей, не описаны, эти поля определены вPostFilter, давайте изменим код, добавим необходимую пояснительную информацию, а затем перейдем к документу, чтобы увидеть эффект!

# filename="blog/filters.py"
from .models import Category, Post, Tag

class PostFilter(drf_filters.FilterSet):
    created_year = drf_filters.NumberFilter(
        field_name="created_time", lookup_expr="year", help_text="根据文章发表年份过滤文章列表"
    )
    created_month = drf_filters.NumberFilter(
        field_name="created_time", lookup_expr="month", help_text="根据文章发表月份过滤文章列表"
    )
    category = drf_filters.ModelChoiceFilter(
        queryset=Category.objects.all(),
        help_text="根据分类过滤文章列表",
    )
    tags = drf_filters.ModelMultipleChoiceFilter(
        queryset=Tag.objects.all(),
        help_text="根据标签过滤文章列表",
    )

    class Meta:
        model = Post
        fields = ["category", "tags", "created_year", "created_month"]

Далее рассмотрим два интерфейса GET /posts/archive/dates/ и GET /posts/{id}/comments/. В первой документации указаны какие-то неправильные параметры, во второй должны быть параметры пагинации, но в документации их нет.

Сначала посмотрите на GET /posts/archive/dates/, его соответствующее действиеlist_archive_dates, так как действие наследует некоторые свойства из набора представлений, в котором оно находится по умолчанию, а drf-yasg разрешает параметры, поддерживаемые интерфейсом, из этих свойств, например, набор представлений установленfilterset_class = PostFilterиpagination_class=PageNumberPagination(Хотя определение не отображается в наборе представлений, оно настраивается глобально), при разбореlist_archive_dates, drf-yasg неправильно разрешает аргумент, унаследованный от набора представленийPostFilterиPageNumberPagination, поэтому параметры, определенные в этих двух классах, также включены в документ.

Зная причину, есть решение.list_archive_datesУстановите эти два свойства в действии наNone, чтобы переопределить настройки по умолчанию в наборе видов:

# filename="blog/views.py"
class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
  @action(
        # ...
        filter_backends=None, # 将 filter_backends 设为 None,filterset_class 也就不起作用了。
        pagination_class=None,
    )
    def list_archive_dates(self, request, *args, **kwargs):
        # ...

Глядя на этот интерфейс еще раз, нет неправильных параметров.

Затем для обработки интерфейса GET /posts/{id}/comments/ нам нужен документ со списком параметров разбиения на страницы. Действие, соответствующее этому интерфейсу,list_comment. Из приведенного выше анализа это действие четко определеноpagination_class=LimitOffsetPagination, почему drf-yasg не может автоматически определить параметры подкачки? Причина в том, что это действие устанавливаетdetail=True. когдаdetial=True, drf-yasg будет рассматривать интерфейс, соответствующий этому действию, как интерфейс для получения одного ресурса, поэтому считает, что пейджинг не нужен. Но на самом деле мы настроили этот интерфейс, и он действительно возвращает список комментариев. Решение состоит в том, чтобы сообщить drf-yasg, что этот интерфейс возвращает результат списка, пожалуйста, проанализируйте некоторые параметры, связанные с интерфейсом списка:

# filename="blog/views.py"
class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    @action(
        methods=["GET"],
        detail=True,
        # ...
        suffix="List",  # 将这个 action 返回的结果标记为列表,否则 drf-yasg 会根据 detail=True 误判为这是返回单个资源的接口
        pagination_class=LimitOffsetPagination,
        serializer_class=CommentSerializer,
    )
    def list_comments(self, request, *args, **kwargs):
        # ...

Но drf-yasg все же не достаточно умен, он при разборе возможных параметров интерфейса списка тоже ставитPostFilterПоля также анализируются вместе. Это используется для фильтрации сообщений в блоге. Очевидно, что его нельзя использовать для фильтрации списка комментариев. Нам нужно удалить эти нерелевантные параметры. Я сказал это,filter_backendsПросто установите для него значение «Нет».

Исправьте неверные параметры ответа

Внимательно просмотрите сгенерированный документ интерфейса и обнаружите, что возвращаемое содержимое двух интерфейсов неверно.

Одним из них является GET /posts/{id}/comments/. Первоначально мы обнаружили, что ответ этого документа интерфейса представляет собой один объект комментария. Мы также проанализировали указанную выше причину. Согласно drf-yasgdetail=TrueОн был ошибочно обработан как интерфейс, возвращающий один ресурс. Добавив в него дополнительную информацию, сказав drf-yasg, что это интерфейс, который возвращает список ресурсов, проблема решается попутно.

Второй — GET /posts/archive/dates/. Возвращаемое содержимое этого интерфейса должно быть списком дат, но отображаемый документ — это список сообщений в блоге. Тип ответа, выведенный drf-yasg, правильный, но содержание неверное. Причина также очевидна, действие, соответствующее этому интерфейсу,list_archive_dates, drf-yasg не нашел сериализатор (Serializer) для разбора результата ответа в этом действии, поэтому перешел к набору представленийPostViewSetЯ искал это, нашел этоPostListSerializer, а затем проанализируйте его как содержимое, возвращаемое интерфейсом.

Поскольку этот интерфейс возвращает простой список дат и не использует сериализаторы, здесь мы не используем указанныйserializer_classзначение свойства, вместо этого используйтеswagger_auto_schemaДекоратор, который напрямую сообщает ответ, возвращаемый интерфейсом drf-yasg:

# filename="blog/views.py"
class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
		@swagger_auto_schema(responses={200: "归档日期列表,时间倒序排列。例如:['2020-08', '2020-06']"})
    @action(
        methods=["GET"],
        detail=False,
        url_path="archive/dates",
        url_name="archive-date",
        filter_backends=None,
        pagination_class=None,
    )
    def list_archive_dates(self, request, *args, **kwargs):
        # ...

responsesЗначением параметра является словарь, ключом словаря является код ответа HTTP, а значением может быть сериализатор, так что drf-yasg будет использовать этот сериализатор для разбора параметров ответа интерфейса; также может быть строка, drf-yasg Строка будет записана непосредственно в документ как результат ответа интерфейса. Взгляните на модифицированный эффект:

На данный момент у нас есть относительно полный набор документов интерфейса блога, и большая часть контента автоматически генерируется для нас drf-yasg, избавляя от многих проблем с рукописными документами.

использованная литература

Вот некоторые ссылки, используемые в учебнике:

Советы:

Официальная документация drf-yasg не очень ясна в отношении использования этой библиотеки.Некоторые варианты использования, перечисленные в этой статье, видны из исходного кода. Если вы столкнулись с проблемой во время использования, сначала попытайтесь проанализировать причину проблемы, а затем следуйте подсказкам, чтобы найти соответствующий исходный код, чтобы увидеть, как библиотека справляется с проблемами, с которыми вы сталкиваетесь, чтобы вы могли предложить целевые решения Решение, многие из проблем, перечисленных в этом руководстве, и решения, приведенные в конце, используются таким образом.