Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.
Хой, джедаи и амазонки!
Продолжаю осваивать новые навыки, необходимые для джуниор-разработчика. В предыдущей публикации улучшал навык с СУБД SQLite, сейчас хочу наработать навык с проектированием спецификаций по стандарту OpenAPI: однажды часть тестового задания была разработка сервиса по такой документации и требовалось сгенерировать свою. Спецификацию я тогда написал вручную и потратил на это несколько дней.
Практиковать автоматизацию документирования API просто так не интересно - решил немного разобраться, как связаны бэкенд и фронтенд. Для этого напишу простой html-код и буду направлять его клиенту - браузеру; будет возможность на веб-странице нажимать на кнопки, создавая запросы для сервера. Намного интереснее тестировать и видеть свою работу, чем просто при работе с Postman без визуальной составляющей в браузере.
Когда вы прочитаете эту публикацию и повторите шаг за шагом каждое действие, вы разберётесь, как автоматически создавать спецификацию API такого типа:
Публикация получилась длинная, вперёд - разбираться.
1. Немного теории и рекламы
Здесь хочу прорекламировать бесплатный курс на Stepik, на котором когда-то давно учился верстать страницы - "Веб-разработка для начинающих: HTML и CSS":
На курсе мы поэтапно верстали аналог страницы о г.Хьюстон с Википедии.
Вам, конечно, необязательно проходить этот курс, чтобы попрактиковать фронтенд, для добавления к своему бэку визуальную часть. Я, чтобы не вспоминать все эти теги, заголовки и прочее из мира вёрстки сайтов, воспользовался услугами нейросети.
По поводу теории, просто напомню, что такое сваггер.
Swagger - это инструментарий для разработки документации на API по стандарту OpenAPI.
OpenAPI - это правила оформления документа для документирования REST API, выполняется в форматах json или yaml. Такая спецификация позволяет разработчикам четко и последовательно определять структуру API, включая конечные точки, параметры, заголовки, ответы, ошибки и другие вещи.
Пример текстового описания API по стандарту OpenAPI ниже:
Согласитесь, вручную писать такое - мягко говоря затруднительно.
Для меня, типичный сценарий, когда в реальной разработке нужна эта документация по стандарту OpenAPI, выглядит так:
Команда разработчиков создаёт веб-сервис и фиксируют документально, какие эндпоинты будут доступны через API, как они организованы, какие запросы и ответы должен поддерживать сервис.
Например, будут эндпоинты: /users, /users/{id} /products/cheese и т.д.
Эндпоинт /users поддерживает запрос GET и ожидает ответ формата yaml с конкретными полями и должны быть предусмотрены определённые статусы ответа сервера; а эндпоинт /products/cheese поддерживает запрос POST и предусматривает передачу формата json со своими полями...
Далее этой информацией будут руководствоваться в процессе разработки веб-сервиса новые программисты, QA-инженеры, сторонние разработчики, хотят интегрировать какое-то своё приложение в этот веб-сайт, или кто-нибудь ещё, глядя на относительно простую и понятную спецификацию API в графическом редакторе
Пример визуализации спецификации по стандарту OpenAPI из шаблона Swagger Editor представлен ниже:
Всю эту информацию можно получить и изучая код - но для этого нужно потратить несоразмерно больше времени и обладать способностью "читать чужой код".
В следующем параграфе мы разработаем приложение, и лишь после этого подготовим спецификацию по стандартам OpenAPI инструментарием Swagger.
2. Разрабатываем веб-сервис
2.1. Структура сервиса
Чтобы не переделывать или дорабатывать сервис, продумаю его архитектуру заранее. Какие у меня есть требования к сервису:
- Минимализм, чтобы не уходить в дебри в целях обучения;
- Наличие CRUD-операций (Create-Read-Update-Delete), чтобы попрактиковать написание спецификации по стандарту OpenAPI в разных вариантах;
- Наличие фронтенда - простейшие html-документы, возможно даже без CSS;
- Наличие json'а в передаваемых клиентом и/или отправляемых сервером данных.
- Без базы данных для минимализма кода сервиса. Хранить входящую информацию будем в файлах.
Исходя из вышеизложенных требований, создал такую структуру:
Я нарочно не стану менять скриншот, чтобы посмотреть - в процессе разработки такого простого сервиса, потребуется ли обновлять структуру, и если да - насколько значимы будут обновления.
Кстати, в целях подготовки к работе с сервером через терминал, выполнил команды по созданию каталогов и файлов через CLI:
Сперва создал файл go mod, затем одной командой mkdir создал три каталога, и далее одной командой touch создал несколько файлов.
2.2. HTML-файл
Чередой запросов-ответов, нейросеть подготовила для меня подходящую страничку .html. Откроем её в браузере без написания какого-либо бэкенда:
Интерфейс веб-страницы поделён на несколько блоков, указаны CRUD-операции.
В html-файле есть три составляющие:
- Сама html-разметка страницы;
- Стили элементов страницы;
- Код на JavaScript.
Стили обычно пишут в отдельном CSS-файле, как и код на JavaScript - не встраивают в html-документ, а создают отдельный файл с расширением .js. Для наших целей удобнее поместить всё в один файл. Ниже примеры фрагментов каждой из трёх составляющих документа.
Если со стилями и разметкой веб-страницы, я думаю, всё понятно, то что делает код JavaScript во фронтенде? В моём коде у него следующие функции:
- Обрабатывает клики на кнопки: мы нажимаем не на текстовые ссылки, а на графические элементы;
- Формирует url-запроса для отправки на сервер;
- Формирует json для тела запроса перед отправкой запроса на сервер;
- Обрабатывает ответ сервера: показывает уведомление о статусе операции или полученные данные.
При загрузке веб-страницы в браузере, код JavaScript выполняется уже на стороне клиента: в браузерах есть встроенные движки для обработки и выполнения JavaScript-кода, как и html и css-разметки.
По п.2 из перечня выше: url-запросы содержат параметры для GET, PUT и PATCH методов согласно принципам REST. Для метода GET не принято передавать тела запроса, а какой именно товар нужно получить, передаётся в url в виде параметра ключ-значение либо расширением эндпоинта, например:
- /home/item/apple - расширение эндпоинта (используется в моём html-документе);
- /home/item?product=apple - добавление параметра к запросу (product - поле json-структуры для общения клиент-сервер, описанной в html-документе).
Переходим к написанию бэкенда.
2.3. Основа backend'а
Напишем минимальный необходимый код для отображения веб-страницы:
Хорошо, сервер запустили, веб-страницу в браузере запустили командой
http://localhost:8080/
Кстати, если вы будете экспериментировать и веб-страница будет отображать не то, что ожидается, проблема может быть с КЭШем, для решения попробуйте:
- Запустите режим "инкогнито" в браузере сочетанием клавиш Ctrl+Shift+N и откройте веб-страницу там - посмотрите, что будет;
- Перезагрузите страницу вне режима "инкогнито" с очисткой КЭШа сочетанием клавиш Ctrl+Shift+R.
По части кода сервера хочу отдельно проговорить о функции Handle пакета http.
Обработчик http.Handle в строке 9 на иллюстрации будет отдавать контент, находящийся в каталоге templates за счёт функции FileServer того же пакета http при любом запросе из браузера (на соответствующий хост и порт). Сейчас функция отдаёт html-файл, который находится у меня в каталоге:
Вопрос: что отдаст сервер, если в этом каталоге несколько файлов? Например, мой hmtl файл и несколько иллюстраций или что-то ещё, например, текстовый файл? Вот примерно так:
Ответ: по-прежнему веб-страницу. Почему? Переходим в документацию, читаем о функции FileServer пакета http:
Суть в чём - в первую очередь функция FileServer ищет в переданном в неё каталоге файл index.html, и отдаёт клиенту его. А вот если такой функции не находит, т.е. в каталоге будет например только два указанных выше файла:
То при загрузке веб-страницы командой localhost:8080, мы получим список файлов из каталога для выбора - что всё-таки хотим загрузить:
Вернём файл index.html в каталог templates, удалим лишние файлы, и продолжим разработку веб-сервиса.
2.4. Обработчик Create: POST-запросы
Операция Create нужна для сохранения данных в хранилище по запросу клиента. Общаясь в соцсети при отправлении текстового сообщения или фотографии, заполняя ФИО в профиле и многое другое - это POST-запросы. Мы постоянно создаём новые данные, которые хранятся на серверах.
2.4.1. Алгоритм запроса клиента
Напишем первый обработчик, который будет обрабатывать запросы клиента при нажатии кнопки "Создать":
Как указано на иллюстрации, команда Create относится к POST-запросу согласно принципам REST и протоколу http. Происходить будет следующее:
- Пользователь вводит в браузер адрес сайта;
- Браузер обращается к серверу по url и сервер отдаёт браузеру файл index.html с разметкой веб-страницей, стилями и кодом JavaScript;
- Пользователь смотрит на появившуюся в браузере веб-страницу, заполняет на веб-странице соответствующие поля и нажимает кнопку "Создать";
- Драйвер браузера обрабатывает код, написанный на JavaScript внутри файла index.html и отправляет серверу json с полями { product и price } с типами данных string и float соответственно по url /home/create_item.
На этом моменте нам нужен обработчик соответствующих событий.
2.4.2. Первая версия обработчика POST-запроса
Реализацию обработчика напишем в другом пакете - в файле /handlers/handlers.go. Но сперва напишем не обработчик, а сделаем внутри этого пакета ещё два файла:
В файле msg буду прописывать в константах код ошибок и информационных сообщений, которые будут повторяться в разных обработчиках. А в файле types пропишу структуру с полями json, которые передаёт фронтенд. Сейчас содержимое файлов выглядит так:
Переходим к реализации обработчика:
В строке 11 log.Printf... я пишу лог, который будет публиковать в терминал информацию о поступившем запросе с указанием с какого url пришёл запрос, протокол передачи данных и метод запроса. Константа msgNewRequest определена в другом файле этого же пакета, см. иллюстрацию выше.
В строке 13 мы создаём экземпляр структуры, сама структура описана в другом файле этого же пакета.
В случае неудачи декодирования переданного клиентом json'а, мы выводим сообщение об ошибке логом в терминал и отправляем клиенту статус ошибки.
http.StatusBadRequest - это константа в виде числа. Хорошая практика прописывать статусы ответов не в виде чисел типа 404 или что-то ещё, а использовать константы, заботливо предоставленные разработчиками языка Go:
Ну и в конце я пока просто печатаю в терминал содержимое декодированного json-файла, чтобы посмотреть - то вообще пришло, или не то, что ожидалось.
2.4.3. Тестирование первой версии обработчика POST-запросов
Прототип обработчика POST-запросов написан, проверяем. Запускаем приложение в терминале:
Переходим в браузер, вводим информацию в поля раздела "Создать товар" и нажимаем кнопку "Создать". Появляется сообщение:
Переходим в терминал, смотрим что он пишет:
Отлично, информация от клиента получена и декодирована.
Доработаем обработчик:
- Добавим функцию сохранения информации в файл (мы при определении архитектуры приложения решили, что не стоит перегружать учебный проект подключением БД, и ограничимся сохранением информации в файл).
- Добавим проверку, что запрос с данного URL поступил с методом POST, а, например не GET, чтобы не делать лишнюю логику при ошибке.
- Изменим тип контента ответа: сейчас он был application/json, но я не возвращаю json-ов, а возвращают простой текст и то, если будет ошибка.
2.4.4. Вторая версия обработчика POST-запросов
Обновил код обработчика:
Функция сохранения полученной от клиента информации в файл:
В коде сохранения мы делаем следующее:
- В строке 43 if ok = ... проверяем, все ли поля json-структуры заполнены: хотя это должно быть реализовано на стороне клиента в JavaScript, но мы фронтендерам доверять не станем и реализуем свои проверки. Детали реализации пока не важны, важно что либо все поля в json переданы и декодированы, либо нет.
- Создаём каталог, если его нет, в котором будем хранить информацию.
- Определяем имя файла по названию товара, переданного в json-поле procuct от клиента.
- Если файл с таким именем уже есть - возвращаем ошибку.
- Преобразуем цену товара в строку.
- Создаём файл с наименованием товара, а внутрь помещаем стоимость товара, переданную в json-поле price.
- Печатаем лог об успешном завершении сохранения данных и возвращаем отсутствие ошибки.
В пакете я объявил константы, необходимые для функции сохранения:
О константе perm и 0644 я писал в публикации #33. Права доступа к файлам и основы обработки ошибок.
Так выглядит функция проверки полей, декодированных в экземпляр структуры из json-объекта клиента:
Также я обновил содержимое файла msg, указав там новые сообщения в виде констант и доработал существующие константы:
2.4.5. Создаём несколько записей в хранилище и тестируем
Итак, обработчик доработан бизнес-логикой. Загрузим на сервер несколько товаров со стороны клиента:
Проверим через терминал содержимое каталога, куда сохраняем товары и цену на последний добавленный товар:
Всё в порядке: в каталоге появляются файлы, в файлах - цена товара.
Кстати, фронтенд не позволяет отправить запрос без заполнения любого или обоих полей:
Но если бы фронтенд не предусматривал такой защиты от ошибок, мы реализовали этот код на стороне сервера: проверим его через postman, в который мы можем прописать свой json, чего не можем сделать через браузер:
Как и ожидали - ответ с ошибкой 400 и сообщением в теле ответа об ошибке.
То же самое можем получить, если в одном терминале запустим сервер, а в другом создадим запрос через утилиту curl, ниже на иллюстрации представлены два терминала - хотя зрительно видимость одного:
Проверим ещё в Postman'e, что будет, если сделаем не POST-операцию, а какую-нибудь другую:
Как и ожидалось - в теле ответа информационное сообщение, и код статуса ответа 405 "Method Not Allowed".
Посмотрим, что пишет терминал:
Терминал соответственно тоже выдаёт лог ошибки.
2.4.6. Третья версия обработчика POST-запроса
Пока я писал этот пост, лучше разобрался с архитектурой REST. И вот что выяснил: для POST-запросов необходимо возвращать код статуса 201, а не 200 и информацию о созданном контенте. Пусть это будет имя созданного файла. Доработаем реализацию обработчика:
Доработаем функцию сохранения - теперь она возвращает имя файла по относительному пути:
При работе из браузера, визуально ничего не изменится. Протестируем обработчик через Postman:
В теле ответа появилась информация о созданном ресурсе. Думаю, можно считать этот обработчик, спроектированным по принципам REST.
Итак, первый эндпоинт обработали. Остаётся ещё 4.
2.5. Обработчик Update: PATCH-запросы
Изменяя отправленное сообщение в соцсети, редактируя свой профиль в Тенчат (отечественный аналог LinkedIn), изменяя язык или настройки уведомления на веб-странице, редактируя пост в блоге, изменяя пароль или адрес доставки в интернет-магазине - мы создаём PATCH-запросы: изменяем ранее созданные данные.
Patch можно перевести с английского, как "заплатка". Это своего-рода доработка ранее пошитой одежды.
2.5.1. Данные от клиента
Разберём, что отдаёт нам клиент при Patch-запросе.
- Клиент отдаёт URL вида: /home/update_item/<ТекущееНазваниеТовара>
- Json-объект вида: { "price": 190.60 }
Таким образом, нам нужно расшифровать конечную часть энпоинта в наименование текущего товара, найти файл с таким именем и перезаписать содержимое файла обновлённой ценой.
В случае успеха обновления будем возвращать статус ответа 204 - работу выполнили, но тела ответа не направляем. Статус 204 (как и все остальные коды статусов) определён константой в пакете http как StatusNoContent.
2.5.2. Первая версия PATCH-запроса
Добавляем обработчик обновлений товара в функцию main пакета main:
Пишем реализацию обработчика:
Вообще, декодирование джейсона и создание экземпляра структуры - такое полезнее делать на отдельном слое (в другом пакете, предназначенном для соответствующих задач). С опытом это будет уже автоматически делаться, а сейчас напишем как пишется, а далее, если будет желание - отрефакторим.
В функции должно быть всё понятно - делаем примерно то же самое, что при реализации обработчика сохранения файлов.
Затем реализуем функцию обновления данных:
В строке 96 получаем из url название товара.
В строке 98 присваиваю полю Product экземпляра структуры значение товара, т.к. в текущем экземпляре структуры это поле отсутствует - клиент передал json-объект с полем. А я хочу воспользоваться функцией по проверке корректности декодированных в экземпляр структуру полей и не создавать новую функцию: существующая функция, созданная ранее, проверяет оба поля и выдаст ошибку, если не найдёт поля Product.
В строке 107 перезаписываю файл новым содержимым.
Ещё я решил вывести в отдельную функцию процесс преобразования типа float64 в строку текста:
В остальном код похож на сохранение файла в POST-запросе. Переходим к проверке.
2.5.3. Проверяем работу обработчика PATCH-запросов
Заполняем поля на веб-странице:
Смотрим, что пишет терминал сервера:
Так, мы видим что получен запрос и напечатан товар, расшифрованный из эндпоинта. Мы забыли добавить лог успешного обновления. Это мы сделаем, а сейчас проверим что там в содержится в файле "Дыня.txt":
Так, у нас нет информации - что было в файле до обновления. Обновим в браузере данные вновь и посмотрим на содержимое файла:
Итак, данные вновь обновлены. Мы молодцы. Переходим к следующему запросу.
2.6. Обработчик Update: PUT-запросы
PUT-запросы похожи на PATCH, но обновляют все данные, а не часть. Честно говоря, я не разобрался, когда нужны PUT, а когда PATCH-запросы: PUT в моём понимании менее применим. А так его цель та же - обновить ранее созданные данные, но если PATCH обновлял часть данных, то PUT перезаписывает все данные.
2.6.1. Данные от клиента
На веб-странице мы обновляем все данные о товаре:
Разберём, что отдаёт нам клиент при PUT-запросе.
- Клиент отдаёт URL вида: /home/update_item/<ТекущееНазваниеТовара>
- Json-объект вида: {"product": "НовыйТовар", "price": 200.50 }
Как и для PATCH-запроса, нам нужно расшифровать конечную часть энпоинта в наименование текущего товара, найти файл с таким именем и перезаписать содержимое файла обновлённой ценой и наименование файла с новым товаром.
В случае успеха обновления будем возвращать статус ответа будет таким же 204 - работу выполнили без создания нового ресурса, и тела ответа не направляем.
Вот что ещё я обнаружил - что клиент отправляет нам по одному url запросы типа PATCH и PUT: /home/update_item/<ТекущееНазваниеТовара>
Если мы не хотим использовать сторонние библиотеки, где при объявлении обработчиков можно сразу указать метод, то нам потребуется сделать отдельную функцию.
2.6.2. Первая версия обработчика PUT-запросов
Функцию main пакета main не будем обновлять: у нас по одному эндпоинту будут обслуживаться два типа запросов: PUT и PATCH.
Создадим новую функцию, которая будет смотреть, какой метод пришёл с /home/update_item/<ТекущееНазваниеТовара>, доработаем реализацию обработчика PATCH и напишем реализацию обработчика PUT.
Код написан, добавлены несколько констант с сообщениями об ошибок, которые я приводить здесь не буду, т.к. их содержимое не принципиально и интуитивно понятно. Тестируем сервис.
2.6.3. Тестируем PUT-запросы
Заполняем соответствующие поля на веб-сайте:
Проверяем, что в терминале:
Сервис отработал корректно: заменил имевшийся файл Дыня со своим содержимым на товар Арбуз с новым содержимым. Проверим содержимое каталога и файла:
Всё на своих местах. Переходим к следующему типу запроса.
2.7. Обработчик DELETE-запросов
DELETE-запросы удаляют ресурсы: ненужные записи в мессенджере или блоге, фотографии, контакты в телефонной книжке (в т.ч. из облачного хранилища) и т.д.
2.7.1. Данные от клиента
На веб-странице мы обновляем все данные о товаре:
Разберём, что отдаёт нам клиент при DELETE-запросе.
- URL вида: /home/delete_item/<НазваниеТовара>
В случае успеха удаления будем возвращать статус ответа таким же, как при PUT и PATCH: 204 - работу выполнили, и тело ответа не направляем.
Если товар не будет найден, вернём клиенту код статуса 404 - ресурс не найден.
В случае других ошибок будем возвращать статусы кодов:
- Передан не DELETE-запрос: код 405 - статус не разрешён;
- Не смогли выполнить запрос: код 500 - внутренняя ошибка сервера.
2.7.2. Первая версия обработчика DELETE-запроса
Добавим обработчик в функцию main пакета main:
Реализуем обработчик:
Особенность этой реализации в том, что из функции, отвечающей за бизнес-логику я возвращаю ошибку и статус: если сервер "пятисотит" - это один ответ клиенту, а если товар не найден - т.е. код статуса 404, то ответ клиенту другой. В остальном логика та же, как в предыдущих запросах.
Реализация функции удаления товара:
Код написан, тестируем.
2.7.3. Реализуем DELETE-запрос
Заполняем поле на веб-сайте:
Смотрим, что пишет терминал:
Я использовал раздельный терминал: верхняя часть - работа сервера, нижняя часть - вспомогательный терминал, посмотрел содержимое каталога где хранятся данные о товарах: видно, что товар удалён.
Попробуем вновь удалить этот же товар:
Ответ сервера браузеру: нечего удалять. Смотрим, что пишет терминал:
Всё как и ожидалось: код работает. Переходим к следующему запросу.
2.8. Обработчик GET-запросов
GET-запросы запрашивают информацию: загрузка и просмотр веб-страницы, поисковой запрос, просмотр изображения из миниатюры и т.д.
2.8.1. Данные от клиента
На веб-странице мы ищем данные о конкретном товаре:
Разберём, что отдаёт нам клиент при GET-запросе.
- URL вида: /home/item/<НазваниеТовара>
В случае успеха получения информации, будем возвращать статус ответа 200 и json с информацией о товаре.
Если товар не будет найден, вернём клиенту код статуса 404 - ресурс не найден.
В случае других ошибок будем возвращать статусы кодов:
- Передан не GET-запрос: код 405 - статус не разрешён;
- Не смогли выполнить запрос: код 500 - внутренняя ошибка сервера.
2.8.2. Обработчик GET-запроса
Дополним функцию main пакета main:
Напишем реализацию обработчика GET-запросов:
В коде мы делаем следующее:
- Печатаем лог в терминал с информацией о полученном запросе;
- Проверяем корректность метода запроса;
- Вызываем функцию, возвращающую нужный для отправки клиенту json-объект, статус ответа ошибки если имеется и ошибку;
- Две проверки на коды статусов, если есть ошибки;
- Отправка кода статусу и json-объект клиенту.
Напишем функцию для подготовки json-объекта для отправки клиенту:
В коде выполняется следующее:
- Извлекаем из ULR наименование необходимого товара;
- Формируем имя файла для поиска товара;
- Выполняем проверку, есть ли такой файл;
- Вызываем функцию, возвращающую цену товара с типом данных float64 и ошибку;
- Выполняем проверку, удалось ли получить цену товара;
- Создаём экземпляр структуры и кладём в неё имя требуемого товара и его цену;
- Формируем json-объект из созданного экземпляра структуры;
- Выполняем проверку успешности создания json-объекта;
- Печатаем лог в терминал об успешном создании json-объекта;
- Возвращаем json-объект.
Напишем функцию чтения файла. Она читает файл и преобразует тип string в тип float64 - хотя считается для чистого кода, каждая функция должна делать что-то одно для гибкости кода, я считаю что эта функция лаконична и гибка:
К слову, название функции readFile не совсем отражает её действие - думаю, логичнее назвать её price или как-то в этом духе.
Итак, код написан, проверяем.
2.8.3. Тестируем обработчик GET-запроса
Заполняем требуемое поле в браузере:
Сразу получаем уведомление с json-объектом. Смотрим, что происходит в терминале:
В верхней части - терминал сервера, в нижней части - вспомогательный терминал, который показывает какие файлы в хранилище и содержимое файла запрошенного товара. Всё сходится.
На этом этапе можно заканчивать разработку сервиса и переходить к Swagger'у.
3. Инструментарий Swagger
Основная идея автоматической генерации спецификации API по стандарту OpenAPI, следующая:
- В коде пишем особые комментарии об API - своего рода, высокоуровневый код;
- Устанавливаем утилиту для генерации спецификации API;
- Запускаем утилиту и она генерирует спецификацию, преобразуя высокоуровневые комментарии в более низкоуровневые.
Далее эту спецификацию можем использовать для визуализации API - скриншоты в начале публикации.
3.1. Общая информация
Я нашёл несколько библиотек для автоматической генерации спецификации API по стандарту OpenAPI:
- Swaggo;
- go-swagge;
- gofiber;
- oapi-codegen;
- ogen-go.
Разбираться будем с первой - Swaggo. Установим его, ниже скриншот из официальной документации на GitHub:
Далее нужно каким-то образом сообразить, как дополнить комментариями наш код, чтобы Swaggo считал информацию и преобразовал в спецификацию API по стандарту OpenAPI. Ниже фрагмент информации по комментированию кода из ReadME библиотеки Swaggo:
Также имеется выбор библиотек по обработке http-запросов, поддерживаемых библиотекой Swaggo - здесь есть и стандартная библиотека net/http:
Суть в чём - нужно добавить комментарии согласно требованиям библиотеки Swaggo. Напишем минимальное количество комментариев для одного обработчика, чтобы протестировать как всё работает.
3.2. Документация на обработчик POST-запроса
Swagger-аннотирование делится на два этапа: общее описание проекта, которое выполняем в пакете main перед функцией main, и аннотации для каждого обработчика.
3.2.1. Минимальная спецификация по OpenAPI
Сделаю самый простой проект, чтобы опробовать технологию автогенерации.
Заполню общее описание в функции main комментариями title, version, description, contact name-url-evail, host и basePath - которые интуитивно понятны:
Описание получилось какое-то неотформатированное. Воспользуюсь командой swag fmt в терминале, результат:
Создал описание обработчика перед функцией-реализацией обработчика POST-запроса:
Посмотрим разъяснения по полям Accept, Produce и Param в ReadME библиотеки Swaggo - остальные поля, думаю, интуитивно понятны.
В документация есть ссылка на Mime Types.
MIME (Multipurpose Internet Mail Extensions) types — это стандартизированные идентификаторы, которые используются для указания типа содержимого, передаваемого через интернет. Изначально разработанные для электронной почты, сейчас MIME-типы широко применяются в HTTP-протоколе для указания типа файлов, передаваемых между клиентом и сервером.
Посмотрим, что в ReadME библиотеки Swaggo говорится о Mime Types:
В общем, по-русски говоря, аннотация @Accept говорит о том, какие данные в теле запроса ожидаются от клиента. В моём случае API ожидает от клиента в теле запроса json-объект.
Аннотация @Produce говорит о том, какой объект в теле ответа сервер возвращает клиенту: в данном случае также json-объект.
Аннотация @Param используется для определения параметра, который передаются в API-метод. Её расшифровка посложнее двух предыдущих аннотаций. Взглянем ещё раз на аннотацию:
// @Param item body Item true "Создаем новый товар"
или её можно представить так:
// @Param <param name> <param type> <data type> <is mandatory?> "<comment>"
Разберем теги аннотации Param подробнее:
1. Тег name - это просто название, чтобы по-названию объяснить структуру данных, которая ожидается из запроса. В нашем случае у тега name наименование item.
2. Тег param type указывает, где в запросе находится параметр, в нашем случае - в body. В ReadMe библиотеки Swaggo указаны следующие типы:
3. Тег data type определяет тип данных для объекта тега <name>. В нашем случае тип данных Item - это наша пользовательская структура, описанная в коде, см. параграф 2.4.2 публикации. Посмотрим, что написано о типах данных в Readme библиотеки Swaggo:
В общем, аннотация @Param через тег <data type> подтягивает структуру Item в спецификацию.
4. Тег is mandatory? определяет, требуется ли этот параметр для выполнения запроса (в нашем случае указано true, - требуется). Пока не могу представить примеров, где этот параметр будет false.
5. Тег comment задаёт описание параметра, чтобы читающие спецификацию специалисты понимали, что должно быть передано в этом параметре.
Теперь строка // @Param item body Item true "Создаем новый товар" не должна вызывать вопросов.
Ещё хочу сказать про аннотацию @Success - документирует, какой должен быть ответ при успешном выполнении запроса. Взглянем на описание аннотации в ReadMe библиотеки Swaggo:
В целом, всё знакомые теги.
Напомню, как выглядит аннотация в нашем коде:
// @Success 201 {object} Item "Товар успешно создан"
Тег {object} указывает, что в ответе будет передан объект, описанный следующим тегом. А следующим тегом у нас Item, а объект Item - это наша пользовательская структура, содержащая json-объект.
Взглянем вновь на написанные для обработчика аннотации:
Теперь уже, думаю, с новым пониманием того, что здесь указано.
Разобрались с аннотациями. Запускаем автоматическую генерацию спецификации по стандарту OpenAPI командой swag init в терминале:
На иллюстрации мы запустили генерацию документации, получили логи документации, посмотрели содержимое каталога где должна создаться документация и начало содержимого одного из файлов.
Может показаться, что мы пишем какой-то непонятный код в виде комментариев Swagger: для чего? Для того, чтобы получить другой код (фрагмент которого представлен в терминале)? А почему бы не убрать из цепочки транслятор. Можно относится к этому так:
При написании комментариев swagger, мы создаём высокоуровневое описание API, а библиотека swaggo выполняет роль интерпретатора, который использует наши аннотации для написания более низкоуровневого кода, понятного компьютеру.
Написание swagger-аннотаций в коде намного проще и гораздо быстрее, чем писать спецификацию на API по стандарту OpenAPI вручную.
3.2.2. Работа в Swagger Editor
Перейдём в браузере в Swagger Editor по пути https://editor.swagger.io/ и вставим в неё сгенерированную спецификацию. Результат:
Йуху! Мы создали нашу первую спецификацию по стандарту OpenAPI методом автоматической генерации. Можно раскрыть ручку POST и посмотреть что там:
Пока здесь нет понятных примеров запросов и кодов статусов возможных ошибок, заголовков - но это демо-версия. Со всем этим разберёмся далее.
Что ж, мы протестировали команды swagger и создали работающую спецификацию, идём дальше.
3.2.3. Доработка Swagger-аннотаций обработчика POST-запросов
3.2.3.1. Добавим описание нашему обработчику:
После генерации спецификации, этот код будет выглядеть в Swagger Editor так (выделил текст):
3.2.3.2. Добавим тег в шапку
Чтобы в шапке был тег не default, а осмысленный, добавим тег item. Он нам понадобиться, чтобы упростить понимание что происходит при обращении по /home/create_item:
После генерации спецификации, этот код будет выглядеть в Swagger Editor так:
3.2.3.3. Добавим пример для запроса
Напомню, как сейчас выглядит визуализация спецификации в части информации о запросе:
Есть возможность указывать не стандартные типы для полей json-объекта, а конкретные цифры и текст: это упрощает понимание API. В ReadMe хитро и как-бы вскользь указано описание этого функционала: нужно доработать нашу структуру, а конкретно - добавить к json-описанию поле example - оно должно называться именно так, и описание к нему, например:
После генерации спецификации, её визуализация будет выглядеть так:
Согласитесь, информация нагляднее.
3.2.3.4. Доработаем ответ
Напомню, как выглядит сейчас визуализация спецификации API:
Так выглядит аннотация, отвечающая за блок Example Value визуализации:
// @Success 201 {object} Item
Вспомним код, формирующий ответ на POST-запрос, вот его финальная часть:
Суть в чём: на запрос POST по REST нужно возвращать идентификатор созданного ресурса при успешном выполнении запроса, а не информацию о созданном товаре - как указано на текущий момент в спецификации API.
В моём случае, идентификатор - это имя созданного файла, а по спецификации API мы возвращаем в теле ответа тело запроса. Некорректно.
Что здесь можно сделать: написать что-то такое:
// @Success 201 {object} string
Чем нас не устраивает такая аннотация? В ней нет примера возвращаемого ресурса. Я так и не нашёл способа создать пример, кроме как добавить тег example к json-объекту. Поэтому я пошёл путём создания новой структуры с соответствующим тегом и возвращением её:
И текст аннотаций:
После этих изменений, сгенерируем документацию API и посмотрим, как она выглядит:
Меня это устраивает. Идём дальше.
3.2.3.5. Коды ошибок
Добавим теги для ошибок. По аналогии с предыдущим, успешным ответом, можно предположить что-то такое:
// @Success 400 {object} string
ну и указать комментарий
// @Success 400 {object} string "Ошибка валидации данных"
Как это будет выглядеть:
Мне не нравится в поле Example Value тип "string". Как его убрать оттуда? Можем вместо типа данных указать nil, и всё получится:
// @Failure 400 {object} nil "Ошибка валидации данных"
Напишем все аннотации на ошибки, возвращаемые нашим обработчиком:
Генерируем спецификацию, проверяем в визуальном редакторе:
Меня такая документация устраивает.
Мы написали расширенную документацию на обработчик POST-запроса. Если попрактиковаться - это просто, а главное - наглядно для других специалистов.
Напишем в таком же духе документацию на остальные обработчики.
Крайне рекомендую своими руками также написать документацию на каждый обработчик, чтобы научиться инструментам Swagger.
3.3. Документация на обработчик DELETE-запроса
Аннотации выглядят так:
Что здесь интересного: добавлен параметризованную часть эндпоинта {id}, и убрал аннотацию @Accept, т.к. клиент не передаёт информации в теле запроса.
Выглядеть это будет так:
Кстати, в предыдущем пункте не указал - можно "поиграть" и сделать пробный запрос, нажав кнопку Try it out:
При заполнении требуемых API полей (в данном случае заполнил только поле id - указал фрукт kiwi), Swagger Editor сформировал запрос.
Пока писал аннотации, понял что обработчики написаны с ошибкой в части типа возвращаемого клиенту контента. Возьмём сперва предыдущий обработчик - для POST-запроса, вот его код:
В строке 34 мы указали тип возвращаемых значений application/json, а сами в ошибках не возвращаем то возвращаем строку, т.е. тип text/plain, то не возвращаем ничего (кроме кода статуса). Это некорректно, и если мы хотим возвращать текст ошибки, то нужно преобразовывать их в json-объект, а не надеяться на продвинутый клиент, который может обработать контент несоответствующего типа согласно переданному сервером заголовку.
Вернёмся к обработчику DELETE-запросов, вот его код:
Здесь мы указываем тип возвращаемого контента клиенту, как простой текст, т.е. text/plain. В действительности, мы возвращаем текст, только для ошибок, а при успешном выполнении кода - ничего не возвращаем (кроме кода статуса). Ошибка ли это? Нет, это не ошибка.
3.4. Документация на обработчик PATCH-запроса
Swagger-аннотации к обработчику выглядят так:
Чтобы добавить пример заполнения тела запроса (см. стр. 154 на иллюстрации), создал структуру:
Что здесь ещё интересного: когда писал аннотации, обратил внимание, что обработчик не возвращает ошибку 500, хотя она возможна. В спецификации я прописал это, а в коде пока такой реализации нет.
Вот так, полезно писать документацию, чтобы выявить ошибки. Хотя на это есть тесты, и их тоже нужно писать, и выявленное отсутствие обработки ошибки сервера - это одна из причин, почему тесты обязательны.
Сгенерируем спецификацию, посмотрим на неё в Swagger Editor:
Документация меня устраивает, идём к следующему обработчику.
3.5. Документация на обработчик PUT-запроса
Напишем swagger-аннотации:
Что здесь интересного понял, когда заполнял аннотации: логика обработки POST-запроса такова, что мы создаём id с привязкой к наименованию ресурса, т.е. наименованию товара. Это не лучшая практика, и вот почему:
При смене наименования товара, у нас фактически изменился id ресурса, и по идее нужно в ответ PUT-запросу возвращать обновлённый id. Но у нас не создавался новый ресурс, и это не соответствует принципам REST.
Короче, по-хорошему нужно изменить логику. Например, для идентификатора ресурса использовать не наименование товара, а хеш, который будет формироваться так: наименование товара + время получения запроса. Так мы получим практически уникальное наименование файла, в котором храним цену.
Ладно, возвращаемся к спецификации. Сгенерируем её и посмотрим:
Всё в порядке, переходим к следующему обработчику.
3.6. Документация на обработчик GET-запроса
Напишем swagger-аннотации:
Смотрим, как выглядит визуализация в Swagger Editor:
Что ж, документация на все обработчики готова. Теперь я хочу обновить swagger-аннотации к общему описанию проекта: затронуть больше доступных тегов.
3.6. Расширяем общее описание проекта
Сейчас, с документированием обработчиков, спецификация API в Swagger Editor выглядит так:
Аннотации к общему описанию сейчас выглядят так:
Здесь указаны 8 swagger-аннотаций. В ReadMe библиотеки Swaggo для общего описания проекта предусмотрено 22 аннотации. Используем этот функционал по-максимуму и напишем аннотации:
Визуализируем спецификацию:
Все эти поля определены в ReadMe библиотеки Swaggo, продублирую свою интерпретацию из ReadMe, а после перечня дополню своими соображениями по сложным аннотациям. Также в тексте перечня ниже в скобках фразой "был ранее" обозначено, что эти аннотации были в первой версии описания проекта:
- @title: название API или проекта (был ранее).
- @version: версия API или проекта (был ранее).
- @description: описание API, объясняющее назначение и функционал (был ранее).
- @termsOfService: URL для условий предоставления услуг. Это может быть юридическая информация о том, как и когда можно использовать API, ограничения ответственного использования и тому подобное.
- @contact.name: имя контактного лица или разработчика (был ранее).
- @contact.url: URL для обратной связи, например, GitHub, (был ранее).
- @contact.email: Email-адрес для связи (был ранее).
- @license.name: название лицензии, под которой выпущен API (например, MIT). Лицензия определяет, как другие могут использовать код, что важно для Open Scource проектов.
- @license.url: URL, по которому можно найти текст лицензии.
- @host: хост, на котором развернут API (был ранее).
- @BasePath: базовый путь для API, обычно используется для определения начального пути для эндпоинтов (был ранее).
- @accept: перечень форматов, которые API принимает (например, json, xml, plain) - обратите внимание, перечисление форматов в аннотации через пробел.
- @produce: перечень форматов, которые API возвращает.
- @query.collection.format: формат, используемый для определения типа массива данных в запросах, если предусмотрено API (например, multi для передачи нескольких значений) - см. скриншот из ReadMe библиотеки Swaggo и дополнительные разъяснения ниже.
- @schemes: протоколы, поддерживаемые вашим API (например, http, https) - обратите внимание, в аннотации перечисление протоколов (если оно есть) выполняется через пробел.
- @externalDocs.description: описание внешней документации, связанной с API - например, чтобы дополнительно прояснить некоторые моменты. Эта строка используется, когда есть, например, специальные руководства по работе с API.
- @externalDocs.url: URL для внешней документации.
- @x-name: пользовательское расширение для документации, может быть использовано для добавления специфической информации. Эта аннотация предназначена для случаев, когда нужно добавить специфическую информацию, которая не вписывается в стандартные аннотации. Она может быть полезна для разработчиков API, чтобы включить метаданные или специальные расширенные функции.
- @tag.name: название тега для группировки эндпоинтов (например, items).
- @tag.description: описание тега.
- @tag.docs.url: URL для документации, связанной с тегом.
- @tag.docs.description: описание документации по тегу.
Теперь разберём спорные, или просто, вызывающие сложности, аннотации.
@termsOfService и @license.name/@license.url - не разобрался, чем они отличаются. Вероятно, если у вас какая-то нестандартная лицензия - нужно указать url для её чтения в аннотации @termsOfService.
@query.collection.format - эта аннотация управляет тем, как API ожидает получение массивов параметров в запросах - если ожидает, как я понимаю. Вот что говорится об этом в ReadMe библиотеки Swaggo:
Разберём форматы на примерах:
csv (Comma-Separated Values): использует запятые для разделения значений.Пример: GET /items?ids=1,2,3
ssv (Space-Separated Values): использует пробелы для разделения значений.Пример: GET /items?ids=1 2 3
tsv (Tab-Separated Values): использует табуляции для разделения значений.Пример: GET /items?ids=1\t2\t3
pipes: использует символы "вертикальная черта" для разделения значений.Пример: GET /items?ids=1|2|3
multi: позволяет отправлять одно и то же имя параметра множество раз, в котором каждый экземпляр параметра представлен отдельно.Пример: GET /items?id=1&id=2&id=3
У меня в API нет поддержания массива в запросах. А теперь, когда посмотрели примеры форматов, если ранее с ними знакомы не были - теперь становится понятно, что за массив данных в запросах определяет аннотация @query.collection.format.
Аннотация @x-name - это документирование API не по стандарту OpenAPI, и визуализироваться в Swagger Editor она не будет. Она может быть полезна, если нужно передать дополнительные данные, которые затруднительно выразить стандартными аннотациями OpenAPI. Например, у себя я ввёл такую запись:
// @x-name {"environment": "production", "version": "1.0.0", "team": "backend"}
Суть её в чём - данные передаются как json-объект, а значение полей я предполагаю такое:
- environment: указывает, в каком окружении работает API (например, production или development).
- version: версия API, которая может быть полезна для отслеживания изменений в зависимости от окружения.
- team: название команды, которая отвечает за разработку данного API.
Разобрались со всеми аннотациями для общего описания. Я, пожалуй, уберу из своей спецификации несколько аннотаций, которые не актуальны для моего API и могут сбивать с толку:
- @query.collection.format, т.к. у меня API не поддерживает массив запросов;
- @termsOfService - у меня нет документации, определяющей особые условия использования API, неоговоренные стандартной лицензией.
Выглядит теперь документация так:
На этом основной разбор проектирования документации на API по стандарту OpenAPI закончен. Далее посмотрим, что ещё интересного можно добавить в разработку документации API.
3.7. Интеграция Swagger UI
Мы можем подключить Swagger UI на нашем сервере, чтобы пользователи могли сразу видеть интерактивную документацию API из браузера, при обращении к серверу, а не копировать спецификацию API на сайт Swagger Edotor.
Воспользуемся сторонней библиотекой http-swagger - это обёртка стандартного пакета net/http для автоматической генерации API:
Допишем обработчик в функцию main:
Теперь, когда мы запустим сервер и в браузере перейдём по url
http://localhost:8080/swagger/
мы вызываем функцию http.Handle("/swagger/", ...), которая использует обработчик WrapHandler пакета http-swagger (httpSwagger).
Swagger UI будет автоматически загружать необходимые HTML/CSS/JS-файлы для отображения интерфейса в виде веб-страницы в браузере.
Посмотрим, что находится в функции WrapHandler, а конкретно я хочу показать код, формирующий фронтенд:
Как видим - в функции сохранён фронтенд, написанный на языке разметки html.
Хорошо, немного посмотрели, что под капотом этой интересной функции, идём дальше.
По-умолчанию Swagger UI требует спецификацию в формате json, поэтому мы также прописываем обработчик с эндпоинтом .../doc.json и используем специальную функцию для возвращения клиенту одного файла - ServeFile. Возвращаем мы .json-файл спецификации API по стандарту OpenAPI, сгенерированного автоматически на предыдущих шагах командой
swag init
Когда мы переходим на
http://localhost:8080/swagger/
Swagger UI автоматически делает запрос к обработчику эндпоинта
/swagger/doc.json
(это поведение Swagger UI по умолчанию). Это происходит потому, что в конфигурации Swagger UI задается URL спецификации, который по умолчанию указывает на doc.json. Почему я об этом подробно говорю? Т.к. по-умолчанию, команда swag init генерирует три файла:
Ладно, запустили сервер, перешли в браузер и ввели команду http://localhost:8080/swagger/ - смотрим, что получилось:
Успех! Мы смогли настроить наш сервер так, чтобы он обрабатывал входящие запросы и через интерфейс Swagger UI, визуализировал нашу спецификацию в виде веб-страницы, как если бы мы копировали её вручную из файла и вставляли в соответствующее поле на сайте Swagger Editor. Удобно.
На этом разбор автоматической генерации спецификации по стандарту OpenAPI считаю законченной с уточнением по безопасности.
В ReadMe библиотеки Swaggo есть блок, посвящённый безопасности:
Безопасность осуществляется через аутентификацию и/или авторизацию пользователя. Согласитесь - это полезная штука: без такой фичи любой пользователь может перейти по url и посмотреть документацию на наш сервер, это как если любой желающий может посмотреть паспорт или другие личные документы: опасно.
Но аутентификацию и авторизацию я пока не изучал, не представляю какие есть способы для этого. Оставим это на домашнее задание.
Код приложения на GitHub >>> здесь <<<
4. Дополнения
В ходе написания публикации, я выявил недочёты и опечатки в аннотациях, которые по-хорошему нужно доработать в сервисе; в целом они не влияют на порядок действий для автоматической генерации спецификации API, поэтому в этом проекте код переделывать не буду, а в следующих учту недочёты, и сделаю сервисы лучше.
Получил обратную связь от опытного разработчика. Информация следующая:
1. Не нужно использовать snake_case в ручках, только kebab-case. Т.е. вместо
/home/create_item
нужно писать
/home/create-item
2. Ручки в ответ должны отдавать структуру, лучше json вида: OKResponse {status: ok, body: data} или ErrorResponse{ code: 500, message: error}, например:
Т.е. не просто статус кода в случае ошибки отдавать в ответе, а дополнительно json-объект. Может возникнуть вопрос: зачем это дублирование статуса. Возможный ответ:
Статус, устанавливаемый в заголовке при ответе сервера и статус json ответа, который будут ждать фронты и другие потребители ручек, будут благодарны, если будешь отдавать стандартизированный json ответ, если речь идёт об OpenAPI и сгенерированные на его основе методы и структуры данных.
5. Выводы
Идея публикации - научиться применять автоматическую генерацию спецификации API по стандарту OpenAPI, - выполнена. Значит ли это, что мы полностью освоили библиотеку Swaggo, даже со скидкой на отсутствие безопасности? Нет. Значит ли это, что мы освоили базовый функционал и часть ситуативного функционала библиотеки Swaggo для генерации спецификации API? Да.
Также мы в процессе разработки сервера коснулись некоторых основ REST API. Постепенно наш уровень растёт, осваиваются новые инструменты, и мы развиваемся, как разработчики.
Благодарю, что дочитали эту публикацию до конца. Продолжаем развиваться технически, улучшать свой характер и у нас всё получится. Будем на связи. Успехов!
Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨💻👩💻👨💻