К старту курса по тестированию на 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 и хотим, чтобы она была чистой и читабельной. Потратим немного времени на внешний вид документации.
Сейчас у нас некрасивый документ:
Якоря к заголовкам выглядят некрасиво и читаются как index.html#orga1b1b2d.
Ответ слишком большой. Чтобы добраться до следующего запроса, придётся прокручивать страницы.
Заголовки ответа выгружаются в нижней части ответа. Это не чистый 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. Он создан только для результатов. В заголовке блока происходит несколько важных вещей:
На этот раз мы вызываем оболочку. Любую (bash, cmd.exe, fish и так далее), которую вы используете.
Устанавливаем новую переменную :var response=get-posts. Теперь этот блок src будет вызывать блок запроса с именем get-posts и помещать результаты в переменную с именем response.
Устанавливаем :results value raw, потому что не хотим, чтобы Org обернул результаты в тип shell.
Устанавливаем :wrap src js, потому что на самом деле хочется, чтобы Org обернул результаты в тип js.
Устанавливаем :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. Вы можете скопировать, вставить и изменить эти части кода:
:CUSTOM_ID: method-get-posts на method-post-posts.
"Запрос" #+name: c get-posts в post-posts
"Ответ" :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.
P. S.
В итоге я написал yasnippet. Введите lit-api и C-<tab> и переходите от поля к полю.
А продолжить изучение тестирования или разработки вы сможете на наших курсах:
Узнайте подробности здесь.
Другие профессии и курсы
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также