К старту курса по тестированию на Python при помощи шаблона Read the Docs, пакетов restclient, ob-restclient и Org Mode в Emacs напишем красивую, полезную документацию API, которая генерируется автоматически и легко отображается на статическом сайте.


По шкале полезности от «NFT» до «Integer» грамотное программирование обычно занимает чуть ниже нижнего уровня. Это несправедливо и не совсем заслуженно, но есть вещи, для которых LitProg подходит очень хорошо.

Вы сможете полностью избавиться от Postman или любого другого проприетарного клиента REST API. При таком подходе документация API — это программа. Единственное требование здесь — наличие API, с которым можно общаться. Мы воспользуемся бесплатным и общедоступным API — JSON Placeholder.

Я писал этот пост в режиме Org Mode, поэтому включённые в него примеры Org экспортируются без подсветки синтаксиса. Достаточно сказать, что текст Org выглядит в Emacs гораздо приятнее.

TL;DR. Законченный пример файла Org.

Начинаем

Сначала установите и настройте в вашем Emacs пакеты restclient и ob-restclient. Затем создайте в Emacs файл jsonplaceholder.org. В верхней части этого файла включите следующую информацию заголовка, измените автора и электронную почту:

#+title: JSON Placeholder API Documentation
#+author: Joseph Edwards VIII
#+email: foobar@example.com

#+startup: indent
#+export_file_name: index.html
#+options: num:nil ^:nil H:5 toc:2

#+setupfile: https://fniessen.github.io/org-html-themes/org/theme-readtheorg.setup

Не сразу понятно, что происходит, поэтому давайте немного поговорим об этом заголовке. Верхний раздел не требует пояснений. Второй раздел просто указывает Emacs на отображение org-режима с отступами, задаёт имя файла для экспорта и управляет отображением заголовков разделов и оглавления.

Последний раздел загружает HTML-шаблон setupfile, исрользуемый при экспорте в HTML. На практике для настройки CSS-темы ReadTheOrg я также добавляю несколько элементов #+html_head:.

Инициализация

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

Параметризация URL API может показаться странной. Разве мы не хотим, чтобы параметры отображались в наших запросах? Возможно. Но, может быть, как у большинства инженеров-программистов, у нас есть отдельные среды — разработка, стейджинг и продакшн. Для изменения среды пришлось бы найти и заменить каждый URL. А так мы можем просто изменить адрес в одном месте.

Одно небольшое замечание: не хочется экспортировать этот раздел в HTML. Можно сообщить об этом Emacs, добавив в заголовок раздела тег :noexport:.

В файл jsonplaceholder.org добавьте этот код:

* Init :noexport:

#+name: api-url
: https://jsonplaceholder.typicode.com

Установка #+name: api-url означает, что можно ссылаться на скалярное значение : https://jsonplaceholder.typicode.com из других блоков src нашего документа. api-url можно взять как входные данные.

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

Хотя на практике для выполнения запросов API мы будем использовать jsonplaceholder.org. При экспорте в HTML для создания документации API-запросы также будет выполнять Emacs. Это напоминает о том, что мы работаем с документацией. Поэтому напишите для своих читателей хорошее введение.

В файле jsonplaceholder.org добавьте этот код. Он скопирован из документации по API JSON Placeholder:

* Introduction

JSONPlaceholder is a free online REST API that you can use whenever you need some fake data. It can be in a README on GitHub, for a demo on CodeSandbox, in code examples on Stack Overflow, ...or simply to test things locally.

Выполнение запросов

Давайте задокументируем наш первый запрос и ответ. JSON Placeholder предоставляет шесть ресурсов и маршруты для всех методов HTTP. Начнём с ресурса /posts.

В файл jsonplaceholder.org добавьте этот код:

* Posts

The ~/posts~ resource allows us to create, read, update, and delete posts.

#+name: get-posts
#+begin_src restclient :var api=api-url
  GET :api/posts
#+end_src

Обратите внимание, что в +begin_src мы указываем Org в качестве языка использовать restclient.

Мы также устанавливаем переменную api в значение, хранящееся в скаляре с именем api-url. Вы просто используете переменную внутри блока src Org, как обычно в этом языке. В restclient переменные предваряются двоеточием, поэтому в запросе она используется так: GET :api/posts.

Теперь выполните вызов API. Для этого внутри блока src наберите C-c C-c. Ответ во всей своей красе отобразится под блоком запроса. И это иллюстрирует полезность грамотной документации API.

Мы не собираемся включать примеры запросов и ответов, которые должны обновляться каждый раз, когда изменяются ответы или конечные точки API. Включены только реальные запросы!

Экспорт

У нас только один запрос. Для экспорта его достаточно. Давайте попробуем!

Для экспорта в index.html в том же каталоге, что и ваш файл org, в файле jsonplaceholder.org введите C-c C-e h o. Затем откройте этот файл в браузере:

Хорошо, но где же ответ? Emacs не сделал запрос API и не включил ответ. Но всё в порядке.

По умолчанию Emacs экспортирует только блок src. Чтобы экспортировать результаты, отредактируйте заголовок блока src с именем get-posts. Добавьте :exports both, чтобы код выглядел так:

#+begin_src restclient :var api=api-url :exports both

И экспорт. Теперь вы должны увидеть ответ:

Больше не нужно обновлять примеры ответов, внося в них досадные опечатки. Это реальные, интерактивные данные ответа.

Внешний вид

Прежде чем мы продолжим «программирование» как часть «грамотного программирования», давайте на минуту вспомним, что мы пишем документацию API и хотим, чтобы она была чистой и читабельной. Потратим немного времени на внешний вид документации.

Сейчас у нас некрасивый документ:

  1. Якоря к заголовкам выглядят некрасиво и читаются как index.html#orga1b1b2d.

  2. Ответ слишком большой. Чтобы добраться до следующего запроса, придётся прокручивать страницы.

  3. Заголовки ответа выгружаются в нижней части ответа. Это не чистый JSON.

Некрасивые якоря

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

В файле jsonplaceholder.org установите курсор в конец заголовка Introduction. Чтобы добавить свойство, введите C-c C-x p. В качестве имени свойства введите CUSTOM_ID, а в качестве значения — introduction.

То же самое сделайте для заголовка Posts, установив CUSTOM_ID в resource-posts.

Заголовок теперь должен выглядеть так:

* Posts
:PROPERTIES:
:CUSTOM_ID: resource-posts
:END:

Огромный ответ

Осмотр index.html показывает, что размер блока ответа определяется тегом и классом pre.src. После некоторой работы с моей стороны получился такой фрагмент:

<style>pre.src{background:#343131;color:white;max-height:500px;overflow-y:auto;}</style>

При помощи заголовка #+html_head: можно очень легко добавить его в наш экспортированный index.html в Org Mode. После строки, которая с началом #+setupfile: добавьте:

#+html_head: <style>pre.src{background:#343131;color:white;max-height:500px;overflow-y:auto;} </style>

Кроме того, никому не нравится светлая тема, поэтому я перешёл на тёмную.

Некрасивый ответ

Некрасивые заголовки ответа приходят из пакета restclient. Без взлома самого restclient подавить их нельзя. Нет, это, конечно, можно сделать… В конце концов, это… это Emacs. Воспользуемся грамотным решением.

Одна из самых мощных особенностей грамотного программирования — языковая диагностика. Из Org Mode выполняется SQL-запрос, его данные передаются в блок src Python, где мы манипулируем ими, а затем передаём их в Bash, R, Elisp, Common Lisp и обратно в Python, сохраняя их в базе данных.

Мы воспользуемся оболочкой и CLI-процессором JSON jq. Установим jq и изменим запрос Posts:

#+name: get-posts
#+begin_src restclient :var api=api-url :results value
  GET :api/posts
#+end_src

#+begin_src shell :var response=get-posts :results value raw :wrap src js :exports results
  echo $response | jq
#+end_src

Поместим курсор в нижний блок src (shell) и введём C-c C-c. Блок ответа вызывает блок запроса get-posts и передаёт $response в jq. И больше никаких заголовков ответов. Только чистый, приятный JSON.

Объясним чёрную магию

Давайте посмотрим, что под капотом. Первое значительное изменение коснулось запроса. Мы больше не говорим: :exports both, но говорим :results value. Как уже упоминалось, по умолчанию для :exports используется только блок src. Удалив :exports both, мы вернёмся к умолчанию. Этот блок src не будет вычисляться, а результаты не будут экспортироваться.

Теперь мы говорим запросу: :results value. Что это значит? Это сложно, но в основном это означает, что Org не использует внешний процесс, а получает значение из самого вычисляемого кода.

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

  1. На этот раз мы вызываем оболочку. Любую (bash, cmd.exe, fish и так далее), которую вы используете.

  2. Устанавливаем новую переменную :var response=get-posts. Теперь этот блок src будет вызывать блок запроса с именем get-posts и помещать результаты в переменную с именем response.

  3. Устанавливаем :results value raw, потому что не хотим, чтобы Org обернул результаты в тип shell.

  4. Устанавливаем :wrap src js, потому что на самом деле хочется, чтобы Org обернул результаты в тип js.

  5. Устанавливаем :exports results, потому что хочется видеть только результаты, а не исходный код.

Наконец, внутри блока shell мы передаём переменную $results в jq, который удаляет строки комментариев в заголовке ответа. Для блока результатов он выводит красиво отформатированный JSON.

Собираем всё вместе

Давайте посмотрим, как сработали наши изменения внешнего вида. Экспортируйте ваш файл jsonplaceholder.org снова с помощью C-c C-e h o. Вы должны увидеть нечто подобное:

Дополнительные штрихи

Выглядит намного лучше, но есть несколько недоработок. Быстро пройдём до конца, потому что применяются те же принципы, которые мы уже обсуждали.

Измените файл jsonplaceholder.org, чтобы он выглядел так:

#+title: JSON Placeholder API Documentation
#+author: Joseph Edwards VIII
#+email: foobar@example.com

#+startup: indent
#+export_file_name: index.html
#+options: num:nil ^:nil H:5 toc:2

#+setupfile: https://fniessen.github.io/org-html-themes/org/theme-readtheorg.setup
#+html_head: <style>pre.src{background:#343131;color:white;max-height:500px;overflow-y:auto;} </style>
#+html_head: <style>p{margin-bottom:1em;}</style>
#+html_head: <style>h2{padding-top:1em;margin-top:2em;border-top:darkgray 2px solid;}</style>
#+html_head: <style>h3{padding-top:1em;margin-top:2em;border-top:lightblue 1px dashed;}</style>
#+html_head: <style>h4{padding-top:1em;margin-bottom:1em;}</style>
#+html_head: <style>h5{color:black;font-size:1em;padding-top:1em;margin-bottom:1em;} </style>
#+html_head: <style>div.response>h5,div.request>h5{padding-top:0;margin-bottom:0.5em;color:darkgray;font-size:0.8em;}</style>
#+html_head: <style>h6{margin-bottom:1em;}</style>

* Init :noexport:

#+name: api-url
: https://jsonplaceholder.typicode.com

* Introduction
:PROPERTIES:
:CUSTOM_ID: introduction
:END:

JSONPlaceholder is a free online REST API that you can use whenever you need some fake data. It can be in a README on GitHub, for a demo on CodeSandbox, in code examples on Stack Overflow, ...or simply to test things locally.

* Posts
:PROPERTIES:
:CUSTOM_ID: resource-posts
:END:

The ~/posts~ resource allows us to create, read, update, and delete posts.

** ~GET /posts~
:PROPERTIES:
:CUSTOM_ID: method-get-posts
:END:

Obtain all posts.

**** Request
:PROPERTIES:
:HTML_CONTAINER_CLASS: request
:END:

#+name: get-posts
#+begin_src restclient :var api=api-url :results value
  GET :api/posts
#+end_src

**** Response
:PROPERTIES:
:HTML_CONTAINER_CLASS: response
:END:

#+begin_src shell :var response=get-posts :results value raw :wrap src js :exports results
  echo $response | jq
#+end_src

Файл index.html в браузере должен выглядеть так:

Немного поговорим

Вкратце остановимся на нескольких пунктах вышеуказанных изменений.

Обратите внимание на новые свойства :HTML_CONTAINER_CLASS: request и :HTML_CONTAINER_CLASS: response.

При экспорте это свойство прикрепляет соответствующий CSS-класс (.request или .response) к элементу div, который содержит заголовок. Таким образом, чтобы было понятно, для чего предназначен каждый из этих блоков, можно стилизовать div.request>h5 и div.response>h5.

Также обратите внимание, что разделы запроса и ответа перенесены в новый подзаголовок Posts, данные для которого вызываются запросом GET /posts. Теперь, нажимая на "Posts" в меню боковой панели, мы увидим GET /posts как пункт подменю.

Это поведение контролируется включением директивы заголовка Org #+options: toc:2. Установив значение toc:1, мы не увидим подменю. А установив значение toc:3 в подменю ниже "GET /posts" увидим "Request" и "Response". Попробуйте и посмотрите!

Заканчиваем работу

Оставшаяся часть API документируется по установленной схеме. Процесс можно ускорить, используя yasnippet для вставки нового раздела метода. Однако это не отражает полезность подхода грамотного программирования к документации API.

Обычно я пишу эту документацию по мере создания новых конечных точек API, чтобы протестировать и усовершенствовать конечную точку. Этот метод я использую вместо Postman, и, когда я заканчиваю работу над API, он уже документирован и находится в системе контроля версий вместе с исходным кодом.

Например, я написал бы то, что мы имеем на данный момент, в то же время, когда писал GET :api/posts, и широко использовал этот файл, делая заметки обо всех необходимых параметрах. После публикации кода любой пользователь Emacs из моей команды может открыть README.org и протестировать API самостоятельно.

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

Позвольте завершить этот пост документированием остальных методов ресурса /posts. Документирование проиллюстрирует, как использовать restclient для выполнения HTTP-методов POST, PUT и DELETE.

Добавление метода POST

Добавим заголовок POST /posts под GET /posts. Вы можете скопировать, вставить и изменить эти части кода:

  1. :CUSTOM_ID: method-get-posts на method-post-posts.

  2. "Запрос" #+name: c get-posts в post-posts

  3. "Ответ" :var response get-posts на post-posts

В заголовок блока src запроса post-posts добавьте переменную :var user-id=1. В requests нужно указать тип содержимого и полезную нагрузку JSON для доставки.

Обратите внимание, что мы можем использовать переменные Org внутри полезной нагрузки JSON.

Окончательный POST-запрос должен выглядеть так:

#+name: post-posts
#+begin_src restclient :var api=api-url :var user-id=1 :results value
  POST :api/posts
  Content-Type: application/json

  {
    "title": "foo",
    "body": "bar",
    "userId": :user-id
  }
#+end_src

Теперь, когда вы поместите курсор в поле «Response» и введёте C-c C-c, вы должны получить данные нового POST, которые создали в JSON Placeholder. Это должно выглядеть примерно так:

{
  "title": "foo",
  "body": "bar",
  "userId": 1,
  "id": 101
}

Добавление метода PUT

Как вы можете себе представить, процесс происходит аналогичным образом. Скопируйте и вставьте POST /posts в PUT /posts/:id, измените пользовательский ID, имя запроса и переменную ответа на put-posts.

Теперь наш запрос должен выглядеть аналогично POST-запросу, только нужно указать id поста в URL. Используемый JSON будет обновлять существующий POST, а не создавать новый. Финальный запрос PUT должен быть таким:

#+name: put-posts
#+begin_src restclient :var api=api-url :var id=1 :var user-id=1 :results value
  PUT :api/posts/:id
  Content-Type: application/json

  {
    "title": "foo",
    "body": "bar",
    "userId": :user-id
  }
#+end_src

Добавление метода DELETE

Снова скопируйте, вставьте, измените и выполните.

#+name: delete-posts
#+begin_src restclient :var api=api-url :var id=1 :results value
  DELETE :api/posts/:id
#+end_src

Заключение

Окончательная документация не очень подробна (мы задокументировали только ресурс /posts), но остальное — это просто повторение того, что мы уже обсуждали. Тем не менее она довольно приятная. И легко сделать её ещё лучше:

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

Также повторюсь, что это не самый полезный подход к написанию документации API в стиле грамотного программирования. Полезнее писать её по ходу дела, используя документ вместо Postman. Тогда ваша документация не только никогда не будет ошибочной или не синхронизированной с API. Ещё вам не придётся возвращаться назад и тратить время на документирование того, что вы уже закончили!

Что дальше?

При написании документации API в стиле грамотного программирования могут оказаться полезными более сложные методы грамотного программирования.

У меня есть один документ, который я написал для Wurkzen API. Он позволяет получить и кешировать токен Bearer, который используется в последующих запросах. Затем с помощью API он конструирует ряд объектов и использует их, чтобы создать фиктивного клиента [программу], клиентов [компании], местоположения и выполнить все действия API над созданными документом фиктивными данными, а затем удалить эти данные при помощи самого документа.

При этом если в моём API есть ошибки, то они отображаются в документации API. Он также действует как интеграционный тест, подробный настолько, насколько вы захотите.

Каждый запрос, выполненный Emacs при экспорте в HTML, отображается в Emacs minibuffer и логируется в буфере Emacs *Messages*, так что ошибки можно найти с помощью инкрементного поиска, не читая index.html.

Исходник org

P. S.

В итоге я написал yasnippet. Введите lit-api и C-<tab> и переходите от поля к полю.

А продолжить изучение тестирования или разработки вы сможете на наших курсах:

Узнайте подробности здесь.

Другие профессии и курсы

Комментарии (7)


  1. fougasse
    02.02.2022 23:02
    -1

    Тема интересная, но читать из-за обилия лишних зяпятых и другой пунктуации очень тяжело.


  1. Shtucer
    03.02.2022 05:29

    Перевод, конечно, кхм... Да и проверять за иностранным специалистом никто не стал, так вот:

    #+begin_src shell :var response=get-posts :results value raw :wrap src js :exports results
      echo $response | jq
    #+end_src

    Результат экспорта этого блока будет совсем не то, что показано на картинках. Потому что :results value raw вернёт неформатировнный... код ошибки выполнения shell-команды. А чтобы как на картинках надо бы :results output raw.


  1. vtb_k
    03.02.2022 16:55

    Уже лет 5 использую restclient, очень удобно. Выглядит попроще, чем орг мод


    1. Shtucer
      04.02.2022 06:32

      При сравнении тёплого с мягким, мягкое проиграло...


      1. vtb_k
        04.02.2022 11:12

        Не понял вашей иронии.

        Я использую restclient, как замену postman. И в этом же контексте сравниваю его со способом из статьи.


        1. Shtucer
          05.02.2022 07:58

          Если бы, вы прочитали статью дальше заголовка, то поняли бы, что заголовок не соответствует содержанию. А если бы ещё хотя бы взглянули на оригинал, то вам стало бы ясно, что перед нами яркий пример всратой "адаптации" вместо перевода.

          Объясню. В статье не рассматривается способа замены restclient'а org-mod'ом. В статье описывается способ оформления документации REST API с помощью org-mod'а И restclient'a.

          Сравнивать restclient и org-mod это нуууу очень странная идея. Это совершенно разные вещи.


          1. vtb_k
            05.02.2022 09:25

            Сравнивать restclient и org-mod это нуууу очень странная идея. Это совершенно разные вещи.

            Да, теперь я понял в чем проблема. Заголовок сбил меня с толку и я подумал что речь идет о другом пакете restclient специально для орг мода. Я наверное единственный пользователь Емакса, который не пользуется при этом орг-модом)